jueves, 31 de octubre de 2013

Dependency Inversion Principle

Dependency Inversion Principle (DIP)
"Depender de abstracciones. No depender de concreciones."

Este post pertenece a la serie Principios SOLID.

Hay diferentes formas de definir el DIP:

  • Las abstracciones no deben depender de los detalles.
  • El código debe depender de cosas que estén en su mismo nivel de abstracción o superior
  • Las reglas de alto nivel no deben depender de los detalles de bajo nivel
  • etc.
Todas ellas se basan en la idea de crear dependencias hacia niveles de abstracción superior.

Una regla a seguir para invertir las dependencias es: Si tenemos una clase A que depende de otra clase B de un nivel de abstracción inferior, invertimos esa dependencia haciendo que ambas clases dependan de una tercera clase abstracta o interfaz C con un nivel de abstracción igual a A. Luego vamos a ver esto en detalle.

¿Por qué preocuparse por las dependencias?
Una dependencia es un riesgo. Si mi software necesita un determinado framework instalado y no lo está, mi software no funcionará. Probablemente también necesite un determinado sistema operativo, o un browser si es una aplicación web. 

Algunas de esas dependencias las podemos controlar y otras simplemente las ignoramos. Por ejemplo, si desarrollamos una aplicación web normalmente no sabemos cuál será el browser que se utilizará para acceder a ella. Por lo que no desarrollaremos una aplicación web dependiente de un browser concreto.

Manejar el riesgo que implican esas dependencias tiene un coste. A través de la experiencia, del siempre socorrido método de prueba y error, o de tus conocimientos debes decidir si quieres mitigar ese riesgo o no.

Significado de "Inversión" en DIP
Cuando leí por primera vez sobre este principio esta fue mi primera duda y tuve que leer muchas explicaciones más o menos vagas y abstractas hasta que lo entendí. Veamos, según el diccionario:
Inversión: Acción y efecto de invertir.
Invertir: Cambiar, sustituyéndolos por sus contrarios, la posición, el orden o el sentido de las cosas.

Así que la cosa "queda más clara". El DIP trata de cambiar el orden de las dependencias. Pero, ¿el orden con respecto a qué? El orden con respecto al diseño top-down.

En el diseño top-down empezamos con problemas de alto nivel y lo descomponemos en partes más pequeñas. Esas partes pequeñas a su vez las descomponemos en otras más pequeñas aún, etc. Es decir, los requerimientos de alto nivel los dividimos cada vez en partes más pequeñas y por consiguiente esos requerimientos dependen de las partes más pequeñas y más detalladas del sistema.

Un ejemplo:
Supongamos que la empresa ACME Suministros S.A. nos encarga un sistema para hacer la facturación y el envío de la factura electrónica mensual a sus clientes de los suministros que han consumido. Aquí de forma muy simplista pongo la descomposición top-down del problema:

1. Facturar a los clientes
    1.1 Obtener datos de los clientes
          1.1.1 Abrir la conexión
          1.1.2 Ejecutar consulta SQL
          1.1.3 Traducir registros en entidades de negocio
    1.2 Calcular los importes de las facturas
          1.2.1 Calcular Subtotales de los consumos
          1.2.2 Calcular Impuestos aplicables
          1.2.3 Sumar Subtotal e impuestos
    1.3 Generar factura electrónica
          1.3.1 Obtener la plantilla de la factura
          1.3.2 Rellenar la plantilla con los datos
          1.3.3 Generar un archivo pdf.
    1.4 Enviar factura por email
          1.4.1 Crear un email
          1.4.2 Adjuntar el pdf al email
          1.4.3 Conectar con el servidor smtp
          1.4.4 Enviar el email

Bueno, no era tan difícil ¿no? Estos serían los pasos a seguir para realizar el requerimiento.

Vamos a detenernos un poco a analizar esto. Nuestro requerimiento de alto nivel "Facturar a los clientes" depende de "Obtener datos de clientes" que depende de "Ejecutar consulta SQL" (las dependencias siguen a la forma de descomponer el requerimiento). Normalmente las cosas de más bajo nivel son las que suelen ser más propensas al cambio. Tenemos requerimientos de alto nivel ("Facturar a los clientes") que dependen de partes pequeñas que son propensas al cambio. Además los pasos a seguir son muy sensibles a los cambios en los niveles altos y eso es un problema por que los requerimientos cambian. 

Queremos invertir la dependencia con respecto a este tipo de descomposición top-down
Pues bien, si queremos invertir esa dependencia, manos a la obra. Supongamos que como parte de nuestro sistema para ACME Suministros S.A. escribimos el siguiente código:

   
namespace Acme.BL
{
    public class ClientsBL
    {
        public IEnumerable<ClientData> GetClientsData()
        {
            var dalClient = new ClientsDAL();
            return dalClient.GetClientData();
        }
    }
}

namespace Acme.DAL
{
    public class ClientsDAL
    {
        public IEnumerable<ClientData> GetClientData()
        {
            var dataResult = Enumerable.Empty<clientdata>();
            // Get data from Database.
            // and fill dataResult
            return dataResult;
        }
    }
}
El problema de este código es que un módulo de abstracción superior (ClientsBL) depende de otro inferior (ClientsDAL) y ese tipo de dependencia es justo la que queremos invertir. Veamos como:

Siguiendo la regla para invertir las dependencias crearemos una interfaz IClientesDAL en el nivel de abstracción de ClientsBL de la que dependerán tanto ClientsBL como ClientsDAL

    
namespace Acme.BL
{
    public class ClientsBL
    {
        private IClientDAL dalClient;
        public IEnumerable<ClientData> GetClientsData()
        {
            return dalClient.GetClientData();
        }
    }
    
    public interface IClientsDAL
    {
        IEnumerable<ClientData> GetClientData();
    }
}

namespace Acme.DAL
{
    public class ClientsDAL : IClientsDAL
    {
        public IEnumerable<ClientData> GetClientData()
        {
            var dataResult = Enumerable.Empty<clientdata>();
            // Get data from Database.
            // and fill dataResult
            return dataResult;
        }
    }
}

Ahora el DIP se cumple porque tanto ClientsBL como ClientsDAL dependen de IClientsDAL que está en un nivel de abstracción igual o superior a ambas (la capa de negocio).
Hemos pasado de esto:

a esto:

Ya no hay dependencias de implementaciones concretas ni de nivel de abstracción inferior.
Según el DIP nunca debemos crear dependencias hacia clases concretas, donde haya una dependencia siempre debe ser hacia una clase abstracta o una interfaz. Seguir este principio hasta las últimas consecuencias puede ser difícil y hay circunstancias en las que no seguirlo es razonable. Sin embargo, debemos tratar de aplicarlo tanto como sea posible.

Hay que tener en cuenta que este principio se basa en la asunción de que las cosas concretas cambian mucho más que las abstractas. Además las interfaces y clases abstractas son puntos de extensión del diseño. Representan puntos donde el diseño puede ser extendido sin tener que ser modificado. Se cumple de esta forma con el OCP.

Soy consciente de que aún queda algo por resolver en el diseño anterior. ClientBL ahora depende de IClientDal, pero no podemos crear una instancia de una interfaz. Entonces, ¿Cómo nos las vamos a arreglar para que ClientBL tenga acceso a un objeto de tipo ClientDAL?

Esto lo haremos aplicando Inyección de dependencias, pero dejaré la explicación de este concepto y la resolución del diseño para el próximo post.

Aquí acaba la serie sobre principios SOLID. A partir de ahora nos esperan algunos post explicando otros principios de diseño (DRY, YAGNI, KISS) y algunos conceptos importantes relacionados con lo que hemos visto hasta ahora (Dependency Injection, Inversion of Control).

5 comentarios:

  1. Magistral explicación!

    Enhorabuena por este artículo, Miguel.

    ResponderEliminar
  2. Si lo he entendido hasta yo. Estupendo. Espero con ganas los siguientes posts

    ResponderEliminar
  3. Como dicen por aqui, crystal clear ...
    Juli from London

    ResponderEliminar
    Respuestas
    1. Jajaja, tu de esto ya sabes algo Juli... cuantos IndraAdapter hemos tenido que picar juntos... jajaja.

      Eliminar