Recibo una inyección de dependencia, pero ¿alguien puede ayudarme a comprender la necesidad de un contenedor de IoC?

15

Pido disculpas si esto parece una repetición más de la pregunta, pero cada vez que encuentro un artículo sobre el tema, en su mayoría solo habla sobre qué es DI. Entonces, recibo DI, pero estoy tratando de entender la necesidad de un contenedor de IoC, en el que todos parecen estar metiéndose. ¿El objetivo de un contenedor de IoC es realmente "resolver automáticamente" la implementación concreta de las dependencias? Tal vez mis clases tienden a no tener varias dependencias y tal vez es por eso que no veo el gran problema, pero quiero asegurarme de que entiendo la utilidad del contenedor correctamente.

Normalmente rompo mi lógica de negocios en una clase que podría verse así:

public class SomeBusinessOperation
{
    private readonly IDataRepository _repository;

    public SomeBusinessOperation(IDataRespository repository = null)
    {
        _repository = repository ?? new ConcreteRepository();
    }

    public SomeType Run(SomeRequestType request)
    {
        // do work...
        var results = _repository.GetThings(request);

        return results;
    }
}

Por lo tanto, solo tiene una dependencia, y en algunos casos puede tener una segunda o tercera, pero no con tanta frecuencia. Entonces, cualquier cosa que llame a esto puede pasar su propio repositorio o permitirle usar el repositorio predeterminado.

En lo que respecta a mi comprensión actual de un contenedor de IoC, todo lo que hace el contenedor es resolver IDataRepository. Pero si eso es todo lo que hace, entonces no estoy viendo un montón de valor ya que mis clases operativas ya definen un retroceso cuando no pasó ninguna dependencia. Entonces, el único otro beneficio que puedo pensar es que si tengo varias operaciones como esto usa el mismo repositorio alternativo, puedo cambiar ese repositorio en un lugar que es el registro / fábrica / contenedor. Y eso es genial, pero ¿es eso?

Sinestésico
fuente
1
A menudo, tener una versión alternativa alternativa de la dependencia realmente no tiene sentido.
Ben Aaronson
¿A qué te refieres? El "respaldo" es la clase concreta que se usa casi todo el tiempo, excepto en pruebas unitarias. En efecto, esta sería la misma clase registrada en el contenedor.
Sinaesthetic
Sí, pero con el contenedor: (1) todos los demás objetos en el contenedor obtienen la misma instancia ConcreteRepositoryy (2) puede proporcionar dependencias adicionales a ConcreteRepository(una conexión de base de datos sería común, por ejemplo).
Jules
@Sinaesthetic No quiero decir que siempre sea una mala idea, pero a menudo no es apropiado. Por ejemplo, le impediría seguir la arquitectura de cebolla con las referencias de su proyecto. Además, puede que no haya una implementación clara por defecto. Y como dice Jules, los contenedores de COI no solo seleccionan el tipo de dependencia, sino que también hacen cosas como compartir instancias y gestión del ciclo de vida
Ben Aaronson
Voy a hacer una camiseta que dice "Parámetros de función - ¡La inyección de dependencia ORIGINAL!"
Graham

Respuestas:

2

El contenedor IoC no se trata del caso en el que tiene una dependencia. Se trata del caso en el que tienes 3 dependencias y ellas tienen varias dependencias que tienen dependencias, etc.

También le ayuda a centralizar la resolución de una dependencia y la gestión del ciclo de vida de las dependencias.

Firmar
fuente
10

Hay varias razones por las que es posible que desee utilizar un contenedor de IoC.

Dlls sin referencia

Puede usar un contenedor de IoC para resolver una clase concreta a partir de un dll sin referencia. Esto significa que puede tomar dependencias completamente de la abstracción, es decir, la interfaz.

Evitar el uso de new

Un contenedor de IoC significa que puede eliminar completamente el uso de la newpalabra clave para crear una clase. Esto tiene dos efectos. La primera es que desacopla tus clases. El segundo (que está relacionado) es que puede colocar simulacros para pruebas unitarias. Esto es increíblemente útil, especialmente cuando está interactuando con un proceso de larga ejecución.

Escribe contra las abstracciones

El uso de un contenedor de IoC para resolver sus dependencias concretas por usted le permite escribir su código contra abstracciones, en lugar de implementar cada clase concreta que necesite según lo necesite. Por ejemplo, es posible que necesite su código para leer datos de una base de datos. En lugar de escribir la clase de interacción de la base de datos, simplemente escribe una interfaz para ella y codifica contra eso. Puede usar un simulacro para probar la funcionalidad del código que está desarrollando a medida que lo desarrolla, en lugar de confiar en el desarrollo de la clase de interacción de base de datos concreta antes de poder probar el otro código.

Evitar código frágil

Otra razón para usar un contenedor de IoC es que al confiar en el contenedor de IoC para resolver sus dependencias, evita la necesidad de cambiar cada llamada a un constructor de clases cuando agrega o elimina una dependencia. El contenedor IoC resolverá automáticamente sus dependencias. Este no es un gran problema cuando creas una clase una vez, pero es un problema gigantesco cuando creas la clase en cien lugares.

Administración de por vida y limpieza de recursos no administrada

La razón final que mencionaré es la gestión de la vida útil de los objetos. Los contenedores de IoC a menudo brindan la capacidad de especificar la vida útil de un objeto. Tiene mucho sentido especificar la vida útil de un objeto en un contenedor de IoC en lugar de tratar de administrarlo manualmente en el código. La gestión manual de por vida puede ser muy difícil. Esto puede ser útil cuando se trata de objetos que requieren eliminación. En lugar de administrar manualmente la eliminación de sus objetos, algunos contenedores de IoC administrarán la eliminación por usted, lo que puede ayudar a evitar pérdidas de memoria y simplificar su base de código.

El problema con el código de muestra que ha proporcionado es que la clase que está escribiendo tiene una dependencia concreta de la clase ConcreteRepository. Un contenedor de IoC eliminaría esa dependencia.

Stephen
fuente
22
Estas no son ventajas de los contenedores de IoC, son ventajas de la inyección de dependencia, que se puede hacer fácilmente con la DI del pobre
Ben Aaronson
Escribir un buen código DI sin un contenedor IoC puede ser bastante difícil. Sí, hay una cierta superposición en las ventajas, pero todas estas ventajas se aprovechan mejor con un contenedor de IoC.
Stephen
Bueno, las dos últimas razones que agregaste desde mi comentario son más específicas del contenedor y son argumentos muy fuertes en mi opinión
Ben Aaronson,
"Evitar el uso de nuevos": también dificulta el análisis del código estático, por lo que debe comenzar a usar algo como esto: hmemcpy.github.io/AgentMulder . Otros beneficios que describe en este párrafo tienen que ver con DI y no con IoC. Además, sus clases seguirán estando acopladas si evita utilizar nuevas pero utilizarán tipos concretos en lugar de interfaces para parámetros.
Den
1
En general, pinta IoC como algo sin fallas, por ejemplo, no se menciona el gran inconveniente: empujar una clase de errores al tiempo de ejecución en lugar del tiempo de compilación.
Den
2

Según el principio de responsabilidad única, cada clase debe tener una sola responsabilidad. Crear nuevas instancias de clases es solo otra responsabilidad, por lo que debe encapsular este tipo de código en una o más clases. Puede hacerlo utilizando cualquier patrón de creación, por ejemplo, fábricas, constructores, contenedores DI, etc.

Hay otros principios como la inversión de control y la inversión de dependencia. En este contexto, están relacionados con la instanciación de dependencias. Afirman que las clases de alto nivel deben estar desacopladas de las clases de bajo nivel (dependencias) que usan. Podemos desacoplar cosas creando interfaces. Por lo tanto, las clases de bajo nivel tienen que implementar interfaces específicas y las clases de alto nivel tienen que utilizar instancias de clases que implementan estas interfaces. (nota: la restricción de interfaz uniforme REST aplica el mismo enfoque a nivel de sistema).

La combinación de estos principios por ejemplo (perdón por el código de baja calidad, usé un lenguaje ad-hoc en lugar de C #, ya que no lo sé):

  1. Sin SRP, sin IoC

    class SomeHighLevelService
    {
        public doFooBar(){
            Crap crap = doFoo();
            doBar(crap);
        }
    
        public Crap doFoo(){
            //...
            return crap;
        }
    
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  2. Más cerca de SRP, sin IoC

    class SomeHighLevelService
    {
        public SomeHighLevelService(){
            Foo foo = new Foo();
            Bar bar = new Bar();
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  3. Sí SRP, no IoC

    class HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new SomeHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new Foo();
        }
    
        private Bar getBar(){
            return new Bar();
        }
    }
    
    class SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    HighLevelServiceProvider provider = new HighLevelServiceProvider();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
  4. Sí SRP, sí IoC

    interface HighLevelServiceProvider {
        SomeHighLevelService getSomeHighLevelService();
    }
    
    interface SomeHighLevelService {
        doFooBar();
    }
    
    interface Foo {
        Crap doFoo();
    }
    
    interface Bar {
        doBar(Crap crap);
    }
    
    
    class ConcreteHighLevelServiceContainer implements HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new ConcreteHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new ConcreteFoo();
        }
    
        private Bar getBar(){
            return new ConcreteBar();
        }
    }
    
    class ConcreteHighLevelService implements SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class ConcreteFoo implements Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class ConcreteBar implements Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    
    HighLevelServiceProvider provider = new ConcreteHighLevelServiceContainer();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();

Así que terminamos teniendo un código en el que puede reemplazar cada implementación concreta por otra que implemente la misma interfaz ofc. Esto es bueno porque las clases participantes están desacopladas entre sí, solo conocen las interfaces. Otra ventaja de que el código de la instanciación es reutilizable.

inf3rno
fuente