18 de noviembre de 2010

Principio Abierto-Cerrado - SOLID

Continuando con la serie prometida de posts sobre principios SOLID, hoy voy a presentar el principio OCP, Open-Closed Principle.

La definición del principio dice lo siguiente:

Las entidades de software (clases, módulos, métodos, etc.) deben estar abiertas para extensión, pero cerradas para modificación.

Dicho de otra forma, esas entidades deben estar diseñadas de tal manera que su comportamiento pueda ser alterado o extendido sin modificar el código fuente, logrando así que la entidad de software quede protegida contra el cambio.

Dicho así puede sonar extraño, pero para entender más fácilmente este principio hay que tener en cuenta dos cosas, la primera es la más obvia y es qué es lo que quiero proteger?, todo un assembly, una clase determinada o un método particular? y la segunda y no tan obvia es, de qué lo quiero proteger?

Voy a presentar un ejemplo basado en la clase Factura de mi post previo sobre SRP que, espero, sirva para aclarar un poco.

Supongamos que mi clase Factura tiene un método Enviar que se encarga de enviar una copia de la factura al cliente y otra copia al sector de administración, pero estas copias son enviadas de distintas formas (por correo, por email, por fax, etc.)

Por un lado tengo un enumerado que me indica el método de envío:


public enum MetodoEnvio
{
eMail,
Correo,
Fax
}


Por otro lado, siguiendo el principio de responsabilidad única (SRP) tengo clases que se encargan de realizar el envío, los despachadores:


public class CorreoDespachadorFacturas
{
public CorreoDespachadorFacturas(Destino destino)
{
// guardar destino en un field
}

public void Despachar(Factura factura)
{
throw new NotImplementedException();
}
}

public class EMailDespachadorFacturas
{
public EMailDespachadorFacturas(Destino destino)
{
// guardar destino en un field
}

public void Despachar(Factura factura)
{
throw new NotImplementedException();
}
}

public class FaxDespachadorFacturas
{
public FaxDespachadorFacturas(Destino destino)
{
// guardar destino en un field
}

public void Despachar(Factura factura)
{
throw new NotImplementedException();
}
}


Y finalmente tengo mi clase Factura y un ejemplo simple para invocar al método Enviar de la factura:


public class Factura
{
public void Enviar(MetodoEnvio metodoEnvioCopiaCliente, Destino destinoCopiaCliente,
MetodoEnvio metodoEnvioCopiaAdministracion, Destino destinoCopiaAdministracion)
{
enviarCopia(metodoEnvioCopiaCliente, destinoCopiaCliente);
enviarCopia(metodoEnvioCopiaAdministracion, destinoCopiaAdministracion);
}

private void enviarCopia(MetodoEnvio metodoEnvio, Destino destino)
{
switch (metodoEnvio)
{
case MetodoEnvio.eMail:
var despachadorEMail = new EMailDespachadorFacturas(destino);
despachadorEMail.Despachar(this);
break;
case MetodoEnvio.Correo:
var despachadorCorreo = new CorreoDespachadorFacturas(destino);
despachadorCorreo.Despachar(this);
break;
case MetodoEnvio.Fax:
var despachadorFax = new FaxDespachadorFacturas(destino);
despachadorFax.Despachar(this);
break;
}
}

//..
}

public class EjemploUsoFactura
{
public void EnviarFacturas(IEnumerable<factura> facturas)
{
foreach (var factura in facturas)
factura.Enviar(MetodoEnvio.Correo, new Destino { /* domicilio del cliente */ },
MetodoEnvio.eMail, new Destino { /* domicilio de la administracion */ });
}

//...
}


Respondamos las dos preguntas planteadas más arriba:

1) Qué quiero proteger?

Quiero proteger el método Enviar de la clase Factura

2) De qué quiero proteger al método Enviar?

Para soportar nuevos métodos de envío que se agreguen en el futuro, tal como está diseñado el método Enviar, debería abrir su código y agregar un nuevo case al switch, por lo tanto no estaría cumpliendo OCP, por lo tanto la respuesta sería "Quiero protegerlo ante el agregado de nuevos métodos de envío"

Una forma de hacer esto es creando una interfaz IDespachadorFacturas y haciendo que esta interfaz sea implementada por todos mis despachadores concretos:


public interface IDespachadorFacturas
{
void Despachar(Factura factura);
}

public class CorreoDespachadorFacturas : IDespachadorFacturas
{
public CorreoDespachadorFacturas(Destino destino)
{
// guardar destino en un field
}

#region IDespachadorFacturas Members

public void Despachar(Factura factura)
{
throw new NotImplementedException();
}

#endregion
}

public class EMailDespachadorFacturas : IDespachadorFacturas
{
public EMailDespachadorFacturas(Destino destino)
{
// guardar destino en un field
}

#region IDespachadorFacturas Members

public void Despachar(Factura factura)
{
throw new NotImplementedException();
}

#endregion
}

public class FaxDespachadorFacturas : IDespachadorFacturas
{
public FaxDespachadorFacturas(Destino destino)
{
// guardar destino en un field
}

#region IDespachadorFacturas Members

public void Despachar(Factura factura)
{
throw new NotImplementedException();
}

#endregion
}


De esta forma nos abstraemos de cuál sea el método de envío ya que simplemente desde la Factura "hablaremos" con la interfaz IDespachadorFacturas:


public class Factura
{
public void Enviar(IDespachadorFacturas despachadorFacturaCopiaCliente,
IDespachadorFacturas despachadorFacturaCopiaAdministracion)
{
despachadorFacturaCopiaCliente.Despachar(this);
despachadorFacturaCopiaAdministracion.Despachar(this);
}

//...

}

public class EjemploUsoFactura
{
public void EnviarFacturas(IEnumerable<factura> facturas)
{
IDespachadorFacturas despachadorCopiaCliente =
new CorreoDespachadorFacturas(new Destino { /* domicilio del cliente */ });
IDespachadorFacturas despachadorCopiaAdministracion =
new EMailDespachadorFacturas(new Destino { /* domicilio de la administracion */ });

foreach (var factura in facturas)
factura.Enviar(despachadorCopiaCliente, despachadorCopiaAdministracion);
}

//...
}


Podríamos terminar aquí ya que hemos cumplido en proteger al método Enviar del agregado a futuro de nuevos métodos de envío, pero miremos un poco más como ha quedado nuestro diseño:


public void Enviar(IDespachadorFacturas despachadorFacturaCopiaCliente,
IDespachadorFacturas despachadorFacturaCopiaAdministracion)
{
despachadorFacturaCopiaCliente.Despachar(this);
despachadorFacturaCopiaAdministracion.Despachar(this);
}


Qué pasaría si en el futuro se presenta la necesidad de enviar una tercer copia de la factura a Tesorería? el método Enviar no está protegido contra este cambio, ya que tendría que abrir el código fuente del método y agregar algo así:


public void Enviar(IDespachadorFacturas despachadorFacturaCopiaCliente,
IDespachadorFacturas despachadorFacturaCopiaAdministracion,
IDespachadorFacturas despachadorFacturaCopiaTesoreria)
{
despachadorFacturaCopiaCliente.Despachar(this);
despachadorFacturaCopiaAdministracion.Despachar(this);
despachadorFacturaCopiaTesoreria.Despachar(this);
}


Para solucionar este problema podríamos hacer lo siguiente:


public class Factura
{
public void Enviar(IEnumerable<idespachadorfacturas> despachadoresFacturas)
{
foreach (var despachador in despachadoresFacturas)
despachador.Despachar(this);
}

//...
}

public class EjemploUsoFactura
{
public void EnviarFacturas(IEnumerable<factura> facturas)
{
IDespachadorFacturas despachadorCopiaCliente =
new CorreoDespachadorFacturas(new Destino { /* domicilio del cliente */ });
IDespachadorFacturas despachadorCopiaAdministracion =
new EMailDespachadorFacturas(new Destino { /* domicilio de la administracion */ });
IDespachadorFacturas despachadorCopiaTesoreria =
new FaxDespachadorFacturas(new Destino { /* domicilio de la tesoreria */ });

var despachadores = new List<idespachadorfacturas>
{ despachadorCopiaCliente, despachadorCopiaAdministracion, despachadorCopiaTesoreria };
foreach (var factura in facturas)
factura.Enviar(despachadores);
}

//...
}


Y de esta forma tendremos protegido al método Enviar de la factura ante dos posibles cambios:
  • El agregado de nuevos métodos de envío
  • La necesidad de enviar más copias a otros destinatarios.

Pero todavía hay un poco más, hay otra forma de solucionar este último problema que se nos presentó y es aplicando un patrón de diseño llamado Composite.

Creamos un nuevo despachador (el Composite) que va a contener todos los despachadores que queramos utilizar para enviar facturas, y pasamos ese despachador al método Enviar.

Luego será el despachador composite el encargado de llamar al método Despachar de cada despachador:


public class CompositeDespachadorFacturas : IDespachadorFacturas
{
public void AgregarDespachador(IDespachadorFacturas despachador)
{
_despachadores.Add(despachador);
}

public void EliminarDespachador(IDespachadorFacturas despachador)
{
throw new NotImplementedException();
}

private readonly IList<idespachadorfacturas> _despachadores = new List<idespachadorfacturas>();

#region IDespachadorFacturas Members

void IDespachadorFacturas.Despachar(Factura factura)
{
foreach (IDespachadorFacturas despachador in _despachadores)
despachador.Despachar(factura);
}

#endregion
}



public class Factura
{
public void Enviar(IDespachadorFacturas despachadorFacturas)
{
despachadorFacturas.Despachar(this);
}

//...
}

public class EjemploUsoFactura
{
public void EnviarFacturas(IEnumerable<factura> facturas)
{
CompositeDespachadorFacturas composite = new CompositeDespachadorFacturas();

IDespachadorFacturas despachadorCopiaCliente =
new CorreoDespachadorFacturas(new Destino { /* domicilio del cliente */ });
composite.AgregarDespachador(despachadorCopiaCliente);

IDespachadorFacturas despachadorCopiaAdministracion =
new EMailDespachadorFacturas(new Destino { /* domicilio de la administracion */ });
composite.AgregarDespachador(despachadorCopiaAdministracion);

IDespachadorFacturas despachadorCopiaTesoreria =
new FaxDespachadorFacturas(new Destino { /* domicilio de la tesoreria */ });
composite.AgregarDespachador(despachadorCopiaTesoreria);

foreach (var factura in facturas)
factura.Enviar(composite);
}

//...
}


Cuál es la ventaja de esta última solución? de esta forma el método enviar de la factura se abstrae al máximo posible de la forma en que se despachan las mismas, es decir, la factura sabe que tiene que hablar con un despachador que tiene un único método Despachar, luego la forma en que se despache la factura, los destinos a los cuáles será enviada y también la cantidad de veces que la misma será despachada es totalmente transparente para la factura.
Esto minimiza la posibilidad de que en el futuro algún cambio a la funcionalidad de mi aplicación impacte de forma tal, que tenga que abrir el código del método enviar de la factura y modificarlo.

Por último, es importante destacar que la decisión de aplicar OCP sobre un método, clase, etc. debe ser una decisión estratégica basándonos en nuestra experiencia, ya que sería imposible intentar cerrar por completo nuestras entidades de software. Será en aquellos casos donde se detecte que hay una gran probabilidad de cambio donde tendremos que aplicar OCP para mejorar la mantenibilidad de nuestro sistema.

En mi post anterior 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.