JavaRush /Blog Java /Random-ES /Cinco principios básicos del diseño de clases (SOLID) en ...
Ve4niY
Nivel 14

Cinco principios básicos del diseño de clases (SOLID) en Java

Publicado en el grupo Random-ES
Las clases son los bloques a partir de los cuales se construye una aplicación. Como los ladrillos de un edificio. Las clases mal escritas pueden causar problemas algún día. Cinco principios básicos del diseño de clases (SOLID) en Java - 1Para saber si una clase está escrita correctamente, puedes consultar los “estándares de calidad”. En Java, estos son los llamados principios SÓLIDOS. Hablemos de ellos.

Principios SÓLIDOS en Java

SOLID es un acrónimo formado por las letras mayúsculas de los primeros cinco principios de la programación orientada a objetos y el diseño. Los principios fueron inventados por Robert Martin a principios de la década de 2000, y el acrónimo fue acuñado más tarde por Michael Feathers. Esto es lo que incluyen 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 (SRP)

Este principio establece que nunca debe haber más de una razón para cambiar de clase. Cada objeto tiene una responsabilidad, completamente encapsulada en una clase. Todos los servicios de clase están destinados a garantizar esta responsabilidad. Estas clases siempre serán fáciles de cambiar si es necesario, porque está claro de qué es responsable la clase y de qué no. Es decir, será posible realizar cambios y no tener miedo de las consecuencias: el impacto en otros objetos. Y dicho código es mucho más fácil de probar, porque cubre una funcionalidad con pruebas de forma aislada de todas las demás. Imagine un módulo que procesa pedidos. Si el pedido está formado correctamente, lo guarda en la 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");
        // guarda el pedido en la base de datos

        return true;
    }

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

        // Enviando una carta al cliente
    }
}
Un módulo de este tipo puede cambiar por tres razones. En primer lugar, la lógica de procesamiento del pedido puede ser diferente, en segundo lugar, el método para guardarlo (tipo de base de datos), en tercer lugar, el método para enviar una carta de confirmación (por ejemplo, en lugar de un correo electrónico debe enviar un SMS). El principio de responsabilidad única implica que los tres aspectos de este problema son en realidad tres responsabilidades diferentes. Esto significa que deben estar en diferentes clases o módulos. Combinar múltiples entidades que pueden cambiar en diferentes momentos y por diferentes motivos se considera una mala decisión de diseño. Es mucho mejor dividir el módulo en tres módulos separados, cada uno de los cuales realizará una única función:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // guarda el pedido en la base de datos

        return true;
    }
}

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

        // Enviando una carta al cliente
    }
}

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 sucintamente de la siguiente manera: las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas a la extensión, pero cerradas al cambio . Esto significa que debería ser posible cambiar el comportamiento externo de una clase sin realizar cambios físicos en la clase misma. Siguiendo este principio, las clases se desarrollan de modo que para ajustar la clase a condiciones de aplicación específicas, basta con ampliarla y redefinir algunas funciones. Por lo tanto, el sistema debe ser flexible, capaz de funcionar en condiciones variables sin cambiar el código fuente. Continuando con nuestro ejemplo de pedido, digamos que necesitamos realizar algunas acciones antes de que se procese el pedido y después de que se envíe el correo electrónico de confirmación. En lugar de cambiar la clase en sí OrderProcessor, la ampliaremos y lograremos una solución al problema en cuestión sin violar el principio de OCP:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Realizar algunas acciones antes de procesar el pedido
    }

    private void afterProcessing() {
        // Realizar algunas acciones después del procesamiento del pedido
    }
}

Principio de sustitución de Barbara Liskov (LSP)

Esta es una variación del principio abierto/cerrado discutido anteriormente. Se puede describir de la siguiente manera: los objetos de un programa pueden ser reemplazados por sus herederos sin cambiar las propiedades del programa. Esto significa que una clase desarrollada extendiendo una clase base debe anular sus métodos de una manera que no rompa la funcionalidad desde el punto de vista del cliente. Es decir, si un desarrollador extiende 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 de una manera que no interrumpa la funcionalidad desde el punto de vista del cliente. Esto se puede examinar en detalle utilizando el siguiente ejemplo. Supongamos que tenemos una clase que es responsable de la validación del pedido y verifica si todos los artículos del pedido están en stock. Esta clase tiene un método isValidque devuelve verdadero o falso :
public class OrderStockValidator {

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

        return true;
    }
}
Supongamos también que algunos pedidos deben validarse de manera diferente: verifique si todos los productos del pedido están en stock y si todos los productos están empaquetados. Para hacer esto, ampliamos la clase OrderStockValidatorcon la clase OrderStockAndPackValidator:
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;
    }
}
Sin embargo, en esta clase violamos el principio LSP, porque en lugar de devolver falso si la orden no pasó la validación, nuestro método arroja una excepción IllegalStateException. Los clientes de este código no esperan esto: esperan que se devuelva verdadero o falso . Esto puede provocar errores en el programa.

Principio de división de interfaz (ISP)

Caracterizado por la siguiente afirmación: No se debe obligar a los clientes a implementar métodos que no utilizarán . El principio de separación de interfaces sugiere que las interfaces que son demasiado "gruesas" deben dividirse en otras más pequeñas y específicas, de modo que los clientes de interfaces pequeñas sólo conozcan los métodos necesarios para su trabajo. Como resultado, al cambiar un método de interfaz, los clientes que no utilizan este método no deberían cambiar. Veamos un ejemplo. El desarrollador Alex creó la interfaz de "informe" y agregó dos métodos: generateExcel()y generatedPdf(). Ahora el Cliente A quiere utilizar esta interfaz, pero sólo pretende utilizar informes en PDF y no en Excel. ¿Estará satisfecho con esta funcionalidad? No. Tendrá que implementar dos métodos, uno de los cuales es en gran medida innecesario y existe sólo gracias a Alex, el diseñador del software. El cliente utilizará una interfaz diferente o dejará el campo de Excel en blanco. Entonces ¿cuál es la solución? Consiste en dividir la interfaz existente en dos más pequeñas. Uno es un informe en formato PDF, el segundo es un informe en formato Excel. Esto le dará al usuario la oportunidad de utilizar sólo la funcionalidad que necesita.

Principio de inversión de dependencia (DIP)

Este principio SÓLIDO en Java se describe a continuación: las dependencias dentro del sistema se construyen sobre la base de abstracciones . Los módulos de nivel superior son independientes de los módulos de nivel inferior. Las abstracciones no deberían depender de los detalles. Los detalles deben depender de abstracciones. El software debe diseñarse de modo que los distintos módulos sean autónomos y se conecten entre sí mediante abstracción. Una aplicación clásica de este principio es el marco Spring. Dentro del marco Spring, todos los módulos se implementan como componentes separados que pueden funcionar juntos. Son tan autónomos que pueden usarse con la misma facilidad en otros módulos de software además del marco Spring. Esto se logra mediante la dependencia de principios cerrados y abiertos. Todos los módulos brindan acceso solo a una abstracción que se puede usar en otro módulo. Intentemos demostrar esto con un ejemplo. Hablando del principio de responsabilidad exclusiva, consideramos algunos OrderProcessor. 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, el nuestro OrderProcessordepende de dos clases específicas MySQLOrderRepositoryy ConfirmationEmailSender. También presentamos el código para estas clases:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // guarda el pedido en la base de datos

        return true;
    }
}

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

        // Enviando una carta al cliente
    }
}
Estas clases están lejos de ser llamadas abstracciones. Y desde el punto de vista del principio DIP, sería más correcto comenzar creando algunas abstracciones que nos permitan operar con ellas en el futuro, en lugar de implementaciones específicas. Creemos dos interfaces MailSendery OrderRepository, que se convertirán en nuestras abstracciones:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Ahora implementemos estas interfaces en clases que ya están listas para esto:
public class ConfirmationEmailSender implements MailSender {

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

        // Enviando una carta al cliente
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // guarda el pedido en la base de datos

        return true;
    }
}
Hemos hecho el trabajo preparatorio para que nuestra clase OrderProcessorno dependa de detalles concretos, sino de abstracciones. Hagámosle cambios introduciendo nuestras dependencias en el constructor de clases:
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);
        }
    }
}
Nuestra clase ahora depende de abstracciones en lugar de implementaciones concretas. Puede cambiar fácilmente su comportamiento inyectando la dependencia deseada en el momento en que se crea la instancia OrderProcessor. Analizamos SOLID: principios de diseño en Java. Más sobre POO en general, los conceptos básicos de este lenguaje de programación, nada aburrido y con cientos de horas de práctica, en el curso JavaRush. Es hora de resolver algunos problemas :) Cinco principios básicos del diseño de clases (SOLID) en Java - 2
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION