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

28 de abril de 2011

Principio de Separación de Interfaces - SOLID

Siguiendo con los principios SOLID, en esta oportunidad vamos a ver el principio ISP, Interface Segregation Principle.

Comencemos por la definición:

Una clase cliente no debe ser forzada a depender de interfaces que no usa.

Vamos a explicar el concepto de ISP utilizando el ejemplo de Daos de sólo lectura del post anterior sobre LSP.

Supongamos que tenemos una interfaz IDao como la siguiente:

public interface IDao
{
void Insert(object entity);
void Update(object id, object entity);
void Delete(object id);
object[] GetAll();
object GetById(object id);
}


Y una clase FacturaDao que implementa IDao

public class FacturaDao : IDao
{
public void Insert(object entity)
{
//se inserta una factura
}

public void Update(object id, object entity)
{
//se actualiza una factura
}

public void Delete(object id)
{
//se elimina una factura
}

public object[] GetAll()
{
//se obtienen todas las facturas
}

public object GetById(object id)
{
//se obtiene una factura por id
}
}


Las clases que actúen como clientes de FacturaDao, dependerán de la interfaz IDao.

En el caso que quisiéramos crear una capa de acceso a datos de solo lectura, podríamos definir este otro Dao:

public class FacturaDaoReadOnly : IDao
{
public void Insert(object entity)
{
throw new DaoReadOnlyException();
}

public void Update(object id, object entity)
{
throw new DaoReadOnlyException();
}

public void Delete(object id)
{
throw new DaoReadOnlyException();
}

public object[] GetAll()
{
//se obtienen todas las facturas
}

public object GetById(object id)
{
//se obtiene una factura por id
}
}


De esta manera, las clases cliente de FacturaDaoReadOnly, también estarán dependiendo de la interfaz IDao.

Aquí es donde no se cumple por primera vez el principio ISP, ya que una capa de solo lectura no tiene porqué depender de una interfaz IDao que contiene métodos como Insert, Update y Delete. Esto no es recomendable ya que le quita claridad a nuestro diseño.

Por otra parte, también esto trae aparejado el problema de que mi clase FacturaDaoReadOnly tuvo que implementar los métodos Insert, Update y Delete lanzando una excepción DaoReadOnlyException. Lo cual, nuevamente, no es recomendable ya que si a futuro se agregan nuevos métodos a IDao (ej. DeleteAll), si no lo implemento en FacturaDaoReadOnly la misma no va a compilar.

Cómo podemos solucionar esto? Separando la interfaz de lectura de la interfaz de lectura/escritura:

public interface IDaoLectura
{
object[] GetAll();
object GetById(object id);
}


public interface IDao : IDaoLectura
{
void Insert(object entity);
void Update(object id, object entity);
void Delete(object id);
}


public class FacturaDao : IDao
{
public void Insert(object entity)
{
//se inserta una factura
}

public void Update(object id, object entity)
{
//se actualiza una factura
}

public void Delete(object id)
{
//se elimina una factura
}

public object[] GetAll()
{
//se obtienen todas las facturas
}

public object GetById(object id)
{
//se obtiene una factura por id
}
}


public class FacturaDaoReadOnly : IDaoLectura
{
public object[] GetAll()
{
//se obtienen todas las facturas
return null;
}

public object GetById(object id)
{
//se obtiene una factura por id
return null;
}
}


De esta forma estaremos cumpliendo el principio de separación de interfaces.

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

21 de enero de 2011

Principio de Substitución de Liskov - SOLID

Siguiendo con la serie sobre principios SOLID, hoy nos toca ver el principio LSP, Liskov Substitution Principle.

LSP es probablemente el principio SOLID más difícil de explicar ya que los ejemplos de la "vida real" que uno suele encontrar son escasos o muchas veces parecen describir mejor el principio OCP que LSP.

Comencemos por la definición del principio:

Métodos que referencian a clases base, deben poder utilizar objetos de clases derivadas sin necesidad de saberlo.

El ejemplo del Cuadrado y el Rectángulo

Dicho esto, vamos a empezar explicando LSP con el típico ejemplo de libro: el caso del cuadrado y el rectángulo.

Supongamos que tenemos una clase Rectangulo como la siguiente:


public class Rectangulo
{
private int _ancho;
private int _alto;

public virtual void AsignarAncho(int ancho)
{
_ancho = ancho;
}

public virtual void AsignarAlto(int alto)
{
_alto = alto;
}

public int ObtenerAncho()
{
return _ancho;
}

public int ObtenerAlto()
{
return _alto;
}

public int CalcularArea()
{
return _ancho * _alto;
}
}


Luego decidimos que un Cuadrado "es-un" rectángulo y por eso creamos la clase Cuadrado como una subclase de Rectangulo, y para asegurarnos que la forma realmente sea la de un cuadrado, hacemos override de AsignarAlto y de AsignarAncho de forma tal que siempre el alto y el ancho sean iguales.


public class Cuadrado : Rectangulo
{
public override void AsignarAncho(int ancho)
{
base.AsignarAncho(ancho);
base.AsignarAlto(ancho);
}

public override void AsignarAlto(int alto)
{
base.AsignarAlto(alto);
base.AsignarAncho(alto);
}
}


Hasta acá todo parecería correcto, pero veamos qué pasa cuando tenemos una tercer clase, en este caso un test unitario, con el siguiente código:


[TestFixture]
public class Test
{
[Test]
public void TestCalculoAreaRectangulo()
{
testAreaFiguraRectangular(new Rectangulo());
}

[Test]
public void TestCalculoAreaCuadrado()
{
testAreaFiguraRectangular(new Cuadrado());
}

private void testAreaFiguraRectangular(Rectangulo rect)
{
rect.AsignarAlto(10);
rect.AsignarAncho(2);
Assert.AreEqual(20, rect.CalcularArea());
}
}


Cuando la variable rectangulo sea una instancia de la clase Cuadrado este test va a fallar (el area del cuadrado va a ser 2*2=4 en lugar de 2*10=20).

Esto ocurre porque se ha violado el principio LSP. Nuestro test referencia a la clase base Rectangulo y al utilizar una instancia de Cuadrado (clase derivada de Rectangulo) el test comienza a fallar.

Sub-tipos vs Sub-clases y Diseño por Contrato

Una vez planteado el ejemplo viene bien mencionar un par de conceptos que ayudan a entender un poco mas cuándo se puede estar violando LSP y cuándo no.

Barbara Liskov (de ahí el nombre del principio) definió como sub-tipo a la propiedad que tiene un objeto A de poder reemplazar a otro objeto B sin que el comportamiento de los objetos que los utilizan deba ser modificado (A es un sub-tipo de B)

En nuestro ejemplo el Cuadrado no puede reemplazar al Rectangulo ya que el comportamiento de nuestro test debería ser modificado para que siga funcionando correctamente, probablemente haciendo algo así:


private void testAreaFiguraRectangular(Rectangulo rect)
{
rect.AsignarAlto(10);
rect.AsignarAncho(2);

if (rect is Cuadrado)
Assert.AreEqual(4, rect.CalcularArea());
else
Assert.AreEqual(20, rect.CalcularArea());
}


De aquí se deduce que el Cuadrado puede ser una sub-clase del Rectangulo, pero no es un sub-tipo, según la definición de Liskov. Por lo tanto, que una clase herede de otra no nos asegura el principio LSP.

Otro concepto que vale la pena mencionar es el de diseño por contrato, que se puede resumir en que los métodos de una clase declaran pre-condiciones y post-condiciones donde las pre-condiciones se deben cumplir para que el método se ejecute y, luego de la ejecución, las post-condiciones deben ser cumplidas también.

Al redefinir métodos de una clase, para no violar LSP, se deben respetar tanto las pre-condiciones como las post-condiciones definidas en la clase base, es decir, el comportamiento de una clase no debe romper ninguna de las restricciones impuestas por la clase base.

En el ejemplo del rectangulo y el cuadrado, las post-condiciones del método AsignarAncho definidas en la clase base Rectangulo son las siguientes:

  • "El ancho tiene el nuevo valor asignado"
  • "El alto se mantiene con el mismo valor que tenía antes"

La segunda post-condición no se respeta en la redefinición del método AsignarAncho de la clase Cuadrado, ya que ante un cambio en el ancho, también se modifica el alto en forma automática.

Un caso donde no se respetarían las pre-condiciones sería por ejemplo que en AsignarAncho de Cuadrado se haga algo así:


public override void AsignarAncho(int ancho)
{
if (ancho == 0)
throw new Exception("El ancho debe ser distinto de 0");

base.AsignarAncho(ancho);
base.AsignarAlto(ancho);
}


En este caso, estamos pidiendo que el ancho sea distinto de cero como pre-condición de AsignarAncho en Cuadrado, mientras que eso no se exige en la clase Rectangulo.

LSP y las interfaces

Por último vale la pena mencionar qué pasa con las interfaces. Si en lugar de heredar de una clase base con comportamiento ya definido, se implementa una interfaz, hay pre-condiciones y post-condiciones que respetar? La respuesta es: en la mayoría de los casos si las hay, y por lo general son implícitas y se desprenden del sentido común.

Por ejemplo, si en lugar de tener la clase Rectangulo, tengo una interfaz IFiguraRectangular, y dos clases que la implementan, Rectangulo y Cuadrado. Algo de lo mencionado previamente deja de tener vigencia? No sería lógico que si una clase interactúa con una variable de tipo IFiguraRectangular, la cual tiene sus métodos AsignarAncho y AsignarAlto, pueda asumir que el alto y el ancho se asignan por separado? Yo creo que sí...

Un ejemplo de la vida real

Otro ejemplo, un poco distinto, que a mi entender es mucho más visto en la vida real es el siguiente.

Supongamos que tenemos una interfaz para mis Daos como la siguiente:


public interface IDao
{
void Insert(object entity);
void Update(object id, object entity);
void Delete(object id);
object[] GetAll();
object GetById(object id);
}


Luego decido crear un Dao para mis Facturas


public class FacturaDao : IDao
{
public void Insert(object entity)
{
//se inserta una factura
}

public void Update(object id, object entity)
{
//se actualiza una factura
}

public void Delete(object id)
{
//se elimina una factura
}

public object[] GetAll()
{
//se obtienen todas las facturas
}

public object GetById(object id)
{
//se obtiene una factura por id
}
}


Hasta ahí todo bien, salvo que luego, por algún motivo necesito crear una capa de acceso a datos donde sólo pueda permitir lecturas, entonces mi Dao para Facturas debería ser como el siguiente:


public class FacturaDaoReadOnly : IDao
{
public void Insert(object entity)
{
throw new DaoReadOnlyException();
}

public void Update(object id, object entity)
{
throw new DaoReadOnlyException();
}

public void Delete(object id)
{
throw new DaoReadOnlyException();
}

public object[] GetAll()
{
//se obtienen todas las facturas
}

public object GetById(object id)
{
//se obtiene una factura por id
}
}


Aquí el problema que se presenta es que las clases que interactúen con IDao, van a tener que tener en cuenta que en algunos casos los métodos Insert, Update y Delete pueden lanzar una excepción DaoReadOnlyException, lo cual viola el principio de Liskov por los siguientes motivos:

  • No se estarían cumpliendo las post-condiciones de los métodos Insert, Update y Delete porque se debe contemplar que el Dao puede lanzar una excepción DaoReadOnlyException, lo cual es una post-condición particular de una implementación concreta del Dao, en este caso el dao readonly de Facturas.
  • No se estaría cumpliendo el concepto de diseño por contrato, ya que en ninguna parte de la interfaz IDao se sugiere que un Dao puede ser readonly.

Una posible forma de solucionar esto sería mediante la separación en distintas interfaces de las distintas acciones del Dao, por ejemplo IDaoLectura, IDaoEscritura, pero esto es algo que vamos a cubrir cuando veamos el principio de segregación de interfaces (ISP).

Conclusión

Por último y resumiendo, no son demasiadas las aplicaciones en la "vida real" que se le pueden dar al principio LSP, pero casos como el anteriormente descripto (o ligeras variaciones del mismo) se ven muy seguido, por lo tanto es un principio que vale la pena tener bien presente.

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