Estoy reflexionando sobre el diseño de una biblioteca de C #, que tendrá varias funciones diferentes de alto nivel. Por supuesto, esas funciones de alto nivel se implementarán utilizando los principios de diseño de clase SOLID tanto como sea posible. Como tal, probablemente habrá clases destinadas a que los consumidores las usen directamente de forma regular y "clases de soporte" que dependen de las clases más comunes de "usuario final".
La pregunta es, ¿cuál es la mejor manera de diseñar la biblioteca para que sea:
- DI Agnóstico: aunque parece razonable agregar "soporte" básico para una o dos de las bibliotecas DI comunes (StructureMap, Ninject, etc.), quiero que los consumidores puedan usar la biblioteca con cualquier marco DI.
- No utilizable por DI: si un consumidor de la biblioteca no utiliza DI, la biblioteca debería ser tan fácil de usar como sea posible, reduciendo la cantidad de trabajo que un usuario tiene que hacer para crear todas estas dependencias "sin importancia" solo para llegar a las clases "reales" que quieren usar.
Mi pensamiento actual es proporcionar algunos "módulos de registro DI" para las bibliotecas DI comunes (por ejemplo, un registro de StructureMap, un módulo Ninject) y un conjunto o clases Factory que no son DI y contienen el acoplamiento a esas pocas fábricas.
Pensamientos?
Respuestas:
Esto es realmente simple de hacer una vez que entiendes que la DI se trata de patrones y principios , no de tecnología.
Para diseñar la API de forma independiente del contenedor DI, siga estos principios generales:
Programa para una interfaz, no una implementación
Este principio es en realidad una cita (de memoria) de Patrones de diseño , pero siempre debe ser su objetivo real . DI es solo un medio para lograr ese fin .
Aplicar el principio de Hollywood
El Principio de Hollywood en términos de DI dice: No llame al contenedor DI, lo llamará a usted .
Nunca solicite directamente una dependencia llamando a un contenedor desde su código. Solicítelo implícitamente usando la inyección de constructor .
Usar inyección de constructor
Cuando necesite una dependencia, solicítela estáticamente a través del constructor:
Observe cómo la clase de Servicio garantiza sus invariantes. Una vez que se crea una instancia, se garantiza que la dependencia estará disponible debido a la combinación de la cláusula de protección y la
readonly
palabra clave.Use Abstract Factory si necesita un objeto de vida corta
Las dependencias inyectadas con la inyección de constructor tienden a ser de larga duración, pero a veces se necesita un objeto de corta duración, o para construir la dependencia en función de un valor conocido solo en tiempo de ejecución.
Vea esto para más información.
Componer solo en el último momento responsable
Mantenga los objetos desacoplados hasta el final. Normalmente, puede esperar y conectar todo en el punto de entrada de la aplicación. Esto se llama la raíz de composición .
Más detalles aquí:
Simplifica usando una Fachada
Si cree que la API resultante se vuelve demasiado compleja para usuarios novatos, siempre puede proporcionar algunas clases de Fachada que encapsulan combinaciones de dependencia comunes.
Para proporcionar una fachada flexible con un alto grado de visibilidad, podría considerar proporcionar constructores con fluidez. Algo como esto:
Esto permitiría a un usuario crear un Foo predeterminado escribiendo
Sin embargo, sería muy reconocible que sea posible proporcionar una dependencia personalizada, y podría escribir
Si imagina que la clase MyFacade encapsula muchas dependencias diferentes, espero que quede claro cómo proporcionaría los valores predeterminados adecuados y al mismo tiempo haría que la extensibilidad sea reconocible.
FWIW, mucho después de escribir esta respuesta, amplié los conceptos en este documento y escribí una publicación de blog más larga sobre Bibliotecas DI-Friendly , y una publicación complementaria sobre Frameworks DI-Friendly .
fuente
El término "inyección de dependencia" no tiene nada que ver específicamente con un contenedor de IoC, a pesar de que tiende a verlos mencionados juntos. Simplemente significa que en lugar de escribir su código así:
Lo escribes así:
Es decir, haces dos cosas cuando escribes tu código:
Confíe en las interfaces en lugar de las clases cada vez que piense que la implementación podría necesitar un cambio;
En lugar de crear instancias de estas interfaces dentro de una clase, pasarlas como argumentos de constructor (alternativamente, podrían asignarse a propiedades públicas; la primera es inyección de constructor , la segunda es inyección de propiedad ).
Nada de esto presupone la existencia de una biblioteca DI, y realmente no hace que el código sea más difícil de escribir sin una.
Si está buscando un ejemplo de esto, no busque más .NET Framework en sí:
List<T>
implementosIList<T>
. Si diseña su clase para usarIList<T>
(oIEnumerable<T>
), puede aprovechar conceptos como la carga diferida, como lo hacen Linq to SQL, Linq to Entities y NHibernate, por lo general a través de la inyección de propiedades. Algunas clases de framework realmente aceptan unIList<T>
argumento como constructor, como, por ejemploBindingList<T>
, que se utiliza para varias características de enlace de datos.Linq to SQL y EF están construidos completamente alrededor de las
IDbConnection
interfaces relacionadas, que pueden pasarse a través de los constructores públicos. Sin embargo, no es necesario usarlos; los constructores predeterminados funcionan bien con una cadena de conexión ubicada en un archivo de configuración en alguna parte.Si alguna vez trabaja en componentes WinForms, se ocupa de "servicios", como
INameCreationService
oIExtenderProviderService
. Ni siquiera sabe lo que realmente lo que las clases concretas son . .NET en realidad tiene su propio contenedor de IoCIContainer
, que se usa para esto, y laComponent
clase tiene unGetService
método que es el localizador de servicio real. Por supuesto, nada le impide usar ninguna o todas estas interfaces sin elIContainer
o ese localizador en particular. Los servicios en sí solo se acoplan libremente con el contenedor.Los contratos en WCF se construyen completamente alrededor de interfaces. La clase de servicio concreta real generalmente se hace referencia por nombre en un archivo de configuración, que es esencialmente DI. Muchas personas no se dan cuenta de esto, pero es completamente posible cambiar este sistema de configuración con otro contenedor de IoC. Quizás lo más interesante es que los comportamientos del servicio son todas instancias
IServiceBehavior
que se pueden agregar más adelante. Una vez más, podría conectarlo fácilmente a un contenedor de IoC y hacer que elija los comportamientos relevantes, pero la característica es completamente utilizable sin uno.Y así sucesivamente y así sucesivamente. Encontrarás DI por todas partes en .NET, es solo que normalmente se hace de manera tan fluida que ni siquiera lo piensas como DI.
Si desea diseñar su biblioteca habilitada para DI para una usabilidad máxima, entonces la mejor sugerencia es probablemente proporcionar su propia implementación de IoC predeterminada utilizando un contenedor liviano.
IContainer
es una gran opción para esto porque es parte de .NET Framework en sí.fuente
IContainer
realidad no es el contenedor en un párrafo. ;)private readonly
campos? Eso es genial si solo los usa la clase de declaración pero el OP especificó que se trataba de código de nivel de marco / biblioteca, lo que implica subclases. En tales casos, normalmente desea dependencias importantes expuestas a subclases. Podría haber escrito unprivate readonly
campo y una propiedad con getters / setters explícitos, pero ... desperdicié espacio en un ejemplo, y más código para mantener en la práctica, sin beneficio real.EDITAR 2015 : ha pasado el tiempo, ahora me doy cuenta de que todo esto fue un gran error. Los contenedores de IoC son terribles y la DI es una forma muy pobre de lidiar con los efectos secundarios. Efectivamente, todas las respuestas aquí (y la pregunta en sí) se deben evitar. Simplemente tenga en cuenta los efectos secundarios, sepárelos del código puro, y todo lo demás encajará o será irrelevante e innecesaria.
La respuesta original sigue:
Tuve que enfrentar esta misma decisión mientras desarrollaba SolrNet . Comencé con el objetivo de ser amigable con los DI y agnóstico al contenedor, pero a medida que agregué más y más componentes internos, las fábricas internas rápidamente se volvieron inmanejables y la biblioteca resultante fue inflexible.
Terminé escribiendo mi propio contenedor de IoC incrustado muy simple al tiempo que proporcionaba una instalación de Windsor y un módulo Ninject . Integrar la biblioteca con otros contenedores es solo una cuestión de conectar correctamente los componentes, por lo que podría integrarlo fácilmente con Autofac, Unity, StructureMap, lo que sea.
La desventaja de esto es que perdí la capacidad de
new
mejorar el servicio. También tomé una dependencia en CommonServiceLocator que podría haber evitado (podría refactorizarlo en el futuro) para hacer que el contenedor incrustado sea más fácil de implementar.Más detalles en esta publicación de blog .
Mass Transit parece confiar en algo similar. Tiene una interfaz IObjectBuilder que es realmente IServiceLocator de CommonServiceLocator con un par de métodos más, luego implementa esto para cada contenedor, es decir, NinjectObjectBuilder y un módulo / instalación regular, es decir, Mass transitModule . Luego se basa en IObjectBuilder para crear instancias de lo que necesita. Este es un enfoque válido, por supuesto, pero personalmente no me gusta mucho ya que en realidad está pasando demasiado el contenedor, utilizándolo como un localizador de servicios.
MonoRail también implementa su propio contenedor , que implementa el antiguo IServiceProvider . Este contenedor se utiliza en todo este marco a través de una interfaz que expone servicios conocidos . Para obtener el contenedor de concreto, tiene un localizador de proveedores de servicios incorporado . Las instalaciones de Windsor apuntan este localizador de proveedores de servicios a Windsor, convirtiéndolo en el proveedor de servicios seleccionado.
En pocas palabras: no hay una solución perfecta. Como con cualquier decisión de diseño, este problema exige un equilibrio entre flexibilidad, facilidad de mantenimiento y conveniencia.
fuente
ServiceLocator
. Si aplica técnicas funcionales, termina con el equivalente de cierres, que son clases inyectadas por dependencia (donde las dependencias son las variables cerradas). ¿De que otra forma? (Estoy realmente curioso, porque no me gusta DI tampoco.)Lo que haría sería diseñar mi biblioteca en una forma agnóstica de contenedor DI para limitar la dependencia del contenedor tanto como sea posible. Esto permite cambiar el contenedor DI por otro si es necesario.
Luego, exponga la capa sobre la lógica DI a los usuarios de la biblioteca para que puedan usar el marco que elija a través de su interfaz. De esta manera, aún pueden usar la funcionalidad DI que usted expuso y son libres de usar cualquier otro marco para sus propios fines.
Permitir que los usuarios de la biblioteca conecten su propio marco DI me parece un poco incorrecto, ya que aumenta drásticamente la cantidad de mantenimiento. Esto también se convierte más en un entorno de complemento que DI directo.
fuente