Todo lo que necesita saber sobre los principios sólidos en Java



En este artículo, aprenderá en detalle sobre qué son los principios sólidos en Java con ejemplos y su importancia con ejemplos de la vida real.

En el mundo de (OOP), hay muchas pautas, patrones o principios de diseño. Cinco de estos principios suelen estar agrupados y se conocen con el acrónimo SOLID. Si bien cada uno de estos cinco principios describe algo específico, también se superponen de modo que la adopción de uno de ellos implica o conduce a la adopción de otro. En este artículo, comprenderemos los principios de SOLID en Java.

Historia de los principios SOLID en Java

Robert C. Martin dio cinco principios de diseño orientado a objetos, y se utiliza el acrónimo 'S.O.L.I.D'. Cuando usa todos los principios de S.O.L.I.D de manera combinada, le resulta más fácil desarrollar software que se pueda administrar fácilmente. Las otras características de usar S.O.L.I.D son:





  • Evita los olores de código.
  • Código refractor rápido.
  • Puede hacer desarrollo de software adaptativo o ágil.

Cuando usa el principio de S.O.L.I.D en su codificación, comienza a escribir el código que es eficiente y efectivo.



¿Cuál es el Significado de S.O.L.I.D?

Solid representa cinco principios de Java que son:

  • S : Principio de responsabilidad única
  • O : Principio abierto-cerrado
  • L : Principio de sustitución de Liskov
  • yo : Principio de segregación de interfaz
  • re : Principio de inversión de dependencia

En este blog, discutiremos los cinco principios SOLID de Java en detalle.



Principio de responsabilidad única en Java

¿Qué dice?

Robert C. Martin lo describe como una clase debería tener una sola responsabilidad.

De acuerdo con el principio de responsabilidad única, solo debe haber una razón por la cual deba cambiarse una clase. Significa que una clase debe tener una tarea que hacer. Este principio a menudo se denomina subjetivo.

El principio puede entenderse bien con un ejemplo. Imagina que hay una clase que realiza las siguientes operaciones.

  • Conectado a una base de datos

  • Leer algunos datos de las tablas de la base de datos

  • Finalmente, escríbalo en un archivo.

¿Has imaginado el escenario? Aquí la clase tiene múltiples razones para cambiar, y algunas de ellas son la modificación de la salida del archivo, la adopción de una nueva base de datos. Cuando hablamos de responsabilidad de principio único, diríamos que hay demasiadas razones para que la clase cambie, por lo que no encaja adecuadamente en el principio de responsabilidad única.

Por ejemplo, una clase de automóvil puede iniciarse o detenerse por sí misma, pero la tarea de lavarla pertenece a la clase CarWash. En otro ejemplo, una clase Book tiene propiedades para almacenar su propio nombre y texto. Pero la tarea de imprimir el libro debe pertenecer a la clase de Impresora de libros. La clase Book Printer puede imprimir en la consola u otro medio, pero estas dependencias se eliminan de la clase Book.

¿Por qué se requiere este principio?

Cuando se sigue el principio de responsabilidad única, las pruebas son más fáciles. Con una sola responsabilidad, la clase tendrá menos casos de prueba. Menos funcionalidad también significa menos dependencias con otras clases. Conduce a una mejor organización del código ya que las clases más pequeñas y bien diseñadas son más fáciles de buscar.

Un ejemplo para aclarar este principio:

Suponga que se le pide que implemente un servicio UserSetting en el que el usuario puede cambiar la configuración, pero antes de eso, el usuario debe estar autenticado. Una forma de implementar esto sería:

public class UserSettingService {public void changeEmail (Usuario usuario) {if (checkAccess (usuario)) {// Otorgar opción para cambiar}} public boolean checkAccess (Usuario usuario) {// Verificar si el usuario es válido. }}

Todo se ve bien hasta que desee reutilizar el código checkAccess en algún otro lugar O desee realizar cambios en la forma en que se realiza checkAccess. En los 2 casos, terminaría cambiando la misma clase y, en el primer caso, también tendría que usar UserSettingService para verificar el acceso.
Una forma de corregir esto es descomponer UserSettingService en UserSettingService y SecurityService. Y mueva el código checkAccess a SecurityService.

public class UserSettingService {public void changeEmail (Usuario usuario) {if (SecurityService.checkAccess (usuario)) {// Otorgar opción para cambiar}}} public class SecurityService {public static boolean checkAccess (Usuario usuario) {// verificar el acceso. }}

Principio abierto cerrado en Java

Robert C. Martin lo describe como Los componentes de software deben estar abiertos para su extensión, pero cerrados para modificaciones.

Para ser precisos, de acuerdo con este principio, una clase debe escribirse de tal manera que realice su trabajo sin problemas sin la suposición de que la gente en el futuro simplemente vendrá y la cambiará. Por lo tanto, la clase debe permanecer cerrada para modificaciones, pero debe tener la opción de ampliarse. Las formas de extender la clase incluyen:

  • Heredando de la clase

  • Sobrescribir los comportamientos requeridos de la clase

  • Extendiendo ciertos comportamientos de la clase

Un excelente ejemplo del principio abierto-cerrado puede entenderse con la ayuda de los navegadores. ¿Recuerda haber instalado extensiones en su navegador Chrome?

La función básica del navegador Chrome es navegar por diferentes sitios. ¿Desea verificar la gramática cuando escribe un correo electrónico con el navegador Chrome? Si es así, simplemente puede usar la extensión Grammarly, que le proporciona una revisión gramatical del contenido.

Este mecanismo en el que está agregando cosas para aumentar la funcionalidad del navegador es una extensión. Por lo tanto, el navegador es un ejemplo perfecto de funcionalidad que está abierta a la extensión pero cerrada a la modificación. En palabras simples, puede mejorar la funcionalidad agregando / instalando complementos en su navegador, pero no puede crear nada nuevo.

¿Por qué se requiere este principio?

OCP es importante ya que las clases pueden llegar a nosotros a través de bibliotecas de terceros. Deberíamos poder extender esas clases sin preocuparnos si esas clases base pueden soportar nuestras extensiones. Pero la herencia puede conducir a subclases que dependen de la implementación de la clase base. Para evitar esto, se recomienda el uso de interfaces. Esta abstracción adicional conduce a un acoplamiento flojo.

Digamos que necesitamos calcular áreas de varias formas. Comenzamos creando una clase para nuestro primer rectángulo de formaque tiene 2 atributos de longitud& ancho.

public class Rectangle {public double length public double width}

A continuación creamos una clase para calcular el área de este rectánguloque tiene un método calculateRectangleAreaque toma el rectángulocomo parámetro de entrada y calcula su área.

Public class AreaCalculator {público double calculateRectangleArea (rectángulo rectángulo) {return rectangle.length * rectangle.width}}

Hasta aquí todo bien. Ahora digamos que obtenemos nuestro segundo círculo de forma. Así que creamos rápidamente un nuevo círculo de clases.con un solo radio de atributo.

Círculo de clase pública {radio doble público}

Luego modificamos Areacalculatorclass para agregar cálculos de círculos a través de un nuevo método calculateCircleaArea ()

public class AreaCalculator {public double calculateRectangleArea (rectángulo rectángulo) {return rectangle.length * rectangle.width} public double calculateCircleArea (círculo círculo) {return (22/7) * circle.radius * circle.radius}}

Sin embargo, tenga en cuenta que hubo fallas en la forma en que diseñamos nuestra solución anterior.

Digamos que tenemos un pentágono de nueva forma. En ese caso, volveremos a terminar modificando la clase AreaCalculator. A medida que los tipos de formas crecen, esto se vuelve más complicado ya que AreaCalculator sigue cambiando y cualquier consumidor de esta clase tendrá que seguir actualizando sus bibliotecas que contienen AreaCalculator. Como resultado, la clase AreaCalculator no se establecerá como base (finalizada) con seguridad, ya que cada vez que aparezca una nueva forma, se modificará. Por lo tanto, este diseño no está cerrado para modificaciones.

AreaCalculator deberá seguir agregando su lógica de cálculo en métodos más nuevos. En realidad, no estamos expandiendo el alcance de las formas, sino que simplemente estamos haciendo una solución por partes (poco a poco) para cada forma que se agrega.

Modificación del diseño anterior para cumplir con el principio de abierto / cerrado:

Veamos ahora un diseño más elegante que resuelve las fallas en el diseño anterior al adherirse al principio abierto / cerrado. En primer lugar, haremos que el diseño sea extensible. Para esto, primero debemos definir una forma de tipo base y tener una interfaz de forma de implementación de círculo y rectángulo.

interfaz pública Forma {público doble área de cálculo ()} clase pública Rectángulo implementa Forma {doble longitud doble ancho público doble área de cálculo () {longitud de retorno * ancho}} clase pública Implementos de círculo Forma {público doble radio público doble área de cálculo () {retorno (22 / 7) * radio * radio}}

Hay una forma de interfaz base. Todas las formas implementan ahora la interfaz base Shape. La interfaz Shape tiene un método abstracto calculateArea (). Tanto el círculo como el rectángulo proporcionan su propia implementación anulada del método calculateArea () utilizando sus propios atributos.
Hemos aportado un grado de extensibilidad ya que las formas son ahora una instancia de interfaces de formas. Esto nos permite usar Shape en lugar de clases individuales.
El último punto mencionado anteriormente al consumidor de estas formas. En nuestro caso, el consumidor será la clase AreaCalculator que ahora se vería así.

clase pública AreaCalculator {public double calculateShapeArea (forma de forma) {return shape.calculateArea ()}}

Esta calculadora de áreaclass ahora elimina por completo nuestros defectos de diseño mencionados anteriormente y brinda una solución limpia que se adhiere al principio abierto-cerrado. Sigamos con otros principios SOLID en Java

Principio de sustitución de Liskov en Java

Robert C. Martin lo describe como Los tipos derivados deben ser completamente sustituibles por sus tipos base.

El principio de sustitución de Liskov supone que q (x) es una propiedad, demostrable sobre entidades de x que pertenecen al tipo T.Ahora, de acuerdo con este principio, q (y) ahora debería ser demostrable para objetos y que pertenecen al tipo S, y la S es en realidad un subtipo de T. ¿Está ahora confundido y no sabe qué significa realmente el principio de sustitución de Liskov? La definición puede resultar un poco compleja, pero de hecho es bastante fácil. Lo único es que cada subclase o clase derivada debe ser sustituible por su clase principal o base.

Puede decirse que es un principio único orientado a objetos. El principio puede simplificarse aún más mediante un tipo de hijo de un tipo de padre en particular sin hacer ninguna complicación o hacer explotar las cosas que deben tener la capacidad de reemplazar a ese padre. Este principio está estrechamente relacionado con el principio de sustitución de Liskov.

¿Por qué se requiere este principio?

Esto evita el mal uso de la herencia. Nos ayuda a conformarnos con la relación 'es-un'. También podemos decir que las subclases deben cumplir con un contrato definido por la clase base. En este sentido, se relaciona conDiseño por contratoque fue descrito por primera vez por Bertrand Meyer. Por ejemplo, es tentador decir que un círculo es un tipo de elipse, pero los círculos no tienen dos focos o ejes mayor / menor.

El LSP se explica popularmente usando el ejemplo del cuadrado y el rectángulo. si asumimos una relación ISA entre Cuadrado y Rectángulo. Por lo tanto, lo llamamos 'El cuadrado es un rectángulo'. El siguiente código representa la relación.

public class Rectangle {privado int longitud privada int amplitud public int getLength () {longitud de retorno} public void setLength (int longitud) {this.length = longitud} public int getBreadth () {return amplitud} public void setBreadth (int amplitud) { this.breadth = amplitud} public int getArea () {devolver this.length * this.breadth}}

A continuación se muestra el código de Square. Tenga en cuenta que Square extiende Rectangle.

public class Cuadrado extiende Rectángulo {public void setBreadth (ancho int) {super.setBreadth (ancho) super.setLength (ancho)} public void setLength (int largo) {super.setLength (largo) super.setBreadth (largo)}}

En este caso, tratamos de establecer una relación ISA entre Square y Rectangle de manera que llamar a 'Square is a Rectangle' en el código siguiente comenzaría a comportarse de forma inesperada si se pasa una instancia de Square. Se arrojará un error de afirmación en el caso de verificar 'Área' y verificar 'Amplitud', aunque el programa terminará cuando se arroje el error de afirmación debido a la falla de la verificación de Área.

public class LSPDemo {public void calculateArea (Rectangle r) {r.setBreadth (2) r.setLength (3) assert r.getArea () == 6: printError ('area', r) assert r.getLength () == 3: printError ('length', r) assert r.getBreadth () == 2: printError ('amplitud', r)} private String printError (String errorIdentifer, Rectangle r) {return 'Valor inesperado de' + errorIdentifer + ' por ejemplo de '+ r.getClass (). getName ()} public static void main (String [] args) {LSPDemo lsp = new LSPDemo () // Se pasa una instancia de Rectangle lsp.calculateArea (new Rectangle ()) // Se pasa una instancia de Square lsp.calculateArea (new Square ())}}

La clase demuestra el principio de sustitución de Liskov (LSP) Según el principio, las funciones que usan referencias a las clases base deben poder usar objetos de la clase derivada sin saberlo.

Por lo tanto, en el ejemplo que se muestra a continuación, la función calculateArea que usa la referencia de “Rectangle” debería poder usar los objetos de clase derivada como Square y cumplir con el requisito planteado por la definición de Rectangle. Se debe tener en cuenta que, según la definición de Rectángulo, lo siguiente siempre debe ser cierto dados los datos a continuación:

  1. La longitud siempre debe ser igual a la longitud pasada como entrada al método, setLength
  2. La amplitud siempre debe ser igual a la amplitud pasada como entrada al método, setBreadth
  3. El área siempre debe ser igual al producto de largo y ancho

En caso de que intentemos establecer una relación ISA entre Square y Rectangle de manera que llamemos 'Square is a Rectangle', el código anterior comenzaría a comportarse de manera inesperada si se pasa una instancia de Square. para amplitud, aunque el programa terminará cuando se produzca el error de afirmación debido a una falla en la verificación del área.

La clase Square no necesita métodos como setBreadth o setLength. La clase LSPDemo necesitaría conocer los detalles de las clases derivadas de Rectangle (como Square) para codificar adecuadamente para evitar errores de lanzamiento. El cambio en el código existente rompe el principio abierto-cerrado en primer lugar.

Principio de segregación de interfaz

Robert C. Martin lo describe como que los clientes no deberían verse obligados a implementar métodos innecesarios que no utilizarán.

De acuerdo aPrincipio de segregación de interfazun cliente, no importa lo que nunca debe ser obligado a implementar una interfaz que no utiliza o el cliente nunca debe estar obligado a depender de ningún método, que no es utilizado por ellos. Así que básicamente, los principios de segregación de interfaz como usted prefiere el interfaces, que son pequeñas pero específicas del cliente en lugar de monolíticas y una interfaz más grande. En resumen, sería malo para usted obligar al cliente a depender de una determinada cosa, que no necesita.

Por ejemplo, una única interfaz de registro para escribir y leer registros es útil para una base de datos pero no para una consola. La lectura de registros no tiene sentido para un registrador de consola. Continuando con este artículo Principios SÓLIDOS en Java.

¿Por qué se requiere este principio?

Supongamos que existe una interfaz de restaurante que contiene métodos para aceptar pedidos de clientes en línea, clientes de acceso telefónico o telefónico y clientes sin cita previa. También contiene métodos para manejar pagos en línea (para clientes en línea) y pagos en persona (para clientes sin cita previa y clientes telefónicos cuando su pedido se entrega en casa).

Ahora creemos una interfaz Java para restaurante y la nombramos RestaurantInterface.java.

public interface RestaurantInterface {public void acceptOnlineOrder () public void takeTelephoneOrder () public void payOnline () public void walkInCustomerOrder () public void payInPerson ()}

Hay 5 métodos definidos en RestaurantInterface que son para aceptar pedidos en línea, tomar pedidos telefónicos, aceptar pedidos de un cliente sin cita previa, aceptar pagos en línea y aceptar pagos en persona.

Comencemos por implementar RestaurantInterface para clientes en línea como OnlineClientImpl.java

public class OnlineClientImpl implementa RestaurantInterface {public void acceptOnlineOrder () {// lógica para realizar un pedido en línea} public void takeTelephoneOrder () {// No aplicable para pedidos en línea throw new UnsupportedOperationException ()} public void payOnline () {// lógica para pagar online} public void walkInCustomerOrder () {// No aplica para pedidos en línea throw new UnsupportedOperationException ()} public void payInPerson () {// No aplica para pedidos en línea throw new UnsupportedOperationException ()}}
  • Dado que el código anterior (OnlineClientImpl.java) es para pedidos en línea, lance UnsupportedOperationException.

  • Los clientes en línea, telefónicos y sin cita previa utilizan la implementación de RestaurantInterface específica para cada uno de ellos.

  • Las clases de implementación para el cliente telefónico y el cliente Walk-in tendrán métodos no admitidos.

  • Dado que los 5 métodos son parte de RestaurantInterface, las clases de implementación deben implementar los 5.

  • Los métodos que arroja cada una de las clases de implementación UnsupportedOperationException. Como puede ver claramente, implementar todos los métodos es ineficiente.

  • Cualquier cambio en cualquiera de los métodos de RestaurantInterface se propagará a todas las clases de implementación. El mantenimiento del código comienza a volverse realmente engorroso y los efectos de regresión de los cambios seguirán aumentando.

  • RestaurantInterface.java rompe el Principio de Responsabilidad Única porque la lógica de los pagos y la de la realización de pedidos se agrupan en una única interfaz.

Para superar los problemas mencionados anteriormente, aplicamos el principio de segregación de interfaces para refactorizar el diseño anterior.

  1. Separe las funcionalidades de pago y colocación de pedidos en dos interfaces lean separadas, PaymentInterface.java y OrderInterface.java.

  2. Cada uno de los clientes usa una implementación de PaymentInterface y OrderInterface. Por ejemplo, OnlineClient.java usa OnlinePaymentImpl y OnlineOrderImpl y así sucesivamente.

  3. El principio de responsabilidad única ahora se adjunta como interfaz de pago (PaymentInterface.java) e interfaz de pedidos (OrderInterface).

  4. El cambio en cualquiera de las interfaces de pedido o pago no afecta a la otra. Ahora son independientes. No habrá necesidad de hacer ninguna implementación ficticia o lanzar una UnsupportedOperationException ya que cada interfaz solo tiene métodos que siempre usará.

Después de aplicar ISP

Principio de inversión de dependencia

Robert C. Martin lo describe como que depende de abstracciones, no de concreciones. Según él, el módulo de alto nivel nunca debe depender de ningún módulo de bajo nivel. por ejemplo

Vas a una tienda local a comprar algo y decides pagarlo con tu tarjeta de débito. Por lo tanto, cuando le da su tarjeta al empleado para que realice el pago, el empleado no se molesta en verificar qué tipo de tarjeta le ha dado.

Incluso si le ha dado una tarjeta Visa, él no apagará una máquina Visa para pasar su tarjeta. El tipo de tarjeta de crédito o débito que tenga para pagar ni siquiera importa, simplemente la deslizarán. Entonces, en este ejemplo, puede ver que tanto usted como el empleado dependen de la abstracción de la tarjeta de crédito y no están preocupados por los detalles de la tarjeta. Esto es lo que es un principio de inversión de dependencia.

system.exit (0) java

¿Por qué se requiere este principio?

Permite a un programador eliminar dependencias codificadas de forma rígida para que la aplicación se acople de forma flexible y se pueda ampliar.

clase pública Estudiante {dirección privada dirección estudiante pública () {dirección = nueva dirección ()}}

En el ejemplo anterior, la clase Student requiere un objeto Address y es responsable de inicializar y usar el objeto Address. Si la clase de dirección se cambia en el futuro, también tenemos que hacer cambios en la clase de estudiante. Esto crea un acoplamiento estrecho entre los objetos Student y Address. Podemos resolver este problema utilizando el patrón de diseño de inversión de dependencia. es decir, el objeto de dirección se implementará de forma independiente y se proporcionará al estudiante cuando se cree una instancia del estudiante mediante el uso de la inversión de dependencia basada en constructor o establecedor.

Con esto, llegamos al final de estos Principios SÓLIDOS en Java.

Revisar la por Edureka, una empresa de aprendizaje en línea de confianza con una red de más de 250.000 alumnos satisfechos repartidos por todo el mundo. El curso de formación y certificación Java J2EE y SOA de Edureka está diseñado para estudiantes y profesionales que desean ser desarrolladores de Java. El curso está diseñado para darle una ventaja en la programación de Java y capacitarlo para los conceptos básicos y avanzados de Java junto con varios marcos de Java como Hibernate y Spring.

Tienes una pregunta para nosotros? Menciónelo en la sección de comentarios de este blog de “Principios SÓLIDOS en Java” y nos pondremos en contacto con usted lo antes posible.