9.1 Inversión de dependencia
¿Recuerdas que una vez dijimos que en una aplicación de servidor no puedes simplemente crear transmisiones new Thread().start()
? Solo el contenedor debe crear hilos. Ahora desarrollaremos esta idea aún más.
Todos los objetos también deben ser creados solo por el contenedor . Por supuesto, no estamos hablando de todos los objetos, sino de los llamados objetos comerciales. También se les suele denominar contenedores. Las piernas de este enfoque surgen del quinto principio de SOLID, que requiere deshacerse de las clases y pasar a las interfaces:
- Los módulos de nivel superior no deben depender de los módulos de nivel inferior. Tanto esos como otros deberían depender de abstracciones.
- Las abstracciones no deben depender de los detalles. La implementación debe depender de la abstracción.
Los módulos no deben contener referencias a implementaciones específicas, y todas las dependencias e interacciones entre ellos deben construirse únicamente sobre la base de abstracciones (es decir, interfaces). La esencia misma de esta regla se puede escribir en una frase: todas las dependencias deben tener la forma de interfaces .
A pesar de su naturaleza fundamental y su aparente simplicidad, esta regla es la que más se viola. Es decir, cada vez que usamos el operador new en el código del programa/módulo y creamos un nuevo objeto de un tipo específico, así, en lugar de depender de la interfaz, se forma la dependencia de la implementación.
Está claro que esto no se puede evitar y los objetos deben crearse en alguna parte. Pero, como mínimo, debe minimizar la cantidad de lugares donde se hace esto y en qué clases se especifican explícitamente, así como localizar y aislar dichos lugares para que no se dispersen por todo el código del programa.
Una muy buena solución es la loca idea de concentrar la creación de nuevos objetos dentro de objetos y módulos especializados: fábricas, localizadores de servicios, contenedores IoC.
En cierto sentido, tal decisión sigue el Principio de Elección Única, que dice: "Siempre que un sistema de software deba admitir muchas alternativas, su lista completa debe ser conocida solo por un módulo del sistema" .
Por lo tanto, si en el futuro es necesario agregar nuevas opciones (o nuevas implementaciones, como en el caso de crear nuevos objetos que estamos considerando), entonces será suficiente actualizar solo el módulo que contiene esta información y todos los demás módulos. no se verán afectados y podrán continuar con su trabajo como de costumbre.
Ejemplo 1
new ArrayList
En lugar de escribir algo como , tendría sentido List.new()
que el JDK le proporcione la implementación correcta de una hoja: ArrayList, LinkedList o incluso ConcurrentList.
Por ejemplo, el compilador ve que hay llamadas al objeto desde diferentes subprocesos y coloca allí una implementación segura para subprocesos. O demasiadas inserciones en el medio de la hoja, entonces la implementación se basará en LinkedList.
Ejemplo 2
Esto ya ha sucedido con géneros, por ejemplo. ¿Cuándo fue la última vez que escribiste un algoritmo de clasificación para clasificar una colección? En cambio, ahora todos usan el método Collections.sort()
y los elementos de la colección deben admitir la interfaz Comparable (comparable).
Si sort()
pasa una colección de menos de 10 elementos al método, es muy posible clasificarla con una clasificación de burbujas (Bubble sort) y no Quicksort.
Ejemplo 3
El compilador ya está observando cómo concatenas cadenas y reemplazará tu código con StringBuilder.append()
.
9.2 Inversión de dependencia en la práctica
Ahora lo más interesante: pensemos en cómo podemos combinar la teoría y la práctica. ¿Cómo pueden los módulos crear y recibir correctamente sus "dependencias" y no violar la inversión de dependencia?
Para ello, a la hora de diseñar un módulo, debes decidir por ti mismo:
- qué hace el módulo, qué función realiza;
- luego el módulo necesita de su entorno, es decir, con qué objetos/módulos tendrá que lidiar;
- ¿Y cómo lo conseguirá?
Para cumplir con los principios de la Inversión de dependencia, definitivamente debe decidir qué objetos externos usa su módulo y cómo obtendrá referencias a ellos.
Y aquí están las siguientes opciones:
- el propio módulo crea objetos;
- el módulo toma objetos del contenedor;
- el módulo no tiene idea de dónde vienen los objetos.
El problema es que para crear un objeto, debe llamar a un constructor de un tipo específico y, como resultado, el módulo no dependerá de la interfaz, sino de la implementación específica. Pero si no queremos que los objetos se creen explícitamente en el código del módulo, entonces podemos usar el patrón Factory Method .
"La conclusión es que, en lugar de instanciar directamente un objeto a través de new, proporcionamos a la clase de cliente alguna interfaz para crear objetos. Dado que dicha interfaz siempre se puede anular con el diseño correcto, obtenemos cierta flexibilidad cuando usamos módulos de bajo nivel en módulos de alto nivel" .
En los casos en que sea necesario crear grupos o familias de objetos relacionados, se utiliza una fábrica abstracta en lugar de un método de fábrica .
9.3 Uso del Localizador de servicios
El módulo toma los objetos necesarios de quien ya los tiene. Se supone que el sistema tiene algún repositorio de objetos, en el cual los módulos pueden “poner” sus objetos y “tomar” objetos del repositorio.
Este enfoque es implementado por el patrón Service Locator , cuya idea principal es que el programa tenga un objeto que sepa cómo obtener todas las dependencias (servicios) que se puedan requerir.
La principal diferencia con las fábricas es que Service Locator no crea objetos, pero en realidad ya contiene objetos instanciados (o sabe dónde / cómo obtenerlos, y si los crea, solo una vez en la primera llamada). La fábrica en cada llamada crea un nuevo objeto del que obtienes la propiedad total y puedes hacer lo que quieras con él.
¡Importante ! El localizador de servicios produce referencias a los mismos objetos ya existentes . Por lo tanto, debe tener mucho cuidado con los objetos emitidos por el Localizador de servicios, ya que otra persona puede usarlos al mismo tiempo que usted.
Los objetos en el Localizador de servicios se pueden agregar directamente a través del archivo de configuración y, de hecho, de cualquier manera conveniente para el programador. El localizador de servicios en sí mismo puede ser una clase estática con un conjunto de métodos estáticos, un singleton o una interfaz, y se puede pasar a las clases requeridas a través de un constructor o método.
El Localizador de servicios a veces se denomina antipatrón y se desaconseja (porque crea conexiones implícitas y solo da la apariencia de un buen diseño). Puedes leer más de Mark Seaman:
9.4 Inyección de dependencia
El módulo no se preocupa en absoluto por las dependencias de "minería". Solo determina lo que necesita para funcionar, y todas las dependencias necesarias son suministradas (introducidas) desde el exterior por otra persona.
Esto es lo que se llama - Inyección de Dependencia . Por lo general, las dependencias requeridas se pasan como parámetros de constructor (inyección de constructor) o mediante métodos de clase (inyección de setter).
Este enfoque invierte el proceso de creación de dependencias: en lugar del propio módulo, alguien externo controla la creación de dependencias. El módulo del emisor activo de objetos se vuelve pasivo: no es él quien crea, sino que otros crean para él.
Este cambio de dirección se llama la Inversión de Control , o el Principio de Hollywood - "No nos llames, te llamaremos".
Esta es la solución más flexible, dando a los módulos la mayor autonomía . Podemos decir que solo implementa completamente el "Principio de responsabilidad única": el módulo debe estar completamente enfocado en hacer bien su trabajo y no preocuparse por nada más.
Proporcionar al módulo todo lo necesario para el trabajo es una tarea separada, que debe ser manejada por el "especialista" apropiado (generalmente un contenedor determinado, un contenedor IoC, es responsable de administrar las dependencias y su implementación).
De hecho, aquí todo es como en la vida: en una empresa bien organizada, los programadores programan, y los escritorios, las computadoras y todo lo que necesitan para trabajar son comprados y proporcionados por el gerente de la oficina. O, si usa la metáfora del programa como constructor, el módulo no debe pensar en cables, alguien más está involucrado en ensamblar el constructor, y no las partes en sí.
No sería una exageración decir que el uso de interfaces para describir dependencias entre módulos (Inversión de dependencia) + la correcta creación e inyección de estas dependencias (principalmente Inyección de dependencia) son técnicas clave para el desacoplamiento .
Sirven como la base sobre la que se sustenta el acoplamiento flexible del código, su flexibilidad, su resistencia a los cambios, su reutilización, y sin la cual todas las demás técnicas tienen poco sentido. Esta es la base del acoplamiento flexible y la buena arquitectura.
Martin Fowler analiza en detalle el principio de inversión de control (junto con la inyección de dependencia y el localizador de servicios). Hay traducciones de sus dos artículos: "Inversión de contenedores de control y el patrón de inyección de dependencia" e "Inversión de control" .
GO TO FULL VERSION