c# - tag - Diseño: dónde deben registrarse los objetos cuando se usa Windsor
summary c# documentation (2)
Tendré los siguientes componentes en mi aplicación
- Acceso a los datos
- DataAccess.Test
- Negocio
- Business.Test
- Solicitud
Esperaba usar Castle Windsor como IoC para unir las capas, pero no estoy seguro del diseño del encolado.
Mi pregunta es ¿quién debería ser responsable de registrar los objetos en Windsor? Tengo un par de ideas;
- Cada capa puede registrar sus propios objetos. Para probar el BL, el banco de pruebas podría registrar clases simuladas para el DAL.
- Cada capa puede registrar el objeto de sus dependencias, por ejemplo, la capa empresarial registra los componentes de la capa de acceso a datos. Para probar el BL, el banco de pruebas debería descargar el objeto DAL "real" y registrar los objetos simulados.
- La aplicación (o aplicación de prueba) registra todos los objetos de las dependencias.
¿Alguien puede ayudarme con algunas ideas y pros / contras con los diferentes caminos? Los enlaces a proyectos de ejemplo que utilizan Castle Windsor de esta manera serían muy útiles.
En general, todos los componentes en una aplicación deben estar compuestos lo más tarde posible, ya que eso asegura una modularidad máxima, y que los módulos se acoplan lo más libremente posible.
En la práctica, esto significa que debe configurar el contenedor en la raíz de su aplicación.
- En una aplicación de escritorio, eso estaría en el método principal (o muy cerca de él)
- En una aplicación ASP.NET (incluida MVC), eso sería en Global.asax
- En WCF, eso sería en un ServiceHostFactory
- etc.
El contenedor es simplemente el motor que compone los módulos en una aplicación en funcionamiento. En principio, puede escribir el código a mano (esto se llama DI de Poor Man ), pero es mucho más fácil usar un Contenedor DI como Windsor.
Dicha Raíz de Composición será, idealmente, la única pieza de código en la raíz de la aplicación, convirtiendo a la aplicación en un Humble Executable (un término de los excelentes Patrones de Prueba xUnit ) que no necesita pruebas unitarias en sí mismo.
Sus pruebas no deberían necesitar el contenedor en absoluto, ya que sus objetos y módulos deberían poder componerse, y usted puede proporcionarles directamente Test Dobles a partir de las pruebas unitarias. Lo mejor es que pueda diseñar todos sus módulos para que sean independientes del contenedor.
Además, específicamente en Windsor, debe encapsular la lógica de registro de los componentes dentro de los instaladores (los tipos que implementan IWindsorInstaller
). Consulte la documentación para obtener más detalles.
Si bien la respuesta de Mark es excelente para escenarios web, la falla clave al aplicarla a todas las arquitecturas (es decir, cliente rico, es decir, WPF, WinForms, iOS, etc.) es suponer que todos los componentes necesarios para una operación pueden / deben crearse En seguida.
Para los servidores web, esto tiene sentido ya que cada solicitud es extremadamente efímera y el marco subyacente (sin código de usuario) crea un controlador ASP.NET MVC para cada solicitud que entra. De este modo, el controlador y todas sus dependencias pueden ser fácilmente integrados por un marco DI, y hay muy poco costo de mantenimiento para hacerlo. Tenga en cuenta que el marco web es responsable de administrar la vida útil del controlador y, a todos los efectos, la duración de todas sus dependencias (que el marco DI creará / inyectará para usted en la creación del controlador). Está muy bien que las dependencias vivan durante la duración de la solicitud y su código de usuario no necesita administrar el tiempo de vida de los componentes y subcomponentes en sí. También tenga en cuenta que los servidores web son apátridas en diferentes solicitudes (excepto en el estado de la sesión, pero eso es irrelevante para esta discusión) y que nunca tiene múltiples instancias de controlador / controlador infantil que necesitan vivir al mismo tiempo para atender una sola solicitud.
Sin embargo, en aplicaciones de cliente enriquecido, este no es el caso. Si utiliza una arquitectura MVC / MVVM (¡lo que debería hacer!), La sesión de un usuario dura mucho tiempo y los controladores crean subcontroladores / controladores hermanos a medida que el usuario navega por la aplicación (consulte la nota sobre MVVM en la parte inferior). La analogía con el mundo de la web es que cada entrada de usuario (clic de botón, operación realizada) en una aplicación de cliente enriquecido equivale a una solicitud que recibe el marco web. Sin embargo, la gran diferencia es que desea que los controladores en una aplicación de cliente enriquecido permanezcan vivos entre las operaciones (es muy posible que el usuario realice varias operaciones en la misma pantalla, que se rige por un controlador en particular) y que los subcontroladores obtengan creado y destruido mientras el usuario realiza diferentes acciones (piense en un control de pestañas que crea la pestaña de forma perezosa si el usuario navega hacia él, o una pieza de UI que solo necesita cargarse si el usuario realiza acciones particulares en una pantalla).
Ambas características significan que es el código de usuario el que necesita administrar la vida útil de los controladores / subcontroladores, y que las dependencias de los controladores NO se deben crear todas por adelantado (es decir, subcontroladores, modelos de visualización, otros componentes de presentación, etc. ) Si usa un marco DI para realizar estas responsabilidades, terminará con no mucho más código donde no pertenece (Ver: Constructor sobre-inyección anti-patrón ) pero también necesitará pasar un contenedor de dependencia a lo largo de la mayor parte de su capa de presentación para que sus componentes puedan usarla para crear sus subcomponentes cuando sea necesario.
¿Por qué es malo que mi código de usuario tenga acceso al contenedor DI?
1) El contenedor de dependencias contiene referencias a muchos componentes en su aplicación. Pasar este chico malo a cada componente que necesita crear / administrar un subcomponente anoter es el equivalente a usar elementos globales en su arquitectura. Peor aún, cualquier subcomponente también puede registrar nuevos componentes en el contenedor, tan pronto como sea posible se convertirá en un almacenamiento global. Los desarrolladores lanzarán objetos al contenedor solo para pasar los datos entre los componentes (ya sea entre los controladores hermanos o entre las jerarquías de los controladores profundos, es decir, un controlador ancestro necesita tomar datos de un controlador de abuelos). Tenga en cuenta que en el mundo web donde el contenedor no se transfiere al código de usuario, esto nunca es un problema.
2) El otro problema con los contenedores de dependencia frente a los localizadores / fábricas de servicios / instanciación directa de objetos es que la resolución desde un contenedor hace que sea completamente ambiguo si está CREANDO un componente o simplemente REUSANDO uno existente. En su lugar, se deja a una configuración centralizada (es decir, bootstrapper / Composition Root) para averiguar cuál es la duración del componente. En ciertos casos, esto está bien (es decir, controladores web, donde no es el código de usuario el que necesita administrar la duración del componente, sino el propio marco de procesamiento de solicitud de tiempo de ejecución). Sin embargo, esto es extremadamente problemático cuando el diseño de los componentes debe INDICAR si es su responsabilidad administrar un componente y cuál debería ser su duración (Ejemplo: una aplicación de teléfono muestra una hoja que le pide al usuario cierta información. Esto se logra mediante un controlador que crea un subcontrolador que gobierna la hoja superpuesta. Una vez que el usuario ingresa cierta información, la hoja se renueva y el control se devuelve al controlador inicial, que aún mantiene el estado de lo que el usuario estaba haciendo previamente). Si DI se usa para resolver el subcontrolador de hojas, es ambiguo qué duración debería tener o quién debería ser el responsable de gestionarlo (el controlador iniciador). Compare esto con la responsabilidad explícita dictada por el uso de otros mecanismos.
Escenario A:
// not sure whether I''m responsible for creating the thing or not
DependencyContainer.GimmeA<Thing>()
Escenario B:
// responsibility is clear that this component is responsible for creation
Factory.CreateMeA<Thing>()
// or simply
new Thing()
Escenario C:
// responsibility is clear that this component is not responsible for creation, but rather only consumption
ServiceLocator.GetMeTheExisting<Thing>()
// or simply
ServiceLocator.Thing
Como puede ver, DI no aclara quién es el responsable de la gestión de vida del subcomponente.
NOTA: Técnicamente hablando, muchos marcos DI tienen alguna forma de crear componentes de forma perezosa (Consulte: Cómo no hacer la inyección de dependencia, el contenedor estático o singleton ) que es mucho mejor que pasar el contenedor, pero aún está pagando el costo de mutando su código para pasar las funciones de creación a todas partes, le falta soporte de primer nivel para pasar parámetros de constructor válidos durante la creación, y al final del día todavía está usando un mecanismo indirecto innecesariamente en lugares donde el único beneficio es lograr la capacidad de prueba , que se puede lograr de una manera mejor y más simple (ver abajo).
¿Qué significa todo esto?
Significa que DI es apropiado para ciertos escenarios e inapropiado para otros. En las aplicaciones de clientes ricos, sucede que tiene muchas de las desventajas de la DI con muy pocas ventajas. Mientras más grande sea su complejidad en la aplicación, mayores serán los costos de mantenimiento. También conlleva el grave potencial de uso indebido, que de acuerdo con cuán ajustados sean los procesos de comunicación de su equipo y de revisión de códigos, puede ser cualquier cosa, desde un costo de emisión de la tecnología no grave hasta un costo severo. Existe el mito de que los Localizadores o Fábricas de Servicios o las buenas instancias antiguas son de alguna manera mecanismos malos y desactualizados simplemente porque pueden no ser el mecanismo óptimo en el mundo de las aplicaciones web, donde tal vez mucha gente juegue. No deberíamos exagerar. generalice estos aprendizajes a todos los escenarios y vea todo como uñas solo porque hemos aprendido a manejar un martillo en particular.
Mi recomendación PARA LAS APLICACIONES DE CLIENTE RICO es usar el mecanismo mínimo que cumpla con los requisitos para cada componente a mano. El 80% del tiempo esto debería ser una instanciación directa. Los localizadores de servicios se pueden usar para alojar los principales componentes de su capa empresarial (es decir, servicios de aplicaciones que generalmente son de naturaleza singleton) y, por supuesto, las fábricas e incluso el patrón Singleton también tienen su lugar. No hay nada que decir que no se puede usar un marco DI oculto detrás de su localizador de servicios para crear las dependencias de la capa de negocios y todo de lo que dependen de una vez, si eso hace la vida más fácil en esa capa, y esa capa no lo hace t exhibe la carga diferida que abrumadoramente hacen las capas de presentación de cliente rico . Solo asegúrate de proteger tu código de usuario del acceso a ese contenedor para que puedas evitar el desorden que puede ocasionar al pasar un contenedor DI.
¿Qué hay de la capacidad de prueba?
La capacidad de prueba se puede lograr absolutamente sin un marco DI. Recomiendo usar un marco de intercepción como UnitBox (gratis) o TypeMock (caro). Estos frameworks le brindan las herramientas que necesita para resolver el problema (cómo se burla de la creación de instancias y las llamadas estáticas en C #) y no le piden que cambie toda su arquitectura para evitarlos (que desafortunadamente es donde la tendencia ha ido en el mundo .NET / Java). Es más sensato encontrar una solución al problema en cuestión y utilizar los mecanismos y patrones de lenguaje natural óptimos para el componente subyacente, luego intentar ajustar cada clavija cuadrada en el agujero DI redondo. Una vez que empiece a utilizar estos mecanismos más simples y más específicos, notará que hay muy poca necesidad de DI en su base de códigos, si es que hay alguno.
NOTA: Para arquitecturas MVVM
En las arquitecturas MVVM básicas, los modelos de vista asumen la responsabilidad de los controladores, por lo tanto, para todos los propósitos, considere la redacción del ''controlador'' anterior para aplicarla a ''ver-modelo''. MVVM básico funciona bien para aplicaciones pequeñas, pero a medida que crece la complejidad de una aplicación es posible que desee utilizar un enfoque MVCVM. Los modelos de vista se convierten en DTO principalmente tontos para facilitar el enlace de datos a la vista mientras que la interacción con la capa empresarial y entre grupos de modelos de visualización que representan pantallas / subpantallas se encapsula en componentes de controlador / subcontrolador explícitos. En cualquier arquitectura, la responsabilidad de los controladores existe y exhibe las mismas características discutidas anteriormente.