¿Cuándo debemos usar la inyección de dependencia (C #) [cerrado]

8

Me gustaría estar seguro de entender el concepto de inyección de dependencia (DI). Bueno, realmente entiendo el concepto, DI no es complicado: creas una interfaz y luego pasas la implementación de mi interfaz a la clase que la usa. La forma común de pasarlo es por constructor, pero también puede pasarlo por setter u otro método.

Lo que no estoy seguro de entender es cuándo usar DI.

Uso 1: Por supuesto, usar DI en caso de que tenga una implementación múltiple de su interfaz parece lógica. Tiene un repositorio para su SQL Server y luego otro para su base de datos Oracle. Ambos comparten la misma interfaz y usted "inyecta" (este es el término utilizado) el que desea en tiempo de ejecución. Esto ni siquiera es DI, aquí es una programación básica de OO.

Uso 2: cuando tiene una capa empresarial con muchos servicios con todos sus métodos específicos, parece que la mejor práctica es crear una interfaz para cada servicio y también inyectar la implementación, incluso si esta es única. Porque esto es mejor para el mantenimiento. Este es este segundo uso que no entiendo.

Tengo algo así como 50 clases de negocios. Nada es común entre ellos. Algunos son repositorios que obtienen o guardan datos en 3 bases de datos diferentes. Algunos leen o escriben archivos. Algunos hacen pura acción comercial. También hay validadores y ayudantes específicos. El desafío es la administración de memoria porque algunas clases se instalan desde diferentes ubicaciones. Un validador puede llamar a varios repositorios y otros validadores que pueden llamar a los mismos repositorios nuevamente.

Ejemplo: capa empresarial

public class SiteService : Service, ICrud<Site>
{
    public Site Read(Item item, Site site)
    {
        return beper4DbContext.Site
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public Site Read(string itemCode, string siteCode)
    {       
        using (var itemService = new ItemService())
        {
            var item = itemService.Read(itemCode);
            return Read(item, site);
        }
    }
}
public class ItemSiteService : Service, ICrud<Site>
{
    public ItemSite Read(Item item, Site site)
    {
        return beper4DbContext.ItemSite
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public ItemSite Read(string itemCode, string siteCode)
    {        
        using (var itemService = new ItemService())
        using (var siteService = new SiteService())
        {
            var item = itemService.Read(itemCode);
            var site = siteService.Read(itemCode, siteCode);
            return Read(item, site);
        }
    }
}

Controlador

public class ItemSiteController : BaseController
{
    [Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
    public IHttpActionResult Get(string itemCode, string siteCode)
    {
        using (var service = new ItemSiteService())
        {
            var itemSite = service.Read(itemCode, siteCode);
            return Ok(itemSite);
        }
    }
}

Este ejemplo es muy básico, pero puede ver cómo puedo crear fácilmente 2 instancias de itemService para obtener un itemSite. Entonces también cada servicio viene con su contexto DB. Entonces esta llamada creará 3 DbContext. 3 conexiones.

Mi primera idea fue crear singleton para reescribir todo este código como abajo. El código es más legible y lo más importante es que el sistema singleton crea solo una instancia de cada servicio utilizado y lo crea en la primera llamada. Perfecto, excepto que todavía tengo diferentes contextos pero puedo hacer el mismo sistema para mis contextos. Hecho.

Capa empresarial

public class SiteService : Service, ICrud<Site>
{
    public Site Read(Item item, Site site)
    {
        return beper4DbContext.Site
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public Site Read(string itemCode, string siteCode)
    {       
            var item = ItemService.Instance.Read(itemCode);
            return Read(item, site);
    }
}
public class ItemSiteService : Service, ICrud<Site>
{
    public ItemSite Read(Item item, Site site)
    {
        return beper4DbContext.ItemSite
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public ItemSite Read(string itemCode, string siteCode)
    {        
            var item = ItemService.Instance.Read(itemCode);
            var site = SiteService.Instance.Read(itemCode, siteCode);
            return Read(item, site);       
    }
}

Controlador

public class ItemSiteController : BaseController
{
    [Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
    public IHttpActionResult Get(string itemCode, string siteCode)
    {
            var itemSite = service.Instance.Read(itemCode, siteCode);
            return Ok(itemSite);
    }
}

Algunas personas me dicen de acuerdo con las buenas prácticas que debería usar DI con una sola instancia y usar singleton es una mala práctica. Debería crear una interfaz para cada clase de negocios y crear una instancia con la ayuda del contenedor DI. De Verdad? Este DI simplifica mi código. Difícil de creer.

Bastien Vandamme
fuente
44
DI nunca ha sido sobre legibilidad.
Robert Harvey

Respuestas:

14

El caso de uso más "popular" para DI (aparte del uso del patrón de "estrategia" que ya describió) es probablemente la prueba unitaria.

Incluso si cree que solo habrá una implementación "real" para una interfaz inyectada, en caso de que realice pruebas unitarias, generalmente hay una segunda: una implementación "simulada" con el único propósito de hacer posible una prueba aislada. Eso le brinda el beneficio de no tener que lidiar con la complejidad, los posibles errores y quizás el impacto en el rendimiento del componente "real".

Entonces, no, DI no es para aumentar la legibilidad, se usa para aumentar la capacidad de prueba (- por supuesto, no exclusivamente).

Este no es un fin en sí mismo. Si su clase ItemServicees muy simple, lo que significa que no tiene acceso a una red externa o base de datos, por lo que no obstaculiza la escritura de pruebas unitarias para algo como SiteService, probar el último de forma aislada puede valer la pena, por lo tanto DI no será necesario. Sin embargo, si ItemServiceestá accediendo a otros sitios por red, es probable que desee realizar una prueba unitaria SiteServicedesacoplada de él, lo que se puede lograr reemplazando el "real" ItemServicepor a MockItemService, que ofrece algunos elementos falsos codificados.

Permítanme señalar otra cosa: en sus ejemplos, uno podría argumentar que uno no necesitará DI aquí para probar la lógica comercial central: los ejemplos muestran siempre dos variantes de los Readmétodos, uno con la lógica comercial real involucrada (que puede ser unidad probada sin DI), y uno que es solo el código de "pegamento" para conectar la ItemServicelógica anterior. En el caso mostrado, ese es realmente un argumento válido contra la DI; de hecho, si la DI puede evitarse sin sacrificar la capacidad de prueba de esa manera, entonces continúe. Pero no todo el código del mundo real es tan simple, y a menudo DI es la solución más simple para lograr una "suficiente" capacidad de prueba de la unidad.

Doc Brown
fuente
1
Realmente lo entiendo, pero ¿qué pasa si no hago pruebas unitarias? Hago pruebas de integración con una base de datos falsa. Los cambios se realizan con demasiada frecuencia para mantener las pruebas unitarias y nuestros datos de producción cambian todo el tiempo. Por lo tanto, probar con una copia de datos reales es la única forma en que podemos probarlo seriamente. En lugar de Unit Testing, protegemos todos nuestros métodos con contratos de código. Sé que esto no es lo mismo y sé que las pruebas unitarias son positivas, pero no tengo tiempo para dar prioridad a las pruebas unitarias. Eso es un hecho, no una elección.
Bastien Vandamme
1
@BastienVandamme: hay un gran espacio entre "ninguna prueba de unidad en absoluto" y "la unidad prueba todo". Y nunca he visto un gran proyecto que no pueda encontrar algunos beneficios al usar pruebas unitarias al menos para algunas partes. Identifique esas partes, luego verifique si se requiere DI para hacer que la unidad sea comprobable.
Doc Brown
4

Al no usar la inyección de dependencia, te permites crear conexiones permanentes con otros objetos. Conexiones que puedes esconder adentro donde sorprenderán a la gente. Conexiones que solo pueden cambiar reescribiendo lo que estás creando.

En lugar de eso, puede usar la inyección de dependencia (o pasar referencias si es de la vieja escuela como yo) para hacer explícito lo que un objeto necesita sin obligarlo a definir cómo se deben satisfacer sus necesidades.

Esto te obliga a aceptar muchos parámetros. Incluso aquellos con valores predeterminados obvios. En C #, los malditos tienen argumentos opcionales y nombrados . Eso significa que tiene argumentos predeterminados. Si no le importa estar estáticamente vinculado a sus valores predeterminados, incluso cuando no los use, puede permitir DI sin sentirse abrumado con las opciones. Esto sigue la convención sobre la configuración .

La prueba no es una buena justificación para la DI. En el momento en que piense que es alguien, le venderá un marco de imitación de wiz bang que usa la reflexión o alguna otra magia para convencerlo de que puede volver a la forma en que trabajaba antes y usar magia para hacer el resto.

Utilizado correctamente, las pruebas pueden ser una buena manera de mostrar si un diseño está aislado. Pero ese no es el punto. No impide que los vendedores intenten demostrar que con suficiente magia todo está aislado. Mantenga la magia al mínimo.

El objetivo de este aislamiento es gestionar el cambio. Es bueno si se puede hacer un cambio en un solo lugar. No es bueno tener que seguirlo archivo tras archivo con la esperanza de que la locura termine.

Ponme en una tienda que se niegue a hacer pruebas unitarias y aún haré DI. Lo hago porque me permite separar lo que se necesita de cómo se hace. Pruebas o no pruebas Quiero ese aislamiento.

naranja confitada
fuente
2
¿Puede explicar por qué las pruebas no son una buena justificación para la DI? Parece que lo estás combinando con marcos de reflexión mágicos, y entiendo que no te gustan, pero no haces ningún contrapunto a la idea en sí.
Jacob Raihle 01 de
1
El argumento burlón parece una pendiente resbaladiza. No uso marcos de imitación, pero entiendo los beneficios de DI.
Robert Harvey
2
Es simplemente una cola de advertencia. Cuando las personas piensan que habilitar la prueba automatizada es la única razón para hacer DI, no solo se pierden la comprensión de los beneficios para diseñar la flexibilidad, sino que también se vuelven vulnerables a los argumentos de venta de productos que prometen beneficios sin el trabajo. DI es trabajo. No hay almuerzo gratis.
candied_orange 01 de
3
Para decirlo de otra manera, el desacoplamiento es la verdadera razón de DI. La prueba es simplemente una de las áreas más comunes donde los beneficios del desacoplamiento son evidentes.
StackOverthrow
2
Oye, no es broma. Si DI no lo ayuda a mantener su código, no lo use.
candied_orange
2

La vista de helicóptero de DI es simplemente la capacidad de intercambiar una implementación por una interfaz . Si bien esto, por supuesto, es una bendición para las pruebas, hay otros beneficios potenciales:

Implementaciones de versiones de un objeto

Si sus métodos aceptan parámetros de interfaz en las capas intermedias, puede pasar cualquier implementación que desee en la capa superior, lo que reduce la cantidad de código que debe escribirse para intercambiar implementaciones. Es cierto que este es un beneficio de las interfaces de todos modos, pero si el código está escrito con DI en mente, obtendrá este beneficio de inmediato.

Reducir la cantidad de objetos que necesitan pasar a través de capas

Si bien esto se aplica principalmente a los marcos DI, si el objeto A requiere instancias del objeto B , es posible consultar el núcleo (o lo que sea) para generar el objeto B sobre la marcha en lugar de pasarlo a través de las capas. Esto reduce la cantidad de código que debe escribirse y probarse. También mantiene capas que no les importanlimpias las objeto B.

Robbie Dee
fuente
2

No es necesario usar interfaces para usar DI. El propósito principal de DI es separar la construcción y el uso de objetos.

El uso de singletons está mal visto en la mayoría de los casos. Una de las razones es que se hace muy difícil obtener una visión general de las dependencias que tiene una clase.

En su ejemplo, ItemSiteController podría simplemente tomar un ItemSiteService como argumento del constructor. Esto le permite evitar cualquier costo de crear objetos, pero evita la inflexibilidad de un singleton. Lo mismo se aplica a ItemSiteService, si necesita un ItemService y un SiteService, inyéctelos en el constructor.

El beneficio es mayor cuando todos los objetos usan inyección de dependencia. Esto le permite centralizar la construcción en un módulo dedicado o delegarlo en un contenedor DI.

Una jerarquía de dependencia podría verse así:

public interface IStorage
{
}

public class DbStorage : IStorage
{
    public DbStorage(string connectionString){}
}

public class FileStorage : IStorage
{
    public FileStorage(FileInfo file){}
}

public class MemoryStorage : IStorage
{
}

public class CachingStorage : IStorage
{
    public CachingStorage(IStorage storage) { }
}

public class MyService
{
    public MyService(IStorage storage){}
}

public class Controller
{
    public Controller(MyService service){}
}

Tenga en cuenta que solo hay una clase sin parámetros de constructor, y solo una interfaz. Al configurar el contenedor DI, puede decidir qué almacenamiento usar, o si se debe usar el almacenamiento en caché, etc. Las pruebas son más fáciles ya que puede decidir qué base de datos usar, o usar algún otro tipo de almacenamiento. También puede configurar el contenedor DI para tratar objetos como singletons si es necesario, dentro del contexto del objeto contenedor.

JonasH
fuente
"En su ejemplo, ItemSiteController podría simplemente tomar un ItemSiteService como argumento del constructor". Por supuesto, pero un controlador puede usar de 1 a 10 servicios. Luego, en algún momento puedo agregar un nuevo servicio que me obliga a cambiar la firma del constructor. ¿Cómo puedes decir que esto es mejor para el mantenimiento? Y no, esto probablemente me costará crear el objeto varias veces porque ya no tengo el mecanismo singleton. Así que tendré que pasarlo a cada controlador que lo use. ¿Cómo puedo asegurarme de que cualquier desarrollador no siempre lo esté recreando? ¿Qué sistema puede garantizar que solo lo creo una vez?
Bastien Vandamme
Creo que, como la mayoría de las personas, está mezclando el patrón DI con la función de contenedor. Su ejemplo es mi Uso 1. No cuestiono este. Cuando tiene una implementación múltiple de una interfaz, debe usar lo que llama DI. Yo llamo a eso OOP. Lo que sea. Desafío el Uso 2, donde la mayoría de las personas parece usarlo para pruebas y mantenimiento. Entiendo pero esto no resuelve mi solicitud de instancia única. Esperemos que Container parezca ofrecer una solución. Contenedor ... no solo DI. Creo que necesito DI con una función de instancia única y puedo encontrarla en contenedores (bibliotecas de terceros).
Bastien Vandamme
@ bastien-vandamme Si necesita agregar nuevos servicios, debe agregarlos al constructor, esto no debería ser un problema. Si necesita inyectar muchos servicios, esto puede indicar un problema con la arquitectura, no la idea de inyectar las dependencias. Se asegura de que otros desarrolladores reutilicen la misma instancia mediante un buen diseño y capacitación de sus colegas. Pero en general debería ser posible tener múltiples instancias de una clase. Como ejemplo, podría tener varias instancias de base de datos y tener un servicio para cada una.
JonasH
Euh no. Tengo una base de datos con 50 tablas, necesito 50 repositorios, 50 servicios. Puedo trabajar con repositorios genéricos, pero mi base de datos está lejos de ser normalizada y limpia, por lo que cualquiera que sea mi repositorio o servicio en algún momento debe tener un código específico debido al historial. Entonces no puedo hacer todo genérico. Cada servicio tiene reglas comerciales específicas que necesito mantener por separado.
Bastien Vandamme
2

Aísla los sistemas externos.

Uso 1: Por supuesto, usar DI en caso de que tenga una implementación múltiple de su interfaz parece lógica. Tiene un repositorio para su SQL Server y luego otro para su base de datos Oracle. Ambos comparten la misma interfaz y usted "inyecta" (este es el término utilizado) el que desea en tiempo de ejecución. Esto ni siquiera es DI, aquí es una programación básica de OO.


Sí, use DI aquí. Si va a la red, base de datos, sistema de archivos, otro proceso, entrada del usuario, etc. Desea aislarlo.

El uso de DI facilitará las pruebas porque fácilmente se burlará de estos sistemas externos. No, no estoy diciendo que ese sea el primer paso hacia las pruebas unitarias. Tampoco que no puedas hacer pruebas sin hacerlo.

Además, incluso si solo tuviera una base de datos, usar DI lo ayudaría el día que desea migrar. Entonces, sí, DI.

Uso 2: cuando tiene una capa empresarial con muchos servicios con todos sus métodos específicos, parece que la mejor práctica es crear una interfaz para cada servicio y también inyectar la implementación, incluso si esta es única. Porque esto es mejor para el mantenimiento. Este es este segundo uso que no entiendo.

Claro, DI puede ayudarte. Debatiría sobre los contenedores.

Quizás, algo digno de mención, es que la inyección de dependencia con tipos concretos sigue siendo inyección de dependencia. Lo que importa es que puede crear instancias personalizadas. No tiene que ser una inyección de interfaz (aunque la inyección de interfaz es más versátil, eso no significa que deba usarla en todas partes).

La idea de crear una interfaz explícita para cada clase tiene que morir. De hecho, si tuviera una sola implementación de una interfaz ... YAGNI . Agregar una interfaz es relativamente barato, se puede hacer cuando lo necesite. De hecho, sugeriría esperar hasta que tenga dos o tres implementaciones candidatas para tener una mejor idea de qué cosas son comunes entre ellas.

Sin embargo, la otra cara de eso es que podría crear interfaces que coincidan más de lo que necesita el código del cliente. Si el código del cliente solo necesita unos pocos miembros de una clase, puede tener una interfaz solo para eso. Eso conducirá a una mejor segregación de la interfaz .


Contenedores?

Sabes que no los necesitas.

Vamos a reducirlo a compensaciones. Hay casos en que no valen la pena. Hará que su clase tome las dependencias que necesita en el constructor. Y eso podría ser lo suficientemente bueno.

Realmente no soy fanático de anotar atributos para la "inyección de setter", y mucho menos los de terceros, entiendo que podría ser necesario para implementaciones más allá de su control ... sin embargo, si decide cambiar la biblioteca, estas tienen que cambiar.

Eventualmente comenzará a desarrollar rutinas para crear estos objetos, porque para crearlos, primero necesita crear estos otros, y para aquellos que necesita un poco más ...

Bueno, cuando eso sucede, quieres poner toda esa lógica en un solo lugar y reutilizarla. Desea una única fuente de verdad sobre cómo crear su objeto. Y lo obtienes al no repetirte . Eso simplificará tu código. Tiene sentido, ¿verdad?

Bueno, ¿dónde pones esa lógica? El primer instinto será tener un Localizador de servicios . Una implementación simple es un singleton con un diccionario de fábrica de solo lectura . Una implementación más compleja podría usar la reflexión para crear las fábricas cuando no haya proporcionado una.

Sin embargo, el uso de un localizador de servicio estático o singleton significa que hará algo como var x = IoC.Resolve<?>cada lugar donde necesita crear una instancia. Lo que está agregando un fuerte acoplamiento a su localizador de servicio / contenedor / inyector. Eso puede hacer que las pruebas unitarias sean más difíciles.

Desea un inyector que cree una instancia y lo guarde solo para usarlo en el controlador. No desea que se profundice en el código. Eso podría hacer que las pruebas sean más difíciles. Si alguna parte de su código lo necesita para crear una instancia de algo, debería esperar una instancia (o la mayoría de una fábrica) en su constructor.

Y si tiene muchos parámetros en el constructor ... vea si tiene parámetros que viajan juntos. Lo más probable es que pueda fusionar parámetros en tipos de descriptor (los tipos de valor idealmente).

Theraot
fuente
Gracias por esta explicación clara. Entonces, de acuerdo con su, debería crear una clase de inyector que contenga la lista de todos mis servicios. No inyecto servicio por servicio. Inyecto mi clase de inyector. ¿Esta clase gestiona también la instancia única de cada uno de mis servicios?
Bastien Vandamme
@BastienVandamme puede acoplar sus controladores con el inyector. Entonces, si te refieres a pasarlo al controlador, entonces estoy de acuerdo. Y sí, puede manejar tener una sola instancia de los servicios. Por otro lado, me preocuparía si el controlador pasa el inyector ... puede pasar fábricas si es necesario. De hecho, la idea es separar los servicios de la inicialización de sus dependencias. Entonces, idealmente, los servicios no conocen el inyector.
Theraot
"Usar DI facilitará las pruebas porque fácilmente se burlarán de estos sistemas externos". He descubierto que confiar en burlarse de estos sistemas en las pruebas unitarias sin minimizar primero el código que tiene alguna conexión con ellos reduce severamente la utilidad de tener pruebas unitarias. Primero, maximice la cantidad de código que se puede probar solo mediante pruebas de entrada / salida. Luego mire si es necesario burlarse.
jpmc26
@ jpmc26 estuvo de acuerdo. Anexo: debemos aislar esos sistemas externos de todos modos.
Theraot
@BastienVandamme He estado pensando en esto. Creo que si no pierde la capacidad de crear para crear inyectores personalizados para pasar, hacerlo debería estar bien.
Theraot