La inyección de dependencia (DI) no es un concepto fácil de entender y aplicarlo a aplicaciones nuevas o existentes es aún más confuso. Jess Smith le muestra cómo realizar una inyección de dependencia sin un contenedor de inyección en los lenguajes de programación C# y Java. En este artículo, le mostraré cómo implementar la inyección de dependencia (DI) en aplicaciones .NET y Java. El concepto de inyección de dependencia llamó la atención de los desarrolladores por primera vez en 2000, cuando Robert Martin escribió el artículo "Principios y patrones de diseño" (más tarde conocido por el acrónimo
SOLID ). La D en SOLID se refiere a la Dependencia de Inversión (DOI), que más tarde se conoció como inyección de dependencia. La definición original y más común: la inversión de dependencias es una inversión de la forma en que una clase base gestiona las dependencias. El artículo original de Martin utilizó el siguiente código para ilustrar la dependencia de una clase
Copy
de una clase de nivel inferior
WritePrinter
:
void Copy()
{
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
El primer problema obvio es que si cambia la lista de parámetros o los tipos de un método
WritePrinter
, necesita implementar actualizaciones siempre que exista una dependencia de ese método. Este proceso aumenta los costos de mantenimiento y es una fuente potencial de nuevos errores.
Otro problema: la clase Copiar ya no es un candidato potencial para su reutilización. Por ejemplo, ¿qué sucede si necesita enviar los caracteres ingresados desde el teclado a un archivo en lugar de a una impresora? Para hacer esto, puede modificar la clase
Copy
de la siguiente manera (sintaxis del lenguaje C++):
void Copy(outputDevice dev)
{
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
A pesar de la introducción de una nueva dependencia
WriteDisk
, la situación no mejoró (sino empeoró) porque se violó otro principio: “las entidades de software, es decir, clases, módulos, funciones, etc., deben estar abiertas a la extensión, pero cerradas a la extensión”. modificación." Martin explica que estas nuevas declaraciones condicionales if/else reducen la estabilidad y flexibilidad del código. La solución es invertir las dependencias para que los métodos de escritura y lectura dependan del archivo
Copy
. En lugar de "hacer estallar" las dependencias, se pasan a través del constructor. El código modificado se ve así:
class Reader
{
public:
virtual int Read() = 0;
};
class Writer
{
public:
virtual void Write(char) = 0;
};
void Copy(Reader& r, Writer& w)
{
int c;
while((c=r.Read()) != EOF)
w.Write(c);
}
Ahora la clase
Copy
se puede reutilizar fácilmente con diferentes implementaciones de métodos de clase
Reader
y
Writer
. La clase
Copy
no tiene información sobre la estructura interna de los tipos
Reader
y
Writer
, por lo que es posible reutilizarlos con diferentes implementaciones. Pero si todo esto le parece una especie de palabrería, tal vez los siguientes ejemplos en Java y C# aclaren la situación.
Ejemplo en Java y C#
Para ilustrar la facilidad de la inyección de dependencias sin un contenedor de dependencias, comencemos con un ejemplo simple que se puede personalizar para su uso
DI
en solo unos pocos pasos. Digamos que tenemos una clase
HtmlUserPresentation
que, cuando se llaman sus métodos, genera una interfaz de usuario HTML. He aquí un ejemplo sencillo:
HtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Cualquier proyecto que utilice este código de clase dependerá de la clase
HtmlUserPresentation
, lo que dará lugar a los problemas de usabilidad y mantenimiento descritos anteriormente. Inmediatamente se sugiere una mejora: crear una interfaz con firmas de todos los métodos disponibles actualmente en la clase
HtmlUserPresentation
. A continuación se muestra un ejemplo de esta interfaz:
public interface IHtmlUserPresentation {
String createTable(ArrayList rowVals, String caption);
String createTableRow(String tableCol);
}
Después de crear la interfaz, modificamos la clase
HtmlUserPresentation
para usarla. Volviendo a crear instancias del tipo
HtmlUserPresentation
, ahora podemos usar el tipo de interfaz en lugar del tipo base:
IHtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
La creación de una interfaz nos permite utilizar fácilmente otras implementaciones de
IHtmlUserPresentation
. Por ejemplo, si queremos probar este tipo, podemos reemplazar fácilmente el tipo base
HtmlUserPresentation
con otro tipo llamado
HtmlUserPresentationTest
. Los cambios realizados hasta ahora hacen que el código sea más fácil de probar, mantener y escalar, pero no contribuyen a su reutilización, ya que todas las
HtmlUserPresentation
clases que utilizan el tipo aún conocen su existencia. Para eliminar esta dependencia directa, puedes pasar un tipo de interfaz
IHtmlUserPresentation
al constructor (o lista de parámetros del método) de la clase o método que lo usará:
public UploadFile(IHtmlUserPresentation htmlUserPresentation)
El constructor
UploadFile
ahora tiene acceso a toda la funcionalidad del tipo
IHtmlUserPresentation
, pero no sabe nada sobre la estructura interna de la clase que implementa esta interfaz. En este contexto, la inyección de tipos ocurre cuando se crea una instancia de la clase
UploadFile
. Un tipo de interfaz
IHtmlUserPresentation
se vuelve reutilizable al pasar diferentes implementaciones a diferentes clases o métodos que requieren diferentes funcionalidades.
Conclusión y recomendaciones para consolidar el material.
Aprendió sobre la inyección de dependencia y que se dice que las clases dependen directamente entre sí cuando una de ellas crea una instancia de otra para obtener acceso a la funcionalidad del tipo de destino. Para desacoplar la dependencia directa entre los dos tipos, debes crear una interfaz. Una interfaz le da a un tipo la capacidad de incluir diferentes implementaciones, según el contexto de la funcionalidad requerida. Al pasar un tipo de interfaz a un método o constructor de clase, la clase/método que necesita la funcionalidad no conoce ningún detalle sobre el tipo que implementa la interfaz. Debido a esto, un tipo de interfaz se puede reutilizar en diferentes clases que requieren un comportamiento similar, pero no idéntico.
- Para experimentar con la inyección de dependencia, mire su código de una o más aplicaciones e intente convertir un tipo base muy utilizado en una interfaz.
- Cambie las clases que crean instancias directas de este tipo base para usar este nuevo tipo de interfaz y páselo a través del constructor o la lista de parámetros del método de clase que lo usará.
- Cree una implementación de prueba para probar este tipo de interfaz. Una vez que su código sea refactorizado,
DI
será más fácil de implementar y notará cuánto más flexible se vuelve su aplicación en términos de reutilización y mantenibilidad.
GO TO FULL VERSION