Biblioteca "amigable" de Inyección de dependencia (DI)
Estoy pensando en el diseño de una biblioteca 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 clases SOLID tanto como sea posible. Como tal, probablemente habrá clases destinadas a que los consumidores las utilicen directamente de forma regular y "clases de soporte" que sean dependencias de aquellas clases de "usuario final" más comunes.
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.
- Utilizable sin DI: si un consumidor de la biblioteca no utiliza DI, la biblioteca aún debería ser lo más fácil de usar 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 idea actual es proporcionar algunos "módulos de registro DI" para las bibliotecas DI comunes (por ejemplo, un registro StructureMap, un módulo Ninject) y un conjunto de clases Factory que no sean DI y contengan el acoplamiento a esas pocas fábricas.
¿Pensamientos?
En realidad, esto es sencillo de hacer una vez que se comprende 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 a una interfaz, no a una implementación.
Este principio es en realidad una cita (aunque de memoria) de Design Patterns , pero siempre debe ser su verdadero objetivo . DI es sólo un medio para lograr ese fin .
Aplicar el principio de Hollywood
El Principio de Hollywood en términos DI dice: No llames al Contenedor DI, él te llamará a ti .
Nunca solicite directamente una dependencia llamando a un contenedor desde su código. Pídelo implícitamente usando Constructor Inyección .
Usar inyección de constructor
Cuando necesites una dependencia, solicítala estáticamente a través del constructor:
public class Service : IService
{
private readonly ISomeDependency dep;
public Service(ISomeDependency dep)
{
if (dep == null)
{
throw new ArgumentNullException("dep");
}
this.dep = dep;
}
public ISomeDependency Dependency
{
get { return this.dep; }
}
}
Observe cómo la clase 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.
Utilice Abstract Factory si necesita un objeto de corta duración
Las dependencias inyectadas con Constructor Injection tienden a ser de larga duración, pero a veces se necesita un objeto de corta duración o construir la dependencia en función de un valor conocido sólo en tiempo de ejecución.
Vea esto para más información.
Redactar sólo en el último momento responsable
Mantenga los objetos desacoplados hasta el final. Normalmente, puedes esperar y conectar todo en el punto de entrada de la aplicación. Esto se llama raíz de composición .
Más detalles aquí:
- ¿Dónde debo realizar la inyección con Ninject 2+ (y cómo organizo mis módulos?)
- Diseño: ¿Dónde se deben registrar los objetos cuando se utiliza Windsor?
Simplifica usando una fachada
Si cree que la API resultante se vuelve demasiado compleja para los usuarios novatos, siempre puede proporcionar algunas clases de Facade que encapsulan combinaciones de dependencias comunes.
Para proporcionar una fachada flexible con un alto grado de capacidad de descubrimiento, podría considerar proporcionar Fluent Builders. Algo como esto:
public class MyFacade
{
private IMyDependency dep;
public MyFacade()
{
this.dep = new DefaultDependency();
}
public MyFacade WithDependency(IMyDependency dependency)
{
this.dep = dependency;
return this;
}
public Foo CreateFoo()
{
return new Foo(this.dep);
}
}
Esto permitiría a un usuario crear un Foo predeterminado escribiendo
var foo = new MyFacade().CreateFoo();
Sin embargo, sería muy reconocible que sea posible proporcionar una dependencia personalizada y podría escribir
var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();
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 fuera reconocible.
FWIW, mucho después de escribir esta respuesta, amplié los conceptos aquí contenidos y escribí una publicación de blog más larga sobre Bibliotecas compatibles con DI y una publicación complementaria sobre Marcos compatibles con DI .
El término "inyección de dependencia" no tiene nada que ver específicamente con un contenedor de IoC, aunque tiende a verlos mencionados juntos. Simplemente significa que en lugar de escribir tu código así:
public class Service
{
public Service()
{
}
public void DoSomething()
{
SqlConnection connection = new SqlConnection("some connection string");
WindowsIdentity identity = WindowsIdentity.GetCurrent();
// Do something with connection and identity variables
}
}
Lo escribes así:
public class Service
{
public Service(IDbConnection connection, IIdentity identity)
{
this.Connection = connection;
this.Identity = identity;
}
public void DoSomething()
{
// Do something with Connection and Identity properties
}
protected IDbConnection Connection { get; private set; }
protected IIdentity Identity { get; private set; }
}
Es decir, haces dos cosas cuando escribes tu código:
Confíe en las interfaces en lugar de las clases siempre que crea que es posible que sea necesario cambiar la implementación;
En lugar de crear instancias de estas interfaces dentro de una clase, páselas como argumentos del 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 ninguna 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 que el propio .NET Framework:
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 detrás de escena, generalmente mediante inyección de propiedades. Algunas clases de marco en realidad aceptanIList<T>
como argumento constructor, comoBindingList<T>
, que se utiliza para varias funciones de enlace de datos.Linq to SQL y EF se basan completamente en las
IDbConnection
interfaces relacionadas, que se pueden pasar a través de constructores públicos. Sin embargo, no es necesario que los utilices; Los constructores predeterminados funcionan bien con una cadena de conexión ubicada en un archivo de configuración en algún lugar.Si alguna vez trabaja con componentes de WinForms, se ocupa de "servicios", como
INameCreationService
oIExtenderProviderService
. Ni siquiera sabes realmente cuáles son las clases concretas . En realidad, .NET tiene su propio contenedor IoC,IContainer
que se utiliza para esto, y laComponent
clase tiene unGetService
método que es el localizador de servicios real. Por supuesto, nada le impide utilizar cualquiera o todas estas interfaces sin elIContainer
localizador en particular. Los servicios en sí están débilmente acoplados al contenedor.Los contratos en WCF se crean completamente en torno a interfaces. Generalmente se hace referencia a la clase de servicio concreta real por su nombre en un archivo de configuración, que es esencialmente DI. Mucha gente no se da cuenta de esto, pero es completamente posible cambiar este sistema de configuración por otro contenedor de IoC. Quizás lo más interesante es que todos los comportamientos del servicio son ejemplos de
IServiceBehavior
los cuales se pueden agregar más adelante. Nuevamente, puede conectar esto 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á DI en todas partes en .NET, solo que normalmente se hace de manera tan fluida que ni siquiera lo considera DI.
Si desea diseñar su biblioteca habilitada para DI para una máxima usabilidad, entonces la mejor sugerencia probablemente sea proporcionar su propia implementación de IoC predeterminada utilizando un contenedor liviano. IContainer
es una excelente opción para esto porque es parte del propio .NET Framework.
EDITAR 2015 : ha pasado el tiempo, ahora me doy cuenta de que todo esto fue un gran error. Los contenedores de COI son terribles y la DI es una forma muy pobre de lidiar con los efectos secundarios. Efectivamente, deben evitarse todas las respuestas aquí (y la pregunta misma). Simplemente tenga en cuenta los efectos secundarios, sepárelos del código puro y todo lo demás encajará o será irrelevante y de complejidad innecesaria.
La respuesta original es la siguiente:
Tuve que enfrentar esta misma decisión mientras desarrollaba SolrNet . Comencé con el objetivo de ser compatible con DI e independiente de los contenedores, pero a medida que agregaba más y más componentes internos, las fábricas internas rápidamente se volvieron inmanejables y la biblioteca resultante era inflexible.
Terminé escribiendo mi propio contenedor IoC integrado muy simple y al mismo tiempo proporcioné una instalación Windsor y un módulo Ninject . Integrar la biblioteca con otros contenedores es solo una cuestión de cablear adecuadamente los componentes, por lo que podría integrarla fácilmente con Autofac, Unity, StructureMap, lo que sea.
La desventaja de esto es que perdí la capacidad de simplemente new
mejorar el servicio. También tomé una dependencia de CommonServiceLocator que podría haber evitado (podría refactorizarlo en el futuro) para hacer que el contenedor integrado sea más fácil de implementar.
Más detalles en esta publicación de blog .
MassTransit parece depender de algo similar. Tiene una interfaz IObjectBuilder que en realidad es el 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 normal, es decir, MassTransitModule . Luego confía 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 pasa demasiado por el contenedor, usándolo como 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 conseguir el contenedor de hormigón, lleva incorporado un localizador de proveedores de servicios . Las instalaciones de Windsor dirigen este localizador de proveedores de servicios a Windsor, convirtiéndolo en el proveedor de servicios seleccionado.
En pocas palabras: no existe una solución perfecta. Como ocurre con cualquier decisión de diseño, esta cuestión exige un equilibrio entre flexibilidad, mantenibilidad y conveniencia.
Lo que haría es diseñar mi biblioteca de forma independiente del 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 cualquier 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 en una DI directa.