En este artículo, aprenderá acerca de los principios SOLID. Obtendrá un entendimiento de cada principio junto con ejemplos de código en Java.
Los principios SOLID son un conjunto de cinco principios de diseño utilizados en programación orientada a objetos. Respetar estos principios le ayudará a desarrollar software robusto. harán que su código sea más eficiente, legible y mantenible.
SOLID es un acrónimo que se refiere a:
- Principio de Responsabilidad Única
- Principio Abierto/Cerrado
- Principio de sustitución de Liskov
- Principio de Segregación de Interfaces
- Principio de Inversión de Dependencias
Principio de Responsabilidad Única
El principio de responsabilidad única establece que cada clase debe tener una sola responsabilidad, un solo motivo para cambiar.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
En el ejemplo anterior, la clase Employee
tiene algunos comportamientos específicos de la clase empleado, como getDesignation
y updateSalary
.
Además, también tiene otro método llamado sendMail
que se desvía de la responsabilidad de la clase Employee
.
Este comportamiento no es específico de esta clase y tenerlo viola el principio de responsabilidad única. Para superar esto, puede mover el método sendMail
a una clase separada.
Así es cómo podría hacerlo:
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
}
public class NotificationService {
public void sendMail() { // }
}
Principio de abierto/cerrado
De acuerdo con el principio de abierto/cerrado, los componentes deben ser abiertos para la extensión, pero cerrados para la modificación. Para entender este principio, tomemos un ejemplo de una clase que calcula el área de un objeto de forma.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// calcular el área del Cuadrado
} else if(shape instanceof Circle){
// calcular el área del Círculo
}
return areaOfShape;
}
El problema con el ejemplo anterior es que si hay una nueva instancia de tipo Shape
para la cual se necesita calcular el área en el futuro, se tiene que modificar la clase anterior agregando otro bloque else-if
condicional. Terminarás haciendo esto para cada nuevo objeto del tipo Shape
.
Para superar esto, puedes crear una interfaz y tener que cada objeto Shape
implemente esta interfaz. A continuación, cada clase puede proporcionar su propia implementación para calcular el área. Esto hará que tu programa sea fácilmente extensible en el futuro.
interface IAreaCalculator(){
double area();
}
class Square implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Square");
return 0.0;
}
}
class Circle implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Circle");
return 0.0;
}
}
Principio de sustitución de Liskov
El principio de sustitución de Liskov establece que debes ser capaz de reemplazar un objeto de superclase con un objeto de subclase sin afectar la corrección del programa.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() { // alguna implementación }
}
class Ostrich extends Bird {
@Override
public void fly() { // implementación ficticia }
}
La clase Eagle
y la clase Ostrich
ambas extienden la clase Bird
y anulan el método fly()
. Sin embargo, la clase Ostrich
se ve forzada a proporcionar una implementación dummy porque no puede volar, y por lo tanto no se comporta de la misma manera si reemplazamos el objeto de la clase Bird
con ella.
Esto viola el principio de sustitución de Liskov. Para abordar esto, podemos crear una clase separada para aves que pueden volar y tener que Eagle
se extienda a ella, mientras que otras aves pueden extender una clase diferente, que no incluirá ningún comportamiento de fly
.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // alguna implementación }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // alguna implementación }
}
Principio de Segregación de Interfaces
Según el principio de segregación de interfaces, se deben construir interfaces pequeñas y centradas que no obliguen al cliente a implementar comportamientos que no necesitan.
Un ejemplo sencillo sería tener una interfaz que calcule tanto el área como el volumen de una forma.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // calcular el área }
double calculateVolume(){ // implementación dummy }
}
El problema con esto es que si una forma Square
implementa esto, entonces se ve forzada a implementar el método calculateVolume()
, que no necesita.
Por otro lado, un Cube
puede implementar ambos. Para superar esto, podemos separar la interfaz y tener dos interfaces separadas: una para calcular el área y otra para calcular el volumen. Esto permitirá que cada figura individual decida qué implementar.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // calcular el área }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // calcular el área }
@Override
public double calculateVolume() {// calcular el volumen }
}
Principio de Inversión de Dependencias
En el principio de inversión de dependencias, los módulos de alto nivel no deben depender de los módulos de bajo nivel. En otras palabras, debes seguir la abstracción y asegurar la desacoplamiento
public interface Notification {
void notify();
}
public class EmailNotification implements Notification {
public void notify() {
System.out.println("Sending notification via email");
}
}
public class Employee {
private EmailNotification emailNotification;
public Employee(EmailNotification emailNotification) {
this.emailNotification = emailNotification;
}
public void notifyUser() {
emailNotification.notify();
}
}
En el ejemplo dado, la clase Employee
depende directamente de la clase EmailNotification
, que es un módulo de bajo nivel. Esto viola el principio de inversión de dependencias.
public interface Notification{
public void notify();
}
public class Employee{
private Notification notification;
public Employee(Notification notification){
this.notification = notification;
}
public void notifyUser(){
notification.notify();
}
}
public class EmailNotification implements Notification{
public void notify(){
//implement notification via email
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
En el ejemplo anterior, hemos asegurado el desacoplamiento. Employee
no depende de ninguna implementación concreta, sino que depende solo de la abstracción (interfaz de notificación).
Si necesitamos cambiar el modo de notificación, podemos crear una nueva implementación y pasarla a la Employee
.
Conclusión
En conclusión, en este artículo hemos cubierto el concepto de los principios SOLID a través de ejemplos sencillos.
Estos principios forman los bloques de construcción para desarrollar aplicaciones altamente extensibles y reutilizables.
Vamos a conectarnos en LinkedIn
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/