ioc container - ¿Por qué son innecesarios los contenedores de IOC con lenguajes dinámicos?
ioc-container dynamic-languages (9)
IoC proporciona un mecanismo para romper el acoplamiento que obtienes cuando un objeto llama ''nuevo'' en otra clase.
Es una visión ingenua en IoC. Por lo general, IoC también resuelve:
- resolviendo la dependencia
- búsqueda automática de componentes e inicialización (si usa ''require'' con IoC, hay algo mal)
- funciona no solo con singletons sino también con alcance dinámico
- 99.9% de las veces es invisible para el desarrollador
- elimina la necesidad de app.config
Artículo completo Usted subestima el poder de IoC
Alguien en el podcast del código de pastoreo n. ° 68, http://herdingcode.com/herding-code-68-new-year-shenanigans/ , afirmó que los contenedores del COI no tenían cabida en Python o Javascript, o palabras al respecto. Estoy asumiendo que esto es sabiduría convencional y que se aplica a todos los lenguajes dinámicos. ¿Por qué? ¿Qué pasa con los lenguajes dinámicos que hace que los contenedores IOC sean innecesarios?
Creo que los contenedores IoC son necesarios en grandes aplicaciones de JavaScript. Puede ver que algunos frameworks de JavaScript populares incluyen un contenedor IoC (por ejemplo, el $injector
angular $injector
).
He desarrollado un contenedor IoC llamado InversifyJS, puede obtener más información al respecto en http://inversify.io/ .
Algunos contenedores JavaScript IoC declaran que las dependencias se deben inyectar de la siguiente manera:
import Katana from "./entitites/katana";
import Shuriken from "./entitites/shuriken";
@inject(Katana, Shuriken) // Wrong as Ninja is aware of Katana and Shuriken!
class Ninja {
constructor(katana: IKatana, shuriken: IShuriken) {
// ...
Lo bueno de este enfoque es que no hay literales de cadenas. Lo malo de esto es que nuestro objetivo era lograr el desacoplamiento y acabamos de agregar una referencia codificada a Katana y Shuriken al archivo en el que se declara Ninja y esto no es un desacoplamiento real.
InversifyJS te ofrece un desacoplamiento real. El archivo ninja.js nunca apuntará a los archivos katana o shuriken. Sin embargo, señalará las interfaces (en tiempo de diseño) o literales de cadena (en tiempo de ejecución) que es admisible porque son abstracciones y, de acuerdo con las abstracciones, de eso se trata DI.
import * as TYPES from "./constants/types";
@inject(TYPES.IKATANA, TYPES.ISHURIKEN) // Right as Ninja is aware of abstractions of Katana and Shuriken!
class Ninja {
constructor(katana: IKatana, shuriken: IShuriken) {
// ...
El kernel de InversifyJS es el único elemento en la aplicación que conoce el ciclo de vida y las dependencias. Recomendamos hacer esto en un archivo llamado inversify.config.ts
y almacenar el archivo en la carpeta raíz que contiene el código fuente de la aplicación:
import * as TYPES from "./constants/types";
import Katana from "./entitites/katana";
import Shuriken from "./entitites/shuriken";
import Ninja from "./entitites/ninja";
kernel.bind<IKatana>(TYPES.IKATANA).to(Katana);
kernel.bind<IShuriken>(TYPES.ISHURIKEN).to(Shuriken);
kernel.bind<INinja>(TYPES.ININJA).to(Ninja);
Esto significa que todo el acoplamiento en su aplicación tiene lugar en un único lugar : el archivo inversify.config.ts
. Esto es realmente importante y vamos a demostrarlo con un ejemplo. Imaginemos que estamos cambiando la dificultad en un juego. Solo tenemos que ir a inversify.config.ts
y cambiar la atadura de Katana:
import Katana from "./entitites/SharpKatana";
if(difficulty === "hard") {
kernel.bind<IKatana>(TYPES.IKATANA).to(SharpKatana);
} else {
kernel.bind<IKatana>(TYPES.IKATANA).to(Katana);
}
¡No necesita cambiar el archivo Ninja!
El precio a pagar son los literales de cadena, pero este precio puede mitigarse si declara todos los literales de cadena en un archivo que contiene constantes ( como acciones en Redux ). La buena noticia es que, en el futuro, los literales de cadenas podrían terminar siendo generados por el compilador TS , pero eso está en manos del comité TC39 por el momento.
Puedes probarlo en línea here .
Estoy de acuerdo con las respuestas anteriores, pero pensé que podría incluir un poco aquí también con respecto a las pruebas:
En sistemas complejos donde hay interacciones entre subsistemas, la inyección de dependencia es la mejor forma que conozco para realizar pruebas unitarias.
Si tiene una unidad lógica X, que tiene interacciones conocidas con la unidad lógica Y, puede crear un MockY que tenga un comportamiento predefinido y probar explícitamente la lógica de X.
Sin inyección de dependencia, escribir pruebas es una pesadilla. No se puede obtener una buena cobertura de código. Algunos frameworks (p. Ej., Django) solucionan este problema mediante la creación de instancias de base de datos simuladas para las pruebas, etc., pero básicamente es una solución pobre al problema.
Debe haber dos tipos de pruebas:
- Pruebas unitarias que se ejecutan en CUALQUIER entorno y prueban la lógica de las unidades de código individuales.
- Pruebas de integración / funcionales que prueban la lógica de aplicación combinada.
Ahora a la pregunta: IoC. ¿Para qué sirve la IoC? Es útil para algunas cosas, pero es realmente bueno para facilitar el uso de la inyección de dependencia :
// Do this every time you want an instance of myServiceType
var SystemA = new SystemA()
var SystemB = new SystemB()
var SystemC = new SystemC(SystemA, "OtherThing")
var SystemD = new SystemD(SystemB, SystemC)
var IRepo = new MySqlRepo()
var myService = new myServiceType(SystemD, IRepo)
En esta lógica:
// Do this at application start
Container.Register(ISystemA, SystemA)
Container.Register(ISystemB, SystemB)
Container.Register(ISystemC, SystemC)
Container.Register(ISystemD, SystemD)
Container.Register(IRepo, MySqlRepo)
Container.Register(myServiceType)
// Do this any time you like
var myService = Container.resolve(myServiceType)
Ahora, ¿por qué no vemos IOC en muchos lenguajes dinámicos?
Yo diría que la razón es que no vemos mucha inyección de dependencia en esos idiomas.
... y eso sería porque normalmente las pruebas realizadas en ellos son inexistentes.
He escuchado todo tipo de excusas para esto; interactuar con el DOM hace que las pruebas sean difíciles, mi código es lo suficientemente simple como para no requerir pruebas, los idiomas dinámicos no necesitan pruebas unitarias porque son increíbles y expresivos.
No tiene sentido.
No hay excusa para un proyecto sin pruebas unitarias o pruebas unitarias con una cobertura de código deficiente.
... pero es increíble la cantidad de proyectos javascript y python que he visto (escogiendo estos dos específicamente solo porque son un área de interés y he visto más proyectos de este tipo que otros) sin IoC, sin DI, y como era de esperar, no hay pruebas.
Hay un excelente artículo sobre DI en el sitio web de guice aquí: http://code.google.com/p/google-guice/wiki/Motivation
No hay nada sobre los lenguajes dinámicos que resuelva ninguno de estos problemas.
Resumen:
- IoC es útil para cosas, pero principalmente para implementar DI
- IoC NO es archivos de configuración xml. > _ <
- DI es útil para las pruebas
- La ausencia de IOC es indicativa de la ausencia de DI, lo que es indicativo de la ausencia de buenas pruebas.
- Use IoC.
Herding Code 82 (6/6/10) compara Ruby con .NET e incluye una discusión detallada sobre hasta qué punto .NET necesita más IOC / DI que Ruby.
IoC proporciona un mecanismo para romper el acoplamiento que obtienes cuando un objeto llama ''nuevo'' en otra clase. Este acoplamiento vincula el objeto que llama con la implementación instanciada de cualquier interfaz que implemente.
En los lenguajes estáticos cuando hace referencia a una clase por nombre (para llamar a new
en él), no hay ambigüedad. Este es un acoplamiento estrecho a una clase específica.
En los lenguajes dinámicos, llamar a new X
es un marcador de posición para "instanciar cualquier clase que se define como X
en el punto de ejecución". Este es un acoplamiento más flexible , ya que solo está acoplado al nombre X
Esta sutil diferencia significa que en un lenguaje dinámico, por lo general, puede cambiar lo que X
es, por lo que la decisión sobre qué clase se crea una instancia todavía se puede modificar fuera de la clase de llamada.
Sin embargo, personalmente encuentro que hay dos ventajas para IoC que no obtengo al confiar en el lenguaje dinámico para permitir la inyección.
Uno de los efectos secundarios de las dependencias que pasan a través de los constructores es que terminas con clases de "building block" que están muy desacopladas, reutilizables y fáciles de probar. No tienen idea en qué contexto están destinados a ser utilizados, por lo que puede volver a utilizarlos en cualquier lugar.
El otro resultado es tener un código explícito para hacer el cableado. Hecho correctamente, esto representa claramente la estructura de su aplicación y su descomposición en subsistemas y ciclos de vida. Esto hace que las personas decidan explícitamente con qué ciclo de vida o subsistema desean asociar su clase (al escribir el código de cableado), y se concentran en el comportamiento del objeto al escribir la clase.
Al igual que Jörg W Mittag dijo ... "Esas herramientas son innecesarias, los principios de diseño no lo son". Creo que son innecesarios, pero están bien, aún son valiosos.
Los contenedores IoC realmente permiten una capa de composición en lenguajes procesados / OO de tipo estático.
Esta capa compositiva existe de forma relativamente natural en lenguajes dinámicos como Python o Javascript (tenga en cuenta que Javascript se basa en gran medida en Scheme).
Probablemente puedas argumentar bien que los contenedores IoC son solo una generalización del patrón Interpreter.
Porque ya están integrados en el lenguaje.
Un contenedor IoC proporciona dos cosas:
- enlace dinámico
- un lenguaje dinámico (por lo general increíblemente malo, construido sobre XML o en versiones más nuevas además de las anotaciones de Java / atributos .NET)
La vinculación dinámica ya es parte del lenguaje dinámico y el lenguaje dinámico ya es un lenguaje dinámico. Por lo tanto, un contenedor IoC simplemente no tiene sentido: el idioma ya es un contenedor IoC.
Otra forma de verlo: ¿qué es lo que un contenedor IoC le permite hacer? Le permite tomar componentes independientes y conectarlos en una aplicación, sin que ninguno de los componentes sepa nada el uno del otro. Hay un nombre para conectar piezas independientes en una aplicación: ¡secuencias de comandos! (Esa es prácticamente la definición de scripting.) Muchos lenguajes dinámicos también son muy buenos para el scripting, por lo tanto, son perfectos como contenedores IoC.
Tenga en cuenta que no estoy hablando de Inyección de Dependencia o Inversión de Control. DI y IoC son tan importantes en los lenguajes dinámicos como en los idiomas estáticos, por exactamente las mismas razones. De lo que estoy hablando son contenedores IoC y marcos DI. Esas herramientas son innecesarias, los principios de diseño no lo son.
Una de las principales características de los contenedores IOC es que puede "conectar" automáticamente sus módulos en tiempo de ejecución. En los lenguajes dinámicos puede hacerlo con bastante facilidad sin una lógica extravagante basada en la reflexión. Sin embargo, los contenedores de IOC son un patrón útil que muchas personas entienden y, en ocasiones, puede ser beneficioso utilizar el mismo estilo de diseño. Vea este artículo para otro punto de vista.
Yo tengo una opinión diferente. Creo que los contenedores de IOC ciertamente tienen un papel en los lenguajes dinámicos.
No comparto la opinión de que un lenguaje dinámico elimina la necesidad de una composición de objetos claramente estructurada. O que un lenguaje dinámico ''proporciona'' la misma funcionalidad.
Un contenedor de IOC es simplemente una herramienta para administrar esta organización.
Incluso en un lenguaje dinámico, quiero "conectar" componentes juntos. Sin hacer dependencias duras entre esos componentes. O tal vez incluso sin especificar la clase de implementación real para esos componentes.