CodeGym /Blog Java /Random-ES /SOLID: cinco principios básicos del diseño de clases en J...
Autor
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

SOLID: cinco principios básicos del diseño de clases en Java

Publicado en el grupo Random-ES
Las clases son los componentes básicos de las aplicaciones. Al igual que los ladrillos en un edificio. Las clases mal escritas eventualmente pueden causar problemas. SOLID: cinco principios básicos del diseño de clases en Java - 1Para comprender si una clase está escrita correctamente, puede verificar cómo se ajusta a los "estándares de calidad". En Java, estos son los llamados principios SOLID, y vamos a hablar de ellos.

Principios SOLID en Java

SOLID es un acrónimo formado por las letras mayúsculas de los primeros cinco principios de OOP y diseño de clases. Los principios fueron expresados ​​por Robert Martin a principios de la década de 2000, y luego Michael Feathers introdujo la abreviatura. Estos son los principios SOLID:
  1. Principio de responsabilidad única
  2. Principio abierto cerrado
  3. Principio de sustitución de Liskov
  4. Principio de segregación de interfaz
  5. Principio de inversión de dependencia

Principio de responsabilidad única (PRS)

Este principio establece que nunca debe haber más de una razón para cambiar de clase. Cada objeto tiene una responsabilidad, que está completamente encapsulada en la clase. Todos los servicios de una clase están destinados a apoyar esta responsabilidad. Dichas clases siempre serán fáciles de modificar si es necesario, porque está claro de qué es y de qué no es responsable la clase. En otras palabras, podremos realizar cambios y no tener miedo a las consecuencias, es decir, al impacto en otros objetos. Además, dicho código es mucho más fácil de probar, porque sus pruebas cubren una parte de la funcionalidad de forma aislada de todas las demás. Imagina un módulo que procesa pedidos. Si un pedido está correctamente formado, este módulo lo guarda en una base de datos y envía un correo electrónico para confirmar el pedido:

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Este módulo podría cambiar por tres razones. Primero, la lógica para procesar pedidos puede cambiar. En segundo lugar, la forma en que se guardan los pedidos (tipo de base de datos) puede cambiar. En tercer lugar, la forma en que se envía la confirmación puede cambiar (por ejemplo, supongamos que necesitamos enviar un mensaje de texto en lugar de un correo electrónico). El principio de responsabilidad única implica que los tres aspectos de este problema son en realidad tres responsabilidades diferentes. Eso significa que deben estar en diferentes clases o módulos. La combinación de varias entidades que pueden cambiar en diferentes momentos y por diferentes motivos se considera una mala decisión de diseño. Es mucho mejor dividir un módulo en tres módulos separados, cada uno de los cuales realiza una sola función:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Principio Abierto Cerrado (OCP)

Este principio se describe de la siguiente manera: las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación . Esto significa que debería ser posible cambiar el comportamiento externo de una clase sin realizar cambios en el código existente de la clase. De acuerdo con este principio, las clases están diseñadas de modo que ajustar una clase para que se ajuste a condiciones específicas simplemente requiera extenderla y anular algunas funciones. Esto significa que el sistema debe ser flexible, capaz de funcionar en condiciones cambiantes sin cambiar el código fuente. Continuando con nuestro ejemplo relacionado con el procesamiento de pedidos, supongamos que necesitamos realizar algunas acciones antes de que se procese un pedido, así como también después de enviar el correo electrónico de confirmación. En lugar de cambiar elOrderProcessorclase en sí, la extenderemos para lograr nuestro objetivo sin violar el Principio Abierto Cerrado:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Principio de sustitución de Liskov (LSP)

Esta es una variación del principio abierto cerrado que mencionamos anteriormente. Se puede definir de la siguiente manera: los objetos se pueden reemplazar por objetos de subclases sin cambiar las propiedades de un programa. Esto significa que una clase creada mediante la extensión de una clase base debe anular sus métodos para que la funcionalidad no se vea comprometida desde el punto de vista del cliente. Es decir, si un desarrollador amplía su clase y la usa en una aplicación, no debería cambiar el comportamiento esperado de los métodos anulados. Las subclases deben anular los métodos de la clase base para que la funcionalidad no se rompa desde el punto de vista del cliente. Podemos explorar esto en detalle en el siguiente ejemplo. Supongamos que tenemos una clase que se encarga de validar un pedido y comprobar si todos los productos del pedido están en stock.isValid()método que devuelve verdadero o falso :

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Suponga también que algunos pedidos deben validarse de forma diferente a otros, por ejemplo, para algunos pedidos necesitamos comprobar si todos los productos del pedido están en stock y si todos los productos están embalados. Para hacer esto, extendemos la OrderStockValidatorclase creando la OrderStockAndPackValidatorclase:

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Pero aquí hemos violado el principio de sustitución de Liskov, porque en lugar de devolver false si la orden falla en la validación, nuestro método arroja un IllegalStateException. Los clientes que usan este código no esperan esto: esperan un valor de retorno de true o false . Esto puede conducir a errores de tiempo de ejecución.

Principio de segregación de interfaz (ISP)

Este principio se caracteriza por la siguiente declaración: no se debe obligar al cliente a implementar métodos que no utilizará . El principio de segregación de interfaces significa que las interfaces que son demasiado "gruesas" deben dividirse en otras más pequeñas y específicas, de modo que los clientes que usan interfaces pequeñas solo conozcan los métodos que necesitan para su trabajo. Como resultado, cuando cambia un método de interfaz, los clientes que no usan ese método no deberían cambiar. Considere este ejemplo: Alex, un desarrollador, creó una interfaz de "informe" y agregó dos métodos: generateExcel()ygeneratedPdf(). Ahora un cliente quiere usar esta interfaz, pero solo pretende usar informes en formato PDF, no en Excel. ¿Esta funcionalidad satisfará a este cliente? No. El cliente tendrá que implementar dos métodos, uno de los cuales en gran medida no es necesario y existe solo gracias a Alex, quien diseñó el software. El cliente usará una interfaz diferente o no hará nada con el método para los informes de Excel. Entonces, ¿cuál es la solución? Consiste en dividir la interfaz existente en dos más pequeñas. Uno para informes en PDF, el otro para informes en Excel. Esto permite a los clientes usar solo la funcionalidad que necesitan.

Principio de inversión de dependencia (DIP)

En Java, este principio SOLID se describe de la siguiente manera: las dependencias dentro del sistema se construyen en base a abstracciones. Los módulos de nivel superior no dependen de los módulos de nivel inferior. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones. El software debe diseñarse de modo que los diversos módulos sean autónomos y estén conectados entre sí a través de la abstracción. Una aplicación clásica de este principio es Spring Framework. En Spring Framework, todos los módulos se implementan como componentes separados que pueden funcionar juntos. Son tan autónomos que se pueden usar con la misma facilidad en módulos de programa que no sean Spring Framework. Esto se logra gracias a la dependencia de los principios cerrado y abierto. Todos los módulos brindan acceso solo a la abstracción, que se puede usar en otro módulo. Intentemos ilustrar esto usando un ejemplo. Hablando del principio de responsabilidad única, consideramos elOrderProcessorclase. Echemos otro vistazo al código de esta clase:

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
En este ejemplo, nuestra OrderProcessorclase depende de dos clases específicas: MySQLOrderRepositoryy ConfirmationEmailSender. También presentaremos el código de estas clases:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Estas clases están lejos de lo que llamaríamos abstracciones. Y desde el punto de vista del principio de inversión de dependencia, sería mejor comenzar creando algunas abstracciones con las que podamos trabajar en el futuro, en lugar de implementaciones específicas. Vamos a crear dos interfaces: MailSendery OrderRepository). Estas serán nuestras abstracciones:

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Ahora implementamos estas interfaces en clases que ya han sido preparadas para esto:

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
Hicimos el trabajo preparatorio para que nuestra OrderProcessorclase dependa, no de detalles concretos, sino de abstracciones. Lo cambiaremos agregando nuestras dependencias al constructor de la clase:

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Ahora nuestra clase depende de abstracciones, no de implementaciones específicas. Podemos cambiar fácilmente su comportamiento agregando la dependencia deseada en el momento en que OrderProcessorse crea un objeto. Hemos examinado los principios de diseño SOLID en Java. Aprenderá más sobre programación orientada a objetos en general y los conceptos básicos de la programación Java (nada aburrido y cientos de horas de práctica) en el curso de CodeGym. Es hora de resolver algunas tareas :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION