Estudiar la inyección de dependencia

A pesar de que el patrón ha existido durante más de una década, hay muchos artículos (y traducciones), sin embargo, cada vez hay más disputas, comentarios, preguntas y diversas realizaciones.

Antecedentes
En 2004, Martin Fowler escribió el famoso artículo " Inversión de contenedores de control y el patrón de inyección de dependencia ", que describe el patrón anterior y su implementación para Java. Desde entonces, el patrón se ha debatido e implementado ampliamente. En el desarrollo móvil, especialmente en iOS, esto se produjo con un retraso significativo. En el Habré hay buenas traducciones del artículo , buena suerte y brillante karma para su autor.

Hay suficiente información incluso en el centro, pero el hecho de que se discuta en todas partes cómo hacerlo, pero prácticamente en ninguna parte, POR QUÉ, me inspiró a escribir la publicación. ¿Es posible crear una buena arquitectura si no sabes para qué sirve y en qué debería ser buena? Se pueden tener en cuenta ciertos principios y tendencias claras; esto ayudará a minimizar problemas imprevistos, pero la comprensión es aún mejor.

La inyección de dependencia es un patrón de diseño en el que los campos o parámetros para crear un objeto se configuran externamente.

Sabiendo que muchos se limitarán a leer los primeros párrafos, cambié el artículo.
A pesar del hecho de que tal "definición" de DI se encuentra en muchas fuentes, es ambigua, porque hace que el usuario piense que la inyección es algo que reemplaza la creación / inicialización de objetos, o al menos está muy activamente involucrada en este proceso. Por supuesto, nadie prohibirá hacer tal implementación de DI. Pero DI puede ser un contenedor pasivo alrededor de la creación de un objeto que proporciona la provisión de parámetros de entrada. En esta implementación, obtenemos otro nivel de abstracción y una excelente separación de tareas: el objeto es responsable de su inicialización, y la inyección implementa el almacenamiento de datos y les proporciona módulos de aplicación.

Ahora sobre todo en orden y en detalle.
Comenzaré con uno simple, ¿por qué había una necesidad de nuevos patrones y por qué algunos patrones antiguos se volvieron muy limitados?

En mi opinión, la mayor parte de los cambios fueron introducidos por la introducción masiva de la autocomprobación. Y para aquellos que están escribiendo autotests activamente, este artículo es obvio como un día blanco, no puedes leer más. Solo que no te puedes imaginar cuántas personas no los escriben. Entiendo que las pequeñas empresas y las nuevas empresas no tienen estos recursos, pero, desafortunadamente, las grandes empresas a menudo tienen problemas más prioritarios.

El razonamiento aquí es muy simple. Suponga que está probando una función con los parámetros a y b , y espera obtener el resultado x . En algún momento, sus expectativas no se hacen realidad, la función devuelve el resultado y , y después de pasar algún tiempo, encuentra un singleton dentro de la función, que en algunos estados lleva el resultado de la función a un valor diferente. Este singleton se llamó adicción implícita , y de todas las formas posibles se negó a usarlo en tales situaciones. Desafortunadamente, no arrojarás palabras fuera de la canción, de lo contrario será una canción completamente diferente. Por lo tanto, sacamos nuestro singleton como una variable de entrada en la función. Ahora tenemos 3 variables de entrada a , b , s . Todo parece ser obvio: cambiamos los parámetros, obtenemos un resultado inequívoco.

Si bien no voy a dar ejemplos. Además, no solo estamos hablando de funciones dentro de una clase, es un argumento esquemático que también se puede aplicar para crear una clase, módulo, etc.

Notas Singleton
Nota 1. Si, dada la crítica del patrón singleton, decide reemplazarlo, por ejemplo, con UserDefaults, entonces, en relación con esta situación, se impone la misma dependencia implícita.

Nota 2. No es del todo correcto decir que solo debido a la auto-evaluación no vale la pena usar singletones dentro del cuerpo de la función. En general, desde el punto de vista de la programación, no es del todo correcto que con la misma entrada, la función produzca resultados diferentes. Es solo que en las pruebas automáticas este problema surgió más claramente.

Complemente el ejemplo anterior. Tiene un objeto que contiene 9 configuraciones de usuario (variables), por ejemplo, derechos para leer / editar / firmar / imprimir / reenviar / eliminar / bloquear / ejecutar / copiar un documento. Su función usa solo tres variables de esta configuración. ¿Qué le pasa a la función: todo el objeto con 9 variables como un parámetro, o solo tres configuraciones necesarias con tres parámetros separados? Muy a menudo ampliamos los objetos transferidos para no establecer muchos parámetros, es decir, seleccionamos la primera opción. Este método se considerará la transferencia de "dependencias excesivamente amplias" . Como ya ha adivinado, para fines de autocomprobación, es mejor usar la segunda opción y pasar solo los parámetros que se usan.

Hicimos 2 conclusiones:
- la función debe recibir todos los parámetros necesarios en la entrada
- la función no debe recibir parámetros de entrada innecesarios

Queríamos lo mejor, pero obtuvimos una función con 6 parámetros. Suponga que todo está en orden dentro de la función, pero alguien debería asumir la responsabilidad de proporcionar parámetros de entrada a la función. Como ya escribí, mi razonamiento es incompleto. Me refiero no solo a una función de clase regular, sino más bien a una función de inicialización / creación de módulos (vip, viper, objeto de datos, etc.). En este contexto, reformulamos la pregunta: ¿quién debe proporcionar los parámetros de entrada para crear el módulo?

Una solución sería cambiar este caso al módulo de llamada. Pero luego resulta que el módulo de llamada necesita pasar los parámetros del niño. Esto conlleva las siguientes complicaciones:

En primer lugar, un poco antes decidimos evitar "dependencias excesivamente amplias". En segundo lugar, no tiene que esforzarse mucho para comprender que habrá muchos parámetros, y será muy tedioso editarlos cada vez que agregue módulos secundarios, incluso duele pensar en eliminar módulos secundarios. Por cierto, en algunas aplicaciones es imposible construir una jerarquía de módulos: mira cualquier red social: perfil -> amigos -> perfil de amigo -> amigos de amigo, etc. En tercer lugar, el principio SOLI D puede recordarse sobre este tema: "Los módulos de nivel superior son independientes de los módulos de nivel inferior"

Esto da lugar a la idea de hacer la creación / inicialización del módulo en una estructura separada. Entonces es hora de escribir algunas líneas como ejemplo:

class AccountList { public func showAccountDetail(account: String) { let accountDetail = AccountDetail.make(account: account) // to do something with accountDetail } } class AccountDetail { private init(account: String, permission1: Bool, permission2: Bool) { print("account = \(account), p1 = \(permission1), p2 = \(permission2)") } } extension AccountDetail { public static func make(account: String) -> AccountDetail? { let p1 = ... let p2 = ... return AccountDetail(account: account, permission1: p1, permission2: p2) } } 

En el ejemplo, hay un módulo de la lista de cuentas AccountList, que llama al módulo de información detallada sobre la cuenta AccountDetail.

Para inicializar el módulo AccountDetail, se necesitan 3 variables. La variable Account AccountDetail recibe del módulo principal, se inyectan las variables permiso1, permiso2. Debido a la inyección, una llamada de módulo con detalles de factura se verá así:

 let accountDetail = AccountDetail.make(account: account) 

en lugar de

 let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2) 

y el módulo principal de la lista de cuentas, AccountList, será relevado de la obligación de pasar parámetros con permisos sobre los cuales él no sabe nada.

Procesé la implementación de inyección (ensamblaje) en una función estática en una extensión de clase. Pero la implementación puede ser cualquiera a su discreción.

Como vemos:

  1. El módulo recibió los parámetros necesarios. Su creación y ejecución se pueden probar de forma segura en todos los conjuntos de valores.
  2. Los módulos son independientes, no hay necesidad de transferir nada para niños o solo el mínimo necesario.
  3. Los módulos NO hacen el trabajo de proporcionar datos; usan datos ya preparados (p1, p2). Por lo tanto, si desea cambiar algo en el almacenamiento o la provisión de datos, entonces no tiene que hacer cambios en el código funcional de los módulos (así como en sus pruebas automáticas), pero solo necesita cambiar el sistema de ensamblaje o las extensiones con el ensamblaje.

La esencia de la inyección de dependencia es la construcción de dicho proceso en el que, al llamar a un módulo desde otro, un objeto / mecanismo independiente transfiere (inyecta) datos al módulo llamado. En otras palabras, el módulo llamado se configura externamente.

Existen varios métodos de configuración:
Inyección de constructor , inyección de propiedad , inyección de interfaz .
Para Swift:
Inyección de inicializador , inyección de propiedad , inyección de método .

Las más comunes son las inyecciones y propiedades del constructor (inicialización).
Importante: en casi todas las fuentes, se recomienda que se prefieran las inyecciones de constructor. Compare la inyección de constructor / inicializador y la inyección de propiedades:

 let account = .. let p1 = ... let p2 = ... let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2) 

mejor que

 let accountDetail = AccountDetail() accountDetail.account = .. accountDetail.permission1 = ... accountDetail.permission2 = ... 

Parece que las ventajas del primer método son obvias, pero por alguna razón algunos entienden la inyección como la configuración de un objeto ya creado y usan el segundo método. Estoy para el primer método:

  1. la creación por el diseñador garantiza un objeto válido;
  2. con la inyección de propiedades, no está claro si es necesario probar un cambio en una propiedad en lugares distintos de la creación;
  3. En los lenguajes que utilizan la opcionalidad, para implementar la inyección de propiedades, debe hacer que los campos sean opcionales o crear métodos de inicialización inteligentes (los flojos no siempre funcionarán). La opción excesiva agrega código innecesario y conjuntos de pruebas innecesarios.

Sin embargo, hasta que nos deshicimos de algunas dependencias, las cambiamos de un hombro a otro. Una pregunta lógica es de dónde obtener los datos en el propio ensamblaje (haga que funcione en el ejemplo).

El uso de singletones en el mecanismo de ensamblaje ya no conduce a los problemas anteriores con dependencia oculta, porque Puede probar la creación de módulos con cualquier conjunto de datos.
Pero aquí nos enfrentamos con otro inconveniente del singleton: mal manejo (probablemente pueda traer muchos argumentos de odio, pero pereza). No es bueno dispersar sus muchos / singletones almacenados en ensamblajes, por analogía con cualquiera, ya que se dispersaron en módulos funcionales. Pero incluso esa refactorización ya será el primer paso hacia la higiene, porque luego puede restablecer el orden en los ensamblajes casi sin afectar el código y las pruebas del módulo.

Si desea optimizar aún más la arquitectura, así como probar las transiciones y el trabajo de ensamblaje, tendrá que trabajar un poco más.

El concepto DI nos ofrece almacenar todos los datos necesarios en un contenedor. Esto es conveniente En primer lugar, guardar (registrar) y recibir (resolver) datos pasa por un único objeto contenedor, respectivamente, por lo que es más fácil administrar los datos y probarlos. En segundo lugar, puede tener en cuenta la dependencia de los datos entre sí. En muchos idiomas, incluido swift, hay contenedores de gestión de dependencias ya preparados, generalmente las dependencias forman un árbol. Los demás pros y contras que no enumeraré, puedes leer sobre ellos en los enlaces que publiqué al principio de la publicación.

Así es como se vería el ensamblaje que usa el contenedor.

 import Foundation import Swinject public class Configurator { private static let container = Container() public static func register<T>(name: String, value: T) { container.register(type(of: value), name: name) { _ in value } } public static func resolve<T>(service: T.Type, name: String) -> T? { return container.resolve(service, name: name) } } extension AccountDetail { public static func make(account: String) -> AccountDetail? { if let p1 = Configurator.resolve(service: Bool.self, name: "permission1"), let p2 = Configurator.resolve(service: Bool.self, name: "permission2") { return AccountDetail(account: account, permission1: p1, permission2: p2) } else { return nil } } } // -   ,         //   ()  Configurator.register(name: "permission1", value: true) Configurator.register(name: "permission2", value: false) ... 

Este es un posible ejemplo de implementación. El ejemplo utiliza el marco Swinject , que nació no hace mucho tiempo. Swinject le permite crear un contenedor para la gestión automatizada de dependencias, y también le permite crear contenedores para Storyboards. Se puede encontrar más información sobre Swinject en los ejemplos en raywenderlich . Realmente me gusta este sitio, pero este ejemplo no es el más exitoso, ya que considera el uso del contenedor solo en las pruebas automáticas, mientras que el contenedor debe establecerse en la arquitectura de la aplicación. Usted en su código, puede escribir un contenedor usted mismo.

Gracias a todos por esto. Espero que no te hayas aburrido leyendo este texto.

Source: https://habr.com/ru/post/456588/


All Articles