En el artículo anterior habíamos abordado dos de los cinco principios SOLID para el desarrollo de software con Java, esta vez continuaremos con Liskov, Interface Segregation y Dependency Inversion.
Principio | Descripción |
---|---|
Single Responsibility | Una clase debería tener solo una responsabilidad y solo una razón para cambiar |
Open/Closed | Los componentes deberán estar abiertos para poder extender su funcionalidad pero cerrados para modificarlos |
Liskov Substitution | Las clases derivadas deberían ser completamente sustituibles por sus tipos base. Si la clase A es un subtipo de la clase B, deberíamos poder reemplazar B con A sin interrumpir el comportamiento de nuestro programa |
Interface Segregation | Los clientes no deberían ser forzados a implementar métodos innecesarios que no usarán. Simplemente significa que las interfaces más grandes deben dividirse en otras más pequeñas. |
Dependency Inversion | El principio de inversión de dependencia se refiere al desacoplamiento de módulos de software. De esta forma, en lugar de que los módulos de alto nivel dependan de los módulos de bajo nivel, ambos dependerán de abstracciones. |
Liskov Substitution
Las clases derivadas deberían ser completamente sustituibles por sus tipos base. Si la clase A es un subtipo de la clase B, deberíamos poder reemplazar B con A sin interrumpir el comportamiento de nuestro programa
El principio es muy similar al concepto de diseño por contrato propuesto por Bertrand Meyer
Un método sobreescrito de una subclase necesita aceptar los mismos valores de parámetros de entrada que el método de la superclase.
Reglas del juego
- Este principio se aplica a las jerarquías de herencia y es solo una extensión del Principio de apertura y cierre.
- Debemos asegurarnos que las nuevas clases derivadas extiendan las clases base sin cambiar su comportamiento original. Básicamente, las clases derivadas nunca deberían hacer menos que su clase base.
- Si un subtipo del supertipo hace algo que el cliente del supertipo no espera, esto constituye una violación de LSP.
Veámoslo en acción
Supongamos el siguiente escenario: Supongamos que una librería nos pide que agreguemos un nuevo tipo de funcionalidad de entrega de libros en la app. Entonces, creamos una clase BookDelivery que informa a los clientes sobre la cantidad de ubicaciones donde pueden recoger su pedido de la siguiente forma:
Sin embargo, la tienda también vende mapas de carteles de lujo que solo quieren entregar en sus tiendas principales. Entonces, creamos una nueva subclase PosterMapDelivery que amplía la funcionalidad de BookDelivery y anula el método getDeliveryLocations() con su propia funcionalidad:
Más tarde, la tienda nos pide que también creemos funcionalidades de entrega para audiolibros. Ahora, ampliamos la clase BookDelivery existente con una subclase AudioBookDelivery. Pero, cuando queremos anular el método getDeliveryLocations(), nos damos cuenta de que los audiolibros no se pueden entregar en ubicaciones físicas.
Podríamos cambiar algunas características del método getDeliveryLocations(), sin embargo, eso violaría el principio de sustitución de Liskov. Después de la modificación, no podemos reemplazar la superclase BookDelivery con la subclase AudioBookDelivery sin romper la aplicación.
Mejorando el diseño
Para solucionar el problema vamos a utilizar los siguientes principios de diseño OOP:
- Coding to the Interface: “Codificación para la interfaz”, “codificación contra la interfaz”, “programación basada en la interfaz”, son solo algunos nombres que se le dan a esta técnica increíblemente útil y escalable para escribir sistemas que dependen en gran medida de los cambios. La codificación de interfaces es una técnica para escribir clases basadas en una interfaz; interfaz que define cuál debe ser el comportamiento del objeto.
- Encapsulate what varies: es una técnica que nos ayuda a manejar detalles que cambian con frecuencia. El código tiende a enredarse cuando se modifica continuamente debido a nuevas características o requisitos. Al aislar las partes que son propensas a cambiar, limitamos el área de superficie que se verá afectada por un cambio en los requisitos.
- Composition over inheritance: la herencia por lo general genera objetos que son altamente acoplados y dependientes entre sí, algo que evita la composición.
Introduzcamos una capa adicional que diferencie mejor los tipos de entrega de libros. Las nuevas clases OfflineDelivery y OnlineDelivery dividen la superclase BookDelivery. También moveremos el método getDeliveryLocations() a OfflineDelivery. A continuación, crearemos un nuevo método getSoftwareOptions() para la clase OnlineDelivery (ya que es más adecuado para entregas en línea). Por ejemplo, el siguiente código demuestra el concepto.
Después de la refactorización, podríamos usar cualquier subclase en lugar de su superclase sin interrumpir la aplicación.
Interface Segregation
Los clientes no deberían ser forzados a implementar métodos innecesarios que no usarán. Simplemente significa que las interfaces más grandes deben dividirse en otras más pequeñas.
En el ejemplo anterior de la entrega de libros podemos aplicar Interface Segregation para solucionar el problema:
Veamos otro ejemplo: supongamos que estamos escribiendo un software para gestionar los pedidos de McDonalds. Los pedidos u órdenes pueden ser de hamburguesas, papas fritas o un combo (ambas opciones).
Dado que un cliente puede pedir papas fritas, una hamburguesa o ambos, decidimos poner todos los métodos de pedido en una sola interfaz.
Ahora, para implementar un pedido solo de hamburguesas, nos vemos obligados a lanzar una excepción en el método orderFries()
:
De manera similar, para un pedido solo de papas fritas, también tendríamos que generar una excepción en el método orderBurger()
.
Y esta no es el único problema de este diseño. Las clases BurgerOrderService
y FriesOrderService
también tendrán efectos secundarios no deseados siempre que hagamos cambios en nuestra abstracción.
Para solucionar este problema se desagregan las interfaces en componentes más pequeños:
Cuidado al utilizar ISP
Aplicar el ISP al extremo dará como resultado interfaces de método único, también conocidas como interfaces de rol.
Esta solución resolverá el problema de la violación de ISP. Aún así, puede resultar en una violación de la cohesión en las interfaces, lo que resulta en una base de código dispersa que es difícil de mantener. Por ejemplo, la interfaz Collection
en Java tiene muchos métodos como size()
y isEmpty()
que a menudo se usan juntos, por lo que tiene sentido que estén en una sola interfaz.
Hay muchos ejemplos más en la red de la aplicación de este principio, este me pareció interesante también.
Dependency Inversion
El principio de inversión de dependencia se refiere al desacoplamiento de módulos de software. De esta forma, en lugar de que los módulos de alto nivel dependan de los módulos de bajo nivel, ambos dependerán de abstracciones.
Supongamos que una librería nos pide que construyamos una nueva función que permita a los clientes colocar sus libros favoritos en una biblioteca.
Para implementar la nueva funcionalidad, creamos una clase Libro de nivel inferior y una clase Biblioteca de nivel superior. La clase Libro permitirá a los usuarios ver reseñas y leer una muestra de cada libro que almacenan en sus bibliotecas. La clase Biblioteca les permitirá agregar un libro a su biblioteca y personalizarla.
Todo parece bien, sin embargo la clase Biblioteca de alto nivel depende del Libro que es de bajo nivel, este código viola el principio de inversión de dependencia. En el siguiente diagrama de clases se aprecia mejor el problema:
Si la librería nos pide que permitamos que los clientes agreguen por ejemplo DVD a sus bibliotecas pasaría lo siguiente:
Ahora tendríamos que modificar la clase Biblioteca para aceptar los DVD’s, rompiendo con el principio Open/Closed también.
La solución a este problema es crear una capa de abstracción para las clases de nivel inferior (Libro y DVD), para esto creamos la interfaz Producto:
El código anterior también sigue el principio de sustitución de Liskov, ya que el tipo de producto se puede sustituir por sus dos subtipos (libro y DVD) sin romper el programa. Al mismo tiempo, también hemos implementado el Principio de Inversión de Dependencia, ya que en el código refactorizado, las clases de alto nivel tampoco dependen de las clases de bajo nivel.
Relación del principio de inversión de dependencia con inyección de dependencia
El tío Bob Martin introdujo el concepto de inversión de dependencia antes de que Martin Fowler introdujera el término inyección de dependencia. Estos dos conceptos están extremadamente relacionados. La inversión de dependencia está más concentrada en la estructura del código. Además, su objetivo es mantener el código lo menos acoplado posible. Por otro lado, la inyección de dependencia se trata de cómo funciona funcionalmente el código.
En una próxima entrega abordaré aspectos técnicos de inyección de dependencia con algunos frameworks como Spring para java y algunos ejemplos prácticos en .NET. Espero que el artículo haya sido de utilidad.