8 de diciembre de 2011

Principio de Inversión de Dependencias - SOLID

Para finalizar con la serie de posts sobre los principios SOLID, luego de unos meses bastante ajetreados que incluyeron una variedad de cambios (cambio de trabajo, cambio de pañales, cambio de sueño... :P) en esta oportunidad vamos a ver el principio DIP, Dependency Inversion Principle.

Este principio intenta solucionar algunos de los problemas más comunes del diseño orientado a objetos.

Rigidez: los cambios afectan a demasiadas partes del sistema
Fragilidad: cada cambio genera problemas en lugares inesperados
Inmobilidad: imposibilidad de reutilización

La definición del principio dice lo siguiente:

A. Módulos de niveles superiores no deben depender de módulos de niveles inferiores. Ambos deben depender de abstracciones.
B. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.


Un ejemplo de un diseño que se vería beneficiado aplicando DIP es el siguiente:

Supongamos que tenemos una aplicación de ventas online hecha con ASP.NET MVC. Esta aplicación tiene un controlador con una acción que es ejecutada cuando el usuario confirma una compra. A su vez, cuando el usuario confirma la compra, se le envía un email con información sobre la misma.

public class ShoppingCartController : Controller
{

    public ActionResult Checkout()
    {
        ...
 
        customer = ...
        order = ...

        var emailSender = new EmailSender(server: "smtp.myserver.com");
        emailSender.SendOrderConfirmed(customer, order);

        ...
    }

}


Imaginemos que pasa el tiempo y tengo que modificar la clase EmailSender para poder especificar, además del servidor, el puerto que se va a utilizar para enviar los emails.

    

    var emailSender = new EmailSender(server: "smtp.myserver.com", port: 255);



Esto hace evidente el primer problema de nuestro diseño, estamos ante un diseño rígido, ya que el cambio en la clase EmailSender me obliga a modificar todos los lugares de mi aplicación donde la esté utilizando, caso contrario, mi aplicación no compilará.

Una posible solución es dejar el constructor de EmailSender tal como está actualmente y agregar una propiedad Port para poder indicar el puerto a utilizar al enviar mails.

    var emailSender = new EmailSender(server: "smtp.myserver.com");
    emailSender.Port = 255;



Este nuevo diseño tiene otro problema, es frágil, ya que si utilizo la clase EmailSender en varios lugares de mi aplicación, fácilmente puedo olvidar asignar el valor correcto a la propiedad Port, lo cual resultará en una aplicación que no siempre se comportará como es esperado (en algunos casos el envío de mails funcionará por tener el puerto asignado correctamente y en otros no, por utilizar el puerto default).

Para solucionar esto podemos mover la creación de la clase EmailSender a un lugar centralizado, y pasarle una instancia ya creada y correctamente configurada al controller:

    public ShoppingCartController(EmailSender emailSender)
    {
        this.emailSender = emailSender;
    }



La acción de pasarle al controlador sus dependencias, en este caso una instancia de EmailSender, es llamado inyección de dependencias (en inglés, dependency injection ó simplemente DI).

La inyección de dependencias puede hacerse por constructor, como es en este caso, o también por propiedades. Por lo general la inyección por constructor es la práctica más recomendable.

Vale la pena mencionar que la inyección de dependencias se puede hacer en forma manual o mediante algún contenedor de inversión de control (en inglés, inversion of control container ó ioc container). Algunos ioc containers son Castle Windsor, Unity o StructureMap, por mencionar algunos.

Hasta acá venimos bien salvo por el hecho que este diseño tiene otro problema, la "inmovilidad", ya que si quisiéramos agregar un nuevo tipo de mecanismo para enviar notificaciones, por ejemplo por SMS, no podríamos reutilizar el controlador tal cual está ya que el mismo depende de EmailSender.

Este problema puede ser solucionado aplicando el principio de inversión de dependencias, para esto debemos crear una abstracción de EmailSender (interfaz IMessageSender) y de esa manera podremos "invertir" la dependencia:





Esto es llamado inversión de dependencias (o inversión de control) ya que se podría decir que la dirección de la dependencia entre ShoppingCartController y EmailSender, al introducir la abstracción IMessageSender, fué invertida.

Finalmente, podemos ver que nuestro diseño final cumple DIP ya que:

A. El controller (módulo de nivel superior) ya no depende de EmailSender (módulo de nivel inferior) y tanto el controller como el EmailSender dependen de IMessageSender (una abstracción)
B. El controller y EmailSender (detalles) dependen de IMessageSender (una abstracción)

En mi primer post sobre el principio SRP podrán encontrar varios enlaces a las lecturas recomendadas sobre principios SOLID.

Espero que sea de utilidad, cualquier comentario será bienvenido.

Posts anteriores sobre principios SOLID
Principio de responsabilidad única - SRP
Principio abierto-cerrado - OCP
Principio de Substitución de Liskov - LSP
Principio de Separación de Interfaces - ISP