Zenject: c贸mo un contenedor de IoC puede matar la inyecci贸n de dependencia en su proyecto

驴D贸nde comienza el peligro? Suponga que est谩 firmemente decidido a desarrollar un proyecto, adhiri茅ndose a un concepto o enfoque espec铆fico. En nuestra situaci贸n, esto es DI, aunque la programaci贸n reactiva, por ejemplo, tambi茅n puede estar en su lugar. Es l贸gico que para lograr su objetivo, recurra a soluciones preparadas (en nuestro ejemplo, el contenedor DI Zenject). Se familiarizar谩 con la documentaci贸n y comenzar谩 a construir el marco de la aplicaci贸n utilizando la funcionalidad principal. Si en las primeras etapas del uso de la soluci贸n no tiene sensaciones desagradables, lo m谩s probable es que permanezca en su proyecto durante toda su vida. A medida que trabaje con las funciones b谩sicas de la soluci贸n (contenedor), es posible que tenga preguntas o deseos para hacer que alguna funcionalidad sea m谩s bella o efectiva. Seguramente, en primer lugar, recurrir谩 a las "caracter铆sticas" m谩s avanzadas de la soluci贸n (contenedor) para esto. Y en esta etapa, puede surgir la siguiente situaci贸n: ya conoce bien y conf铆a en la soluci贸n elegida, por lo que muchos pueden no pensar en cu谩n ideol贸gicamente correcto puede ser el uso de uno u otro funcional en la soluci贸n, o la transici贸n a otra soluci贸n ya es bastante costosa e inapropiada ( por ejemplo, se acerca la fecha l铆mite). Es en esta etapa que puede surgir la situaci贸n m谩s peligrosa: la funcionalidad de la soluci贸n se utiliza con poco cuidado o, en casos excepcionales, simplemente en la m谩quina (sin pensar).

驴Qui茅n podr铆a estar interesado en esto?


Este art铆culo ser谩 煤til tanto para quienes est谩n familiarizados con la DI como para los principiantes. Para comprender suficiente conocimiento b谩sico sobre qu茅 patrones utiliza DI, el prop贸sito de DI y las funciones que realiza un contenedor de IoC. No se trata de las complejidades de la implementaci贸n de Zenject, sino de la aplicaci贸n de parte de su funcionalidad. El art铆culo se basa 煤nicamente en la documentaci贸n oficial de Zenject y ejemplos de c贸digo, as铆 como en el libro de Mark Siman "Inyecci贸n de dependencias en .NET", que es un trabajo exhaustivo cl谩sico sobre el tema de la teor铆a DI. Todas las citas en este art铆culo son extractos del libro de Mark Siman. A pesar del hecho de que hablaremos sobre un contenedor espec铆fico, el art铆culo puede ser 煤til para aquellos que usan otros contenedores.

El prop贸sito de este art铆culo es mostrar c贸mo una herramienta cuyo prop贸sito es ayudarlo a implementar DI en su proyecto puede guiarlo en una direcci贸n completamente diferente, empuj谩ndolo a cometer errores que vinculan su c贸digo, reducen la capacidad de prueba del c贸digo y, en general, lo privan de todas las ventajas que pueden brindarle usted DI.

Descargo de responsabilidad : El prop贸sito de este art铆culo no es criticar a Zenject ni a sus autores. Zenject se puede utilizar para el prop贸sito previsto y servir como una excelente herramienta para implementar DI, siempre que no utilice un conjunto completo de sus funciones, ya que ha definido algunas limitaciones para usted.

Introduccion


Zenject es un contenedor de inyecci贸n de dependencia de c贸digo abierto destinado a usar el motor de juego Unity3D, que funciona en la mayor铆a de las plataformas compatibles con Unity3D. Vale la pena se帽alar que Zenject tambi茅n se puede usar para aplicaciones C # desarrolladas sin Unity3D. Este contenedor es bastante popular entre los desarrolladores de Unity, est谩 activamente respaldado y desarrollado. Adem谩s, Zenject tiene toda la funcionalidad necesaria del contenedor DI.

Us茅 Zenject en 3 grandes proyectos de Unity, y tambi茅n me comuniqu茅 con una gran cantidad de desarrolladores que lo usaron. La raz贸n para escribir este art铆culo son las preguntas frecuentes:

  • 驴Usar Zenject es una buena soluci贸n?
  • 驴Qu茅 le pasa a Zenject?
  • 驴Qu茅 dificultades surgen al usar Zenject?

Y tambi茅n algunos proyectos en los que el uso de Zenject no condujo a la soluci贸n de problemas de conectividad de c贸digo fuerte y arquitectura fallida, sino que exacerb贸 la situaci贸n.

Veamos por qu茅 los desarrolladores tienen tales preguntas y problemas. Puede responder de la siguiente manera:
Ir贸nicamente, los contenedores DI en s铆 tienden a ser dependencias estables. ... Cuando decide desarrollar su aplicaci贸n en funci贸n de un contenedor DI particular, corre el riesgo de limitarse a esta opci贸n durante todo el ciclo de vida de la aplicaci贸n.
Vale la pena se帽alar que con el uso adecuado y limitado del contenedor, cambiar a usar otro contenedor en la aplicaci贸n (o negarse a usar el contenedor a favor de la " implementaci贸n para los pobres ") es bastante posible y no tomar谩 mucho tiempo. Es cierto que en tal situaci贸n, es poco probable que lo necesite.

Antes de comenzar a desmontar la funcionalidad potencialmente peligrosa de Zenject, tiene sentido actualizar superficialmente varios aspectos b谩sicos de DI.

El primer aspecto es el prop贸sito de los contenedores DI. Mark Siman escribe lo siguiente en su libro sobre este tema:
Un contenedor DI es una biblioteca de software que puede automatizar muchas de las tareas que se realizan al ensamblar objetos y administrar su ciclo de vida.
No espere que el contenedor DI convierta m谩gicamente el c贸digo fuertemente acoplado en c贸digo d茅bilmente acoplado. Un contenedor puede mejorar la eficiencia del uso de DI, pero el 茅nfasis en la aplicaci贸n debe colocarse principalmente en el uso de patrones y trabajar con DI.
El segundo aspecto son los patrones DI . Mark Siman identifica cuatro patrones principales, ordenados por frecuencia y la necesidad de su uso:

  1. Implementaci贸n del constructor: 驴c贸mo podemos garantizar que la dependencia requerida siempre estar谩 disponible para la clase que se est谩 desarrollando?
  2. Implementaci贸n de propiedad: 驴c贸mo puedo habilitar DI como una opci贸n en la clase si hay un valor predeterminado local adecuado?
  3. Implementaci贸n del m茅todo: 驴c贸mo puedo inyectar dependencias en una clase si son diferentes para cada operaci贸n?
  4. Contexto ambiental: 驴c贸mo podemos hacer que una dependencia est茅 disponible en cada m贸dulo sin incluir aspectos transversales de la aplicaci贸n en cada componente API?

Las preguntas indicadas junto al nombre de los patrones describen completamente su alcance. Al mismo tiempo, el art铆culo no discutir谩 la Implementaci贸n del Constructor (ya que pr谩cticamente no hay quejas sobre su implementaci贸n en Zenject) y el Contexto Ambiental (su implementaci贸n no est谩 en el contenedor, pero puede implementarlo f谩cilmente en funci贸n de la funcionalidad existente).
Ahora puede ir directamente a la funcionalidad potencialmente peligrosa de Zenject.

Funcionalidad peligrosa.


Implementar propiedades


Este es el segundo patr贸n DI m谩s com煤n, despu茅s de la implementaci贸n del constructor, pero se usa con mucha menos frecuencia. Implementado en Zenject de la siguiente manera:

public class Foo { [Inject] public IBar Bar { get; private set; } } 

Adem谩s, Zenject tambi茅n tiene un concepto como "Inyecci贸n de campo". Veamos por qu茅 en todos los Zenject esta funcionalidad es la m谩s peligrosa.

  • Se utiliza un atributo para mostrar al contenedor qu茅 campo incrustar. Esta es una soluci贸n completamente comprensible, desde el punto de vista de la simplicidad y la l贸gica de implementaci贸n del contenedor en s铆. Sin embargo, vemos un atributo (as铆 como un espacio de nombres) en el c贸digo de la clase. Es decir, al menos indirectamente, pero la clase comienza a saber de d贸nde obtiene la dependencia. Adem谩s, estamos comenzando a ajustar el c贸digo de clase en el contenedor. En otras palabras, ya no podemos negarnos a usar Zenject sin manipular el c贸digo de clase.
  • El patr贸n en s铆 se usa en situaciones en las que la dependencia tiene un valor predeterminado local. Es decir, esta es una dependencia opcional, y si el contenedor no puede proporcionarla, entonces no habr谩 errores en el proyecto y todo funcionar谩. Sin embargo, con Zenject, siempre obtienes esta dependencia: la dependencia no se convierte en opcional.
  • Dado que la dependencia en este caso no es opcional, comienza a estropear toda la l贸gica de la implementaci贸n del constructor, porque solo las dependencias requeridas deben introducirse all铆. Al implementar dependencias no opcionales a trav茅s de propiedades, tiene la oportunidad de crear dependencias circulares en el c贸digo. No ser谩n tan obvios, porque en Zenject, la implementaci贸n del constructor se cumple primero, y luego la implementaci贸n de la propiedad, y no recibir谩 una advertencia del contenedor.
  • El uso del contenedor DI implica la implementaci贸n del patr贸n de ra铆z de composici贸n, sin embargo, el uso del atributo para configurar la implementaci贸n de la propiedad lleva al hecho de que configura el c贸digo no solo en la ra铆z de composici贸n, sino tambi茅n seg煤n sea necesario en cada clase.

F谩bricas (y MemoryPool)


La documentaci贸n de Zenject tiene una secci贸n completa sobre f谩bricas. Esta funcionalidad se implementa a nivel del contenedor en s铆, y tambi茅n es posible crear sus propias f谩bricas personalizadas. Echemos un vistazo al primer ejemplo de la documentaci贸n:

 public class Enemy { DiContainer Container; public Enemy(DiContainer container) { Container = container; } public void Update() { ... var player = Container.Resolve<Player>(); WalkTowards(player.Position); ... etc. } } 

Ya en este ejemplo hay una violaci贸n grave de DI. Pero este es m谩s bien un ejemplo de c贸mo hacer una f谩brica totalmente personalizada. 驴Cu谩l es el principal problema aqu铆?
Un contenedor DI puede considerarse err贸neamente como un localizador de servicios, pero solo debe usarse como un mecanismo para vincular gr谩ficos de objetos. Si consideramos el contenedor desde este punto de vista, tiene sentido limitar su uso solo a la ra铆z del dise帽o. Este enfoque tiene la ventaja importante de que elimina cualquier enlace entre el contenedor y el resto del c贸digo de la aplicaci贸n.
Veamos c贸mo funcionan las f谩bricas "integradas" de Zenject. Hay una interfaz IFactory para esto, cuya implementaci贸n nos lleva a la clase PlaceholderFactory:

  public abstract class PlaceholderFactory<TValue> : IPlaceholderFactory { [Inject] void Construct(IProvider provider, InjectContext injectContext) 

En 茅l vemos el par谩metro InjectContext que tiene muchos constructores, de la forma:

  public InjectContext(DiContainer container, Type memberType) : this() { Container = container; MemberType = memberType; } 

Y nuevamente, obtenemos la transferencia del contenedor en s铆 como una dependencia de la clase. Este enfoque es una violaci贸n grave de DI y una transformaci贸n parcial del contenedor en un Localizador de servicios.
Adem谩s, la desventaja de esta soluci贸n es que el contenedor se usa para crear dependencias a corto plazo, y solo debe crear dependencias a largo plazo.

Para evitar tales violaciones, los autores del contenedor podr铆an excluir por completo la posibilidad de pasar el contenedor como una dependencia a todas las clases registradas. No ser铆a dif铆cil implementar esto, dado que todo el contenedor funciona mediante la reflexi贸n y el an谩lisis de los par谩metros de m茅todos y constructores para crear y dise帽ar el gr谩fico de los objetos de la aplicaci贸n.

Implementaci贸n del m茅todo


La l贸gica de la implementaci贸n del M茅todo en Zenject es la siguiente: primero, en todas las clases, se implementa el constructor, luego se implementan las propiedades y finalmente se implementa el m茅todo. Considere el ejemplo de implementaci贸n proporcionado en la documentaci贸n:

 public class Foo { [Inject] public Init(IBar bar, Qux qux) { _bar = bar; _qux = qux; } } 

驴Cu谩les son las desventajas aqu铆:

  • Puede escribir cualquier n煤mero de m茅todos que se implementar谩n dentro del marco de una clase. Por lo tanto, como en el caso de la implementaci贸n de la propiedad, tenemos la oportunidad de hacer tantas dependencias c铆clicas como sea posible.
  • Al igual que la implementaci贸n de una propiedad, la implementaci贸n de un m茅todo se implementa mediante un atributo, que asocia su c贸digo con el c贸digo del contenedor en s铆.
  • La implementaci贸n del m茅todo en Zenject se usa solo como una alternativa a los constructores, lo cual es conveniente en el caso de las clases MonoBehavior, pero contradice absolutamente la teor铆a descrita por Mark Siman. El ejemplo cl谩sico de la implementaci贸n can贸nica del m茅todo puede considerarse el uso de f谩bricas (m茅todos de f谩brica).
  • Si hay varios m茅todos introducidos en la clase, o adem谩s del m茅todo tambi茅n hay un constructor, resulta que las dependencias necesarias para la clase se dispersar谩n en diferentes lugares, lo que interferir谩 con la imagen como un todo. Es decir, si la clase 1 tiene un constructor, entonces el n煤mero de sus par谩metros puede mostrar claramente si hay errores de dise帽o en la clase y si se viola el principio de responsabilidad exclusiva, y si las dependencias se dispersan por varios m茅todos, por el constructor, o tal vez por un par de propiedades, entonces la imagen no ser谩 tan obvia como podr铆a ser.

Se deduce que la presencia de tal implementaci贸n de la implementaci贸n del m茅todo en el contenedor, que contradice la teor铆a DI, no tiene una sola ventaja. Con una gran advertencia, un plus solo puede considerarse la posibilidad de utilizar el m茅todo implementado, como un constructor para MonoBehaviour. Pero este es un momento bastante controvertido, ya que desde el punto de vista de la l贸gica del contenedor, los patrones DI y el dispositivo de memoria interna Unity3D, todos los objetos MonoBehaviour en su aplicaci贸n pueden considerarse administrados por recursos, y en este caso, ser谩 mucho m谩s eficiente delegar la administraci贸n del ciclo de vida de dichos objetos. no un contenedor DI, sino una clase auxiliar (ya sea Wrapper, ViewModel, Fasade u otra cosa).

Enlaces globales


Esta es una funcionalidad auxiliar bastante conveniente que le permite establecer carpetas globales que pueden vivir independientemente de la transici贸n entre escenas. Puedes leer m谩s en la documentaci贸n . Esta funcionalidad es extremadamente conveniente y bastante 煤til. Vale la pena se帽alar que no viola los patrones y principios de DI, sin embargo, tiene una implementaci贸n fea y obvia. La conclusi贸n es que crea un tipo especial de prefabricado, adjunta un script con la configuraci贸n del contenedor (instalador) y lo guarda en una carpeta de proyecto estrictamente definida, sin la capacidad de moverse a ning煤n lado y sin ning煤n enlace. La desventaja de esta herramienta radica 煤nicamente en su car谩cter impl铆cito. Cuando se trata de instaladores normales, todo es bastante simple: tienes un objeto en el escenario, el script del instalador se cuelga de 茅l. Si un nuevo desarrollador llega al proyecto, el instalador se convierte en un excelente punto de inmersi贸n en el proyecto. Basado en un 煤nico instalador, un desarrollador puede hacerse una idea de en qu茅 m贸dulos consiste un proyecto y c贸mo se construye un gr谩fico de objetos. Pero con el uso de carpetas globales, el instalador en el escenario deja de ser una fuente suficiente de esta informaci贸n. No hay un solo enlace al enlace global en el c贸digo de otros instaladores (presente en las escenas) y, por lo tanto, no ve el gr谩fico completo de los objetos. Y solo durante el an谩lisis de las clases entiendes que algunos de los ligantes no son suficientes en el instalador en el escenario. Una vez m谩s, har茅 una reserva de que este inconveniente es puramente cosm茅tico.

Identificadores


La capacidad de establecer un enlace espec铆fico para un identificador con el fin de obtener una cierta dependencia de un conjunto de dependencias similares en una clase. Un ejemplo:

 Container.Bind<IFoo>().WithId("foo").To<Foo1>().AsSingle(); Container.Bind<IFoo>().To<Foo2>().AsSingle(); public class Bar1 { [Inject(Id = "foo")] IFoo _foo; } public class Bar2 { [Inject] IFoo _foo; } 

Esta funcionalidad puede ser realmente 煤til situacionalmente, y es una opci贸n adicional para la implementaci贸n de propiedades. Sin embargo, junto con la conveniencia, hereda todos los problemas identificados en el p谩rrafo "Implementando propiedades", agregando a煤n m谩s coherencia al c贸digo al introducir una cierta constante que debe recordar al configurar su c贸digo. Si elimina accidentalmente este identificador, puede obtener f谩cilmente uno que no funciona de la aplicaci贸n de trabajo.

Se帽ales e ITickable


Las se帽ales son un an谩logo del mecanismo del Agregador de eventos integrado en el contenedor. La idea de implementar esta funcionalidad es indudablemente noble, ya que tiene como objetivo reducir el n煤mero de conexiones entre objetos que se comunican a trav茅s del mecanismo de eventos-suscripciones. Un ejemplo bastante voluminoso se puede encontrar en la documentaci贸n , sin embargo, no estar谩 en el art铆culo, porque la implementaci贸n espec铆fica no importa.

Soporte para la interfaz ITickable: reemplaza los m茅todos est谩ndar Update, LateUpdate y FixedUpdate en Unity al delegar las llamadas a los m茅todos de actualizaci贸n de objetos con la interfaz ITickable en el contenedor. Tambi茅n hay un ejemplo en la documentaci贸n , y su implementaci贸n en el contexto del art铆culo tampoco importa.

El problema de Signals and ITickable no se refiere a aspectos de su implementaci贸n, su ra铆z radica en el uso de efectos secundarios del contenedor. En esencia, el contenedor conoce casi todas las clases y sus instancias dentro del proyecto, pero su responsabilidad es crear un gr谩fico de objetos y administrar su ciclo de vida. Al agregar mecanismos como Signals, ITickable, etc., agregamos m谩s responsabilidades al contenedor, y cada vez m谩s le adjuntamos el c贸digo de la aplicaci贸n, convirti茅ndolo en la parte exclusiva e irremplazable del c贸digo, pr谩cticamente un "objeto divino".

En lugar de salida


Lo m谩s importante acerca de los contenedores es entender que el uso de DI es independiente del uso de un contenedor de DI. Se puede construir una aplicaci贸n a partir de muchas clases y m贸dulos poco acoplados, y ninguno de estos m贸dulos debe saber nada sobre el contenedor.
Tenga cuidado al usar soluciones listas para usar (en caja) o peque帽os complementos. 脷salos cuidadosamente. De hecho, incluso cosas m谩s grandiosas en las que conf铆a (por ejemplo, motores de juego de la escala de Unity3D) pueden pecar con tales errores te贸ricos y borrones. Y esto, en 煤ltima instancia, afectar谩 no el trabajo de la soluci贸n que usa, sino la sostenibilidad, el trabajo y la calidad de su producto final. Espero que todos los que hayan le铆do hasta el final, el art铆culo les sea 煤til o, al menos, no se arrepientan del tiempo dedicado a leerlo.

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


All Articles