David Lay

6 minute read

Los cinco principios de diseño de software SOLID fueron reunidos por Robert Martin (@unclebobmartin) a partir de varias ideas y papers pre-existentes en la comunidad. En estos artículos los revisaremos uno a uno con la interpretación desde mi experiencia con ellos y cómo han afectado mi forma de programar.

Todos los principios los puedes ver en el índice: http://www.davidlaym.com/2015/05/solid-resumen-de-los-principios

Principio de Inversión de dependencias

La D en SOLID es por “Dependency Inversion Principle” o “Principio de inversión de dependencias” como podría traducirse al español. La definición de este principio según Robert Martin consta de dos partes:

a) Módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.

b) Abstracciones no deberían depender de detalles. Los detalles debieran depender de abstracciones.

Muy elegante, pero no entendí nada.

Comencemos por algunas aclaraciones, ya que el lenguaje usado para describir este principio es bastante técnico y un poco grueso.

Los niveles

Cuando se habla de “alto nivel” o “bajo nivel”, se refiere al nivel de abstracción relativo de una capa en un conjunto de “capas de abstracción”. Una capa de abstracción no es nada más que una forma de esconder detalles de implementación de un conjunto específico de funcionalidades, para permitir la separación de resposabilidades y facilitar la interoperabilidad. Dentro de nuestro programa, nosotros definimos las capas de abstración que utilizamos. Hace algunos años estaba de moda el uso de la separación por N capas, hoy aun se utiliza, pero me gustaría pensar que solo cuando lo justifica. El modelo de 3 capas (simplificación de las N capas: presentación, lógica, persistencia) también se usa bastante. Para esta discusión, las capas de abstracción son un poco más granulares, por ejemplo, dentro de la presentación probablemente tengamos una o dos sub-capas, no formales, de abstracción (vista, controlador) mientras que en la lógica, quizás tengamos varias (servicio, factory,strategy,lo-que-sea) y lo mismo hacia la persistencia. Habrán algunas operaciones que pasen por solo una mientras que otras ocuparán bastantes, no todas las opreraciones ocuparan todas las sub-capas.

Un ejemplo

Por ejemplo, si en un controlador usamos un factory que internamente utiliza el patrón strategy para generar un objeto, en esa operación tenemos 3 capas de abstracción, siendo 1: controlador, 2: factory, 3:strategy, en ese orden de abstracción (el strategy es el menos abstracto en esta lista). El principio dice entonces que controlador no debe depender de factory ni factory de strategy para funcionar. Deben haber abstracciones (interfaces, clases base) entre ellos.La segunda clausula es un casi repetir lo mismo de otra forma, excepto que nos advierte que nuestras abstracciones no deben depender de “detalles” o de clases particulares (insinuando que las abstracciones también deben depender de abstracciones).

Cuando se siguen este principio, rápídamente se puede ver como el control de creación de objetos comienza a invertirse. Para continuar con el ejemplo, Si antes teníamos un factory que crea nuestras estrategias en el constructor una a una, al terminar nuestro refactoring para aplicar este servicio, no debieramos ver ningun keyword “new” en ninguna parte, ya que crear un objeto concreto es una dependencia directa en él. Esto lo resolveríamos creando una interface para nuestras estrategias y recibiendo por parámetro en el constructor nuestras estrategias ya creadas por nosotros. Y así repentinamente, se ha invertido la dependencia. Lo mismo para el controlador y factory.

Entonces, ¿Quién crea los objetos?

En nuestro código no solo tenemos tres capas, este es un humilde ejemplo. Podemos llegar a tener 10 capas involucradas en una operación. Eso significa que el origen de todo esto, probablemente nuestro Main, o nuestro router, es el encargado de para cada operación crear 10 o más objetos al inicio y engancharlos todos en una infinta cadena, que obviamente, se torna muy difícil de mantener.

Es por esto que se inventó la herramienta llamada “Contenedor de Dependencias”. Puedes pensar de un contenedor como una madre de objetos, o un mega-factory, que es capaz de no solo crear un objeto en base a los parámetros que tenga en el constructor, sino además entender que los parámetros son abstractos y crear los objetos concretos que correspondan, y luego las dependencias de ellos también. En nuestro ejemplo, si usáramos un contenedor y pidiéramos al contenedor que nos fabrique un controlador, nuestro contenedor vería que el controlador depende de un factory abstracto, así que intentaría crear un factory concreto, solo para encontrar que el factory concreto depende de un strategy abstracto, entonces primero que nada crearía un strategy concreto, se lo pasaría al factory, se lo pasaría al controller y recién ahí, nos devolvería el controller creado para nosotros.

Distintas implementaciones de contenedores tienen distintas formas de configurar. Los iniciales se basaban en archivos XML gigantes en donde uno decía “esta abstracción corresponde a esta clase concreta” un millón de veces (una por cada par abstracción objeto) y algunas otras configuraciones más complejas. Hoy en día los contenedores modernos son capaces de trabajar en base a nomenclaturas (ICosa corresponde con Cosa y OtraCosaAbstract corresponde con OtraCosa) y uno debe configurar, generalmente usando código, solo los casos especiales o más complejos.

¿Y Qué se gana con todo esto?

Al seguir este principio logras fácilmente clases y objetos que colaboran muy bien entre ellos, pero que no están amarrados; es decir, logras una alta cohesión y un bajo acople. Esto a su vez es bueno porque reduce ampliamente las preocupaciones de “romper otra cosa cuando se arregla algo” (cada componente está aislado), resulta fácil realizar pruebas unitarias y resulta fácil extraer bloques de funcionalidades para reusarlas de otra manera. A modo personal puedo ofrecer mi observación de este principio como uno de los que ha ocasionado más impacto en la forma en que escribo el código, ya que el simple hecho de saber que un componente particular no depende directamente de otros, y que todo lo que declara como colaboradores (dependencias) son abstractos, me libera la mente en cuanto a planificar o modificar la funcionalidad de ese componente y me hace mucho más productivo.

En mi experiencia una de las mayores diferencias con el grado de dificultad que te puede presentar este principio en la vida diaria, es saber elegir tu contenedor. Si tu contenedor de dependencias es demasiado complejo y te obliga a declarar cada minucia de su funcionamiento, vas a odiar haber ido por este camino. Si elijes un contenedor que es demasiado simplista en su diseño y solo hace una o dos cosas, vas a encontrar que el contenedor te coloca límites. También está la preocupación de la performance, mientras más hace por tí el contenedor, más recursos ocupa. Es por esto que hay muchos contenedores de dependencias para cada lenguaje, porque cada uno está diseñado para distintas características. Conócelos y elije bien.

También puedes ver cómo se interlaza con otros principios:

  • Si una clase utiliza muchos colaboradores (recibe muchos parametros) es posible que esté cumpliendo más de una sola responsabilidad: DIP actúa como vigilante del SRP en este caso.
  • Al tener todas tus dependencias como abstractas, naturalmente estás muy avanzado en el cumplimiento de OCP: DIP actúa como facilitador en este caso.
  • El LSP te entrega la guia para construir correctamente las abstracciones. En este caso DIP requiere de LSP.
  • EL ISP también entrega guias para definir tus interfaces.En este caso DIP requiere de ISP.

 

comments powered by Disqus