En el ámbito del desarrollo de software, siempre es bueno eliminar dependencias. En otros términos, bajar el nivel de acoplamiento.

Hay casos en que se logra más directamente y otros en que no es tan simple.

Para estos últimos contamos con un pattern llamado Dependency Injection, Inyección de Dependencia, término por primera vez usado por Martin Fowler. Si te interesa ir a la fuente, acá está su documento sobre Dependency Injection.

En este post voy a intentar explicar que viene a resolver este patrón y sus distintas implementaciones.

Cuando tenemos un objeto que necesita de otro para funcionar correctamente, tenemos definida una dependencia. Esta dependecia puede ser altamente acoplada, tight coupling, o levemente acoplada, loose coupling. Si el acoplamiento es bajo el objeto independiente es fácilmente reemplazable, si, en cambio el acoplamiento es alto, el reemplazo no es fácil y se dificulta el diseño de tests unitarios.

Supongamos que tenemos una clase que define un producto, que necesita un componente, este componente puede provenir de distintos fabricantes, en general cubren las mismas funciones. Los productos son varios y alternativamente pueden valerse del componente de cualquier fabricante.

Si instanciamos uno de los componentes en forma directa necesitamos referenciarlo. Si el mercado nos exige cambiar de componente, tendríamos que eliminar la referencia anterior, crear la nueva, revisar si los métodos, funciones y propiedades son homogéneos, si no lo son, corregir el código donde sea necesario… Si a esto le agregamos que valdría la pena tener una versión con el primer componente y otra con el segundo, la situación empeora. Si hubiera cincuenta componentes alternativos, la situación ya sería inmanejable.

Acá es donde la necesidad del patrón Dependency Injection se hace evidente.

Supongamos que este conjunto de componentes ejecuta básicamente cuatro métodos, que son los que nosotros necesitamos: Initialize, Shutdown, Prepare y DoIt.

Entonces comencemos por escribir una interface que defina estos cuatro métodos:

[VB]

Public Interface ISomeHardware

Sub Initialization()

Sub Shutdown()

Sub Prepare()

Sub DoIt()

End Interface

[C#]

public interface ISomeHardware

{

void Initialization();

void Shutdown();

void Prepare();

void DoIt();

}

Las clases que definan los componentes deben implementar la interface ISomeHardware. En nuestro ejemplo, ComponentA y ComponentB:

[VB]

Public Class ComponentA

Implements ISomeHardware

Public Sub InitializationBegin() Implements ISomeHardware.Initialization

End Sub

Public Sub ShutdownBegin() Implements ISomeHardware.Shutdown

End Sub

Public Sub PrepareBegin() Implements ISomeHardware.Prepare

End Sub

Public Sub DoItBegin() Implements ISomeHardware.DoIt

End Sub

End Class

Public Class ComponentB

Implements ISomeHardware

Public Sub InitializationBegin() Implements ISomeHardware.Initialization

End Sub

Public Sub ShutdownBegin() Implements ISomeHardware.Shutdown

End Sub

Public Sub PrepareBegin() Implements ISomeHardware.Prepare

End Sub

Public Sub DoItBegin() Implements ISomeHardware.DoIt

End Sub

End Class

[C#]

class ComponentA : ISomeHardware

{

public void Initialization() { }

public void Shutdown() { }

public void Prepare() { }

public void DoIt() { }

}

class ComponentB : ISomeHardware

{

public void Initialization() { }

public void Shutdown() { }

public void Prepare() { }

public void DoIt() { }

}

Aclaración: Acá hago una simplificación a efecto didáctico. Las clases que definen ComponentA y ComponentB, en realidad debieran ser wrappers de las dll’s provistas por los fabricantes. Para no complicar inútilmente las dejaré así.

Veremos tres implementaciones del patrón Dependency Injection: por Constructor, por Setter y por Interface.

Por Constructor:

Ahora voy a usar otro patrón llamado Facade o Fachada, la idea es encapsular todos los aspectos complejos de un subsistema de clases en una única y simple interface. En nuestro caso, ProductFacade:

[VB]

Public Class ProductFacade

Private SomeHardware As ISomeHardware

Public Sub New(ByVal pSomeHardware As ISomeHardware)

SomeHardware = pSomeHardware

End Sub

End Class

[C#]

public class ProductFacade

{

private ISomeHardware SomeHardware;

public ProductFacade(ISomeHardware SomeHardware)

{

this.SomeHardware = SomeHardware;

}

}

En esta clase se implementa la Inyección de Dependencia por medio del constructor, que acepta un parámetro de entrada de tipo de interface ISomeHardware. Este punto es determinante. Cualquier clase que implemente la interface ISomeHardware es aceptada como parámetro del constructor de ProductFacade. Este es todo el secreto.

Entonces, para inyectar la dependencia de un componente de un fabricante u otro por Constructor, se hace así:

[VB]

Dim ComponentA As ISomeHardware = New ComponentA

Dim Product01 As ProductFacade = New ProductFacade(ComponentA)

Dim ComponentB As ISomeHardware = New ComponentB

Dim Product02 As ProductFacade = New ProductFacade(ComponentB)

[C#]

ISomeHardware componentA = new ComponentA();

ProductFacade product01 = new ProductFacade(componentA);

ISomeHardware componentB = new ComponentB();

ProductFacade product02 = new ProductFacade(componentB);

Realmente elegante.

Para las siguientes dos implementaciones del patrón, usaremos la misma interface ISomeHardware y la definición de las clases ComponentA y ComponentB.

Las variaciones estarán en la Fachada y en la Inyección de Dependencia.

Por Setter:

En este caso la Inyección de Dependencia se efectiviza a través de una propiedad definida en la Fachada.

[VB]

Public Class ProductFacade

Private mSomeHardware As ISomeHardware

Public Property SomeHardware() As ISomeHardware

Get

Return mSomeHardware

End Get

Set(ByVal value As ISomeHardware)

mSomeHardware = value

End Set

End Property

End Class

[C#]

public class ProductFacade

{

private ISomeHardware SomeHardware;

public ISomeHardware Component

{

get

{

return SomeHardware;

}

set

{

SomeHardware = value;

}

}

}

Para el caso de Setter, la Inyección de Dependencia queda determinada así:

[VB]

Dim ComponentA As ISomeHardware = New ComponentA

Dim Product01 As ProductFacade = New ProductFacade()

Product01.SomeHardware = ComponentA

Dim ComponentB As ISomeHardware = New ComponentB

Dim Product02 As ProductFacade = New ProductFacade()

Product02.SomeHardware = ComponentB

[C#]

ISomeHardware componentA = new ComponentA();

ProductFacade product01 = new ProductFacade();

product01.Component = componentA;

ISomeHardware componentB = new ComponentB();

ProductFacade product02 = new ProductFacade();

product02.Component = componentB;

Por Interface:

Ahora la Inyección de Dependencia está implementada sobre un método que acepta un parámetro de tipo de interface ISomeHardware.

[VB]

Public Class ProductFacade

Private mSomeHardware As ISomeHardware

Public Sub SetComponent(ByVal pSomeHardware As ISomeHardware)

mSomeHardware = pSomeHardware

End Sub

End Class

[C#]

public class ProductFacade

{

private ISomeHardware SomeHardware;

public void SetComponent(ISomeHardware SomeHardware)

{

this.SomeHardware = SomeHardware;

}

}

Cuando la Inyección de Dependencia es por Interface, se invoca de esta forma:

[VB]

Dim ComponentA As ISomeHardware = New ComponentA

Dim Product01 As ProductFacade = New ProductFacade()

Product01.SetComponent(ComponentA)

Dim ComponentB As ISomeHardware = New ComponentB

Dim Product02 As ProductFacade = New ProductFacade()

Product02.SetComponent(ComponentB)

[C#]

ISomeHardware componentA = new ComponentA();

ProductFacade product01 = new ProductFacade();

product01.SetComponent(componentA);

ISomeHardware componentB = new ComponentB();

ProductFacade product02 = new ProductFacade();

product02.SetComponent(componentB);

Las tres implementaciones difieren sutilmente entre si. La diferencia más importante que yo encuentro es que, Por Constructor, nos obliga a inyectar al crear la clase, mientras que las otras modalidades, la difieren para más adelante.

La diferencia entre estas dos últimas está en si se prefiere inyectar la dependencia en el set de una propiedad, Por Setter, o como argumento de un parámetro de entrada a un método o función, Por Interface. Cuestión de gusto sintáctico.

Espero que les sea útil.🙂