Biblioteca "amigable" de Dependency Inject (DI)

230

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?

Pete
fuente
La nueva referencia al artículo de enlace roto (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Respuestas:

360

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:

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 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 readonlypalabra 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:

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 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 .

Mark Seemann
fuente
21
Si bien esto suena muy bien en el papel, en mi experiencia, una vez que tiene muchos componentes internos que interactúan de manera compleja, termina con muchas fábricas para administrar, lo que dificulta el mantenimiento. Además, las fábricas tendrán que administrar los estilos de vida de sus componentes creados, una vez que instale la biblioteca en un contenedor real, esto entrará en conflicto con la gestión del estilo de vida del contenedor. Las fábricas y fachadas se interponen en el camino de los contenedores reales.
Mauricio Scheffer el
44
Todavía tengo que encontrar un proyecto que haga todo esto.
Mauricio Scheffer el
31
Bueno, así es como desarrollamos software en Safewhere, por lo que no compartimos su experiencia ...
Mark Seemann
19
Creo que las fachadas deberían estar codificadas a mano porque representan combinaciones conocidas (y presumiblemente comunes) de componentes. Un contenedor DI no debería ser necesario porque todo se puede conectar a mano (piense en el DI de Poor Man). Recuerde que una Fachada es solo una clase de conveniencia opcional para los usuarios de su API. Los usuarios avanzados aún pueden querer evitar la Fachada y conectar los componentes a su gusto. Es posible que quieran usar su propio DI Contaier para esto, por lo que creo que sería desagradable forzar un contenedor DI particular sobre ellos si no lo van a usar. Posible pero no aconsejable
Mark Seemann
8
Esta puede ser la mejor respuesta que he visto en SO.
Nick Hodges
40

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í:

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:

  1. Confíe en las interfaces en lugar de las clases cada vez que piense que la implementación podría necesitar un cambio;

  2. 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>implementos IList<T>. Si diseña su clase para usar IList<T>(o IEnumerable<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 un IList<T>argumento como constructor, como, por ejemplo BindingList<T>, que se utiliza para varias características de enlace de datos.

  • Linq to SQL y EF están construidos completamente alrededor de las IDbConnectioninterfaces 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 INameCreationServiceo IExtenderProviderService. Ni siquiera sabe lo que realmente lo que las clases concretas son . .NET en realidad tiene su propio contenedor de IoC IContainer, que se usa para esto, y la Componentclase tiene un GetServicemétodo que es el localizador de servicio real. Por supuesto, nada le impide usar ninguna o todas estas interfaces sin el IContainero 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 IServiceBehaviorque 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. IContaineres una gran opción para esto porque es parte de .NET Framework en sí.

Aaronaught
fuente
2
La abstracción real para un contenedor es IServiceProvider, no IContainer.
Mauricio Scheffer el
2
@Mauricio: Tienes razón, por supuesto, pero trata de explicarle a alguien que nunca ha usado el sistema LWC por qué en IContainerrealidad no es el contenedor en un párrafo. ;)
Aaronaught
Aaron, solo curiosidad por saber por qué usas un set privado; en lugar de solo especificar los campos como de solo lectura?
jr3
@Jreeter: ¿Quieres decir por qué no son private readonlycampos? 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 un private readonlycampo 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.
Aaronaught
¡Gracias por la aclaración! Supuse que los campos de solo lectura serían accesibles para el niño.
jr3
5

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 newmejorar 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.

Mauricio Scheffer
fuente
12
Si bien respeto su opinión, considero que su generalización general "todas las demás respuestas aquí están desactualizadas y deben evitarse" extremadamente ingenuas. Si aprendimos algo desde entonces, es que la inyección de dependencia es una respuesta a las limitaciones del lenguaje. Sin evolucionar o abandonar nuestros idiomas actuales, parece que necesitaremos DI para aplanar nuestras arquitecturas y lograr la modularidad. IoC como concepto general es solo una consecuencia de estos objetivos y definitivamente uno deseable. Los contenedores de IoC no son absolutamente necesarios para IoC o DI, y su utilidad depende de las composiciones de módulos que hagamos.
TNE
@tne Tu mención de que soy "extremadamente ingenua" es un ad-hominem no bienvenido. He escrito aplicaciones complejas sin DI o IoC en C #, VB.NET, Java (lenguajes que pensarías que "necesitan" IoC aparentemente a juzgar por tu descripción), definitivamente no se trata de limitaciones de idiomas. Se trata de limitaciones conceptuales. Los invito a aprender sobre programación funcional en lugar de recurrir a ataques ad-hominem y argumentos pobres.
Mauricio Scheffer
18
Mencioné que tu declaración me pareció ingenua; esto no dice nada de ti personalmente y se trata de mi opinión. Por favor, no te sientas insultado por un extraño al azar. Implicar que soy ignorante de todos los enfoques de programación funcional relevantes tampoco es útil; no lo sabes y estarías equivocado. Sabía que estabas bien informado allí (rápidamente miraste tu blog), por eso me pareció molesto que un chico aparentemente conocedor dijera cosas como "no necesitamos IoC". COI pasa literalmente por todas partes en la programación funcional (..)
TNE
44
y en software de alto nivel en general. De hecho, los lenguajes funcionales (y muchos otros estilos) demuestran que resuelven IoC sin DI, creo que ambos estamos de acuerdo con eso, y eso es todo lo que quería decir. Si se las arregló sin DI en los idiomas que menciona, ¿cómo lo hizo? Supongo que no mediante el uso 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.)
TNE
1
Los contenedores IoC son diccionarios globales mutables, que eliminan la parametricidad como una herramienta de razonamiento, por lo tanto, deben evitarse. IoC (el concepto) como en "no nos llame, lo llamaremos" se aplica técnicamente cada vez que pasa una función como valor. En lugar de DI, modele su dominio con ADT y luego escriba intérpretes (cf Gratis).
Mauricio Scheffer
1

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.

Igor Zevaka
fuente