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.
fuente
Respuestas:
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
ItemService
es 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 comoSiteService
, probar el último de forma aislada puede valer la pena, por lo tanto DI no será necesario. Sin embargo, siItemService
está accediendo a otros sitios por red, es probable que desee realizar una prueba unitariaSiteService
desacoplada de él, lo que se puede lograr reemplazando el "real"ItemService
por aMockItemService
, 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
Read
mé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 laItemService
ló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.fuente
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.
fuente
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.
fuente
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í:
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.
fuente
Aísla los sistemas externos.
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.
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).
fuente