Recibo una inyección de dependencia, pero ¿puede alguien ayudarme a entender 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 se habla de lo que es DI. Entonces, obtengo DI, pero estoy tratando de entender la necesidad de un contenedor IoC, en el que todos parecen estar metiéndose. ¿El punto de un contenedor de IoC realmente es simplemente "resolver automáticamente" la implementación concreta de las dependencias? Quizás mis clases tienden a no tener varias dependencias y tal vez por eso no veo el problema, pero quiero asegurarme de que estoy entendiendo la utilidad del contenedor correctamente.

Normalmente, rompo mi lógica de negocios en una clase que podría parecerse a esto:

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;
    }
}

Así que solo tiene una dependencia, y en algunos casos puede tener una segunda o tercera, pero no tan a menudo. Así que 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 IoC, todo lo que hace el contenedor es resolver IDataRepository. Pero si eso es todo lo que hace, entonces no veo un montón de valor en ello ya que mis clases operativas ya definen un repliegue cuando no se transmite ninguna dependencia. Entonces, el único otro beneficio que puedo pensar es que si tengo varias operaciones como este uso del mismo repositorio alternativo, puedo cambiar ese repositorio en un lugar que es el registro / fábrica / contenedor. Y eso es genial, pero ¿es eso?

    
pregunta Sinaesthetic 19.09.2014 - 01:49
fuente

3 respuestas

2

El contenedor IoC no se trata del caso en el que tiene una dependencia. Se trata del caso en el que tiene 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.

    
respondido por el Sign 20.09.2014 - 01:07
fuente
10

Hay varias razones por las que podría querer usar un contenedor IoC.

Dlls sin referencia

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

Evita el uso de new

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

Escribir en contra de abstracciones

El uso de un contenedor IoC para resolver sus dependencias concretas le permite escribir su código en contra de las abstracciones, en lugar de implementar cada clase concreta que necesita cuando la necesita. 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 eso. Puede utilizar 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.

Evita el código quebradizo

Otra razón para usar un contenedor IoC es que al confiar en el contenedor 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 las vidas de los objetos. Los contenedores IoC a menudo ofrecen la posibilidad 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 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 IoC administrarán la eliminación por usted, lo que puede ayudar a prevenir pérdidas de memoria y simplificar su base de código.

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

    
respondido por el Stephen 19.09.2014 - 02:31
fuente
2

De acuerdo con el principio de responsabilidad única, cada clase debe tener una sola responsabilidad. Crear nuevas instancias de clases es solo otra responsabilidad, por lo que tiene que 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 se relacionan con la instanciación de dependencias. Afirman que las clases de alto nivel deben desacoplarse de las clases de bajo nivel (dependencias) que utilizan. Podemos desacoplar las cosas creando interfaces. Así que 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, no 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 puedes reemplazar cada implementación concreta con otra que implementa la misma interfaz de c. Así que esto es bueno porque las clases participantes están desconectadas unas de otras, solo conocen las interfaces. Otra ventaja es que el código de la instanciación es reutilizable.

    
respondido por el inf3rno 19.09.2014 - 04:38
fuente

Lea otras preguntas en las etiquetas