Continuamos con el segundo principio SOLID sobre la Programación Orientada a Objetos.
Fundamentos de la orientación a objetos
La cuestión se centra en cómo minimizar el impacto de una modificación en nuestro sistema, sin comprometer OCP; esto es, manteniendo la "simbiosis" entre las dos características del principio: abierto en extensión y cerrado en modificación.Volvamos a la entidad Tarea del ejemplo anterior. Por lo que hemos podido ver, los métodos dependen en gran medida del estado de la tarea. Así, una tarea podrá finalizarse o cancelarse dependiendo de su estado previo, pues no podremos cancelar una tarea que haya sido finalizada. De la misma forma, introduciendo el nuevo estado EstadosTarea. Pospuesta implementaríamos un nuevo método llamado Posponer, cuya lógica sería obvia: únicamente podría posponerse una tarea que estuviera en estado pendiente. En definitiva, todo gira alrededor del estado de la tarea, y por tanto el comportamiento de la misma dependerá del estado en que se encuentre. Una opción sería encapsular dicho estado en una clase auxiliar e implementar en ella los métodos Finalizar, Cancelar y Posponer, mediante los cuales definimos el comportamiento, tal y como se muestra en el listado 4, para luego delegar los métodos del objeto Tarea hacia dicha clase.
Listado 4
class EstadosTareaHelper
{
public virtual void Finalizar(EstadosTarea estado)
{
switch ( estado) {
case EstadosTarea.Pendiente:
// finalizamos
case EstadosTarea.Pospuesta:
throw new ApplicationException("Imposible finalizar. Tarea no completada");
default:
throw new ArgumentOutOfRangeException();
}
}
public virtual void Cancelar(EstadosTarea estado)
{
switch (estado) {
// ...
// cancelamos
}
}
public virtual void Posponer(EstadosTarea estado)
{
switch (estado) {
// ...
// posponemos
}
}
}
Pese a que hayamos extraido y aislado el estado de la entidad Tarea, aun no hemos resuelto el problema. De hecho, ahora hemos aislado la responsabilidad en la clase EstadosTareaHelper; sin embargo, estamos algo mas cerca de la solucion. Estudiemos de nuevo los estados -metodos- de la clase Estados- TareaHelper. La logica de cada accion esta escrita en todos los metodos y por tanto se repite; es decir, todos los metodos contemplan la opcion de Finalizar una tarea, y en base a ello actuan de una forma u otra. La operacion Posponer no podra ejecutarse si el estado de la tarea es Cancelada, y la operacion Cancelar unicamente podra ejecutarse si el estado es Pendiente. A traves de este razonamiento, podemos detectar un patron: un mismo contrato .los metodos. y diferentes comportamientos en base a un estado. Esto en OO puede ser solucionado mediante polimorfismo, como se muestra en el listado 5.
Listado 5
abstract class EstadoTareaBase
{
protected Tarea _tarea;
public abstract void Finalizar();
public abstract void Cancelar();
public abstract void Posponer();
}
class EstadoTareaPendiente : EstadoTareaBase
{
public override void Finalizar()
{
// finalizamos
}
public override void Cancelar()
{
// cancelamos
}
public override void Posponer()
{
// posponemos
}
}
class EstadoTareaFinalizada : EstadoTareaBase
{
public override void Finalizar()
{
throw new ApplicationException("Tarea ya finalizada");
}
public override void Cancelar()
{
throw new ApplicationException("Imposible cancelar. Tarea finalizada");
}
public override void Posponer()
{
throw new ApplicationException("Imposible posponer. Tarea finalizada");
}
}
class EstadoTareaCancelada : EstadoTareaBase
{
public override void Finalizar()
{
throw new ApplicationException("Imposible finalizar. Tarea cancelada");
}
public override void Cancelar()
{
throw new ApplicationException("Tarea ya cancelada");
}
public override void Posponer()
{
throw new ApplicationException("Imposible posponer. Tarea cancelada");
}
}
class EstadoTareaPospuesta : EstadoTareaBase
{
public override void Finalizar()
{
throw new ApplicationException("Imposible posponer. Tarea finalizada");
}
public override void Cancelar()
{
// cancelamos
}
public override void Posponer()
{
throw new ApplicationException("Tarea ya pospuesta");
}
}
class Tarea
{
private EstadoTareaBase _estadoTarea;
public Tarea()
{
_estadoTarea = new EstadoTareaPendiente();
}
public void Finalizar()
{
_estadoTarea.Finalizar();
}
public void Cancelar()
{
_estadoTarea.Cancelar();
}
public void Posponer()
{
_estadoTarea.Posponer();
}
}
Básicamente, lo que hemos hecho es crear una clase por cada estado en lugar de tener una única clase cuyos métodos están basados en sentencias condicionadas por el estado de la tarea (switch o if). Además, con esta nueva implementación hemos delegado la responsabilidad de finalizar, cancelar o posponer a una nueva clase EstadoTareaBase que hemos marcado como abstracta. La clase Tarea implementará sus propios métodos y delegará la responsabilidad a través de las clasesestados que heredan de EstadoTareaBase. Debido a que la clase Tarea gira en torno a un estado, asumimos que el estado inicial por defecto es Pendiente, y así lo especificamos en el constructor, instanciando EstadoTareaPendiente.
En realidad, hemos aplicado un patrón ya conocido, el patrón de diseño State, ya que el comportamiento de la clase cambia dependiendo del estado, en este caso, de la tarea, y por lo tanto hemos abstraído cada uno de los estados como entidades independientes. Ante un nuevo requisito en el que intervenga un nuevo estado, lo único que deberemos hacer es crear una nueva clase que herede de EstadoTareaBase e implementar los métodos virtuales, extendiendo así el comportamiento de la aplicación sin comprometer el código existente.
Conclusión
Cuando hablábamos el mes pasado del Principio de Responsabilidad Única, argumentamos la importancia de que cada clase tuviera una y solo una responsabilidad dentro del sistema, de forma que cuanto menos impacto tenga una clase en el conjunto global del sistema, menos repercusión global tendrá una modificación de la clase en dicho sistema. Este mismo argumento es la línea que pretende seguir el Principio Open/Closed, que pese a ser relativamente sencillo de comprender conceptualmente, no sucede lo mismo cuando se aplica. Las claves para la correcta aplicación de este principio son la abstracción y el polimorfismo, como hemos podido ver en el ejemplo.José Miguel Torres
MVP de Device Application Development