> Manuales > Principios fundamentales de la Programación Orientada a Objetos

Este es el segundo de una serie de cinco principios SOLID y su aplicación en la Programación Orientada a Objetos.

Después de examinar en nuestra entrega anterior el Principio de Responsabilidad Única, este mes nos adentramos en otro de los principios, que además guarda una estrecha relación con el primero: el Principio Open/Closed.

odas las aplicaciones cambian durante su ciclo de vida, y siempre vendrán nuevas versiones tras la primera release. No por ello debemos adelantarnos a desarrollar características que el cliente podría necesitar en el futuro; si nos pusiéramos en el papel de adivinos, seguramente fallaríamos y probablemente desarrollaríamos características que el cliente nunca necesitará. El principio YAGNI ("You Ain’t Gonna Need It" o "No vas a necesitarlo"), utilizado en la Programación Extrema, previene de implementar nada más que lo que realmente se requiera. La idea es desarrollar ahora sobre los requisitos funcionales actuales, no sobre los que supongamos que aparecerán dentro de un mes.

La actitud de adelantarnos a los acontecimientos es un mecanismo de defensa que en ocasiones acusamos los desarrolladores para prevenir lo que tarde o temprano será inevitable: la modificación. Lo único que podemos hacer es minimizar el impacto de una futura modificación en nuestro sistema, y para ello es imprescindible empezar con un buen diseño, ya que la modificación de una clase o módulo de una aplicación mal diseñada generará cambios en cascada sobre las clases dependientes que derivarán en unos efectos indeseables. La aplicación se convierte, así, en rígida, impredecible y no reutilizable.

Ahora bien, ¿cómo debemos plantear nuestras aplicaciones para que se mantengan estables ante cualquier modificación?

El Principio Open/Closed

El Principio Open/Closed (Open/Closed Principle, OCP) fue acuñado por el Dr. Bertrand Meyer en su libro "Object Oriented Software Construction" y afirma que:
Una clase debe estar abierta a extensiones, pero cerrada a las modificaciones.

OCP es la respuesta a la pregunta que hacíamos anteriormente, ya que argumenta que deberíamos diseñar clases que nunca cambien, y que cuando un requisito cambie, lo que debemos hacer es extender el comportamiento de dichas clases añadiendo código, no modificando el existente.

Las clases que cumplen con OCP tienen dos características:

Podría parecer que ambas características son incompatibles, pero eso no es así. Veamos un ejemplo de una clase que rompe con OCP. Supongamos un sistema de gestión de proyectos al estilo de Microsoft Project. Obviemos de momento la complejidad real que existe en dicho sistema, y centrémonos únicamente en la entidad Tarea, tal y como muestra la figura 1. Dicha clase viene determinada por uno de los estados Pendiente, Finalizada o Cancelada, representados mediante la enumeración EstadosTarea. Además, la clase implementa dos métodos, Cancelar y Finalizar que cambian, si es posible, el estado de la tarea. En el listado 1 podemos ver la implementación inicial del método Finalizar.

Listado 1
public void Finalizar()
{
   switch (_estadoTarea)
   {
      case EstadosTarea.Pendiente:
      // finalizamos
      break;
      case EstadosTarea.Finalizada:
      throw new ApplicationException("Tarea ya finalizada");
      case EstadosTarea.Cancelada:
      throw new ApplicationException("Imposible finalizar. Tarea cancelada");
      default:
      throw new ArgumentOutOfRangeException();
   }
}

Un cambio típico solicitado por el cliente de la aplicación sería la adición de un nuevo estado para controlar las tareas que se han pospuesto, con lo que la adaptación a esta modificación podría ser la expuesta en el listado 2. Aparentemente, parece una modificación trivial; sin embargo, este cambio puede replicarse en otros métodos o clases que utilicen la enumeración EstadosTarea, de forma que en nuestro caso también deberíamos modificar el método Cancelar (listado 3).

Listado 2
public void Finalizar()
{
   switch (_estadoTarea)
   {
      case EstadosTarea.Pendiente:
      // finalizamos
      break;
      case EstadosTarea.Finalizada:
      throw new ApplicationException("Tarea ya finalizada");
      case EstadosTarea.Cancelada:
      throw new ApplicationException("Imposible finalizar. Tarea cancelada");
      case EstadosTarea.Pospuesta:
      throw new ApplicationException("Imposible finalizar. Tarea no completada");
      default:
      throw new ArgumentOutOfRangeException();
   }
}

Listado 3
public void Cancelar()
{
   switch (_estadoTarea)
   {
      case EstadosTarea.Pendiente:
      // cancelamos
      _estadoTarea = EstadosTarea.Cancelada;
      break;
      case EstadosTarea.Finalizada:
      throw new ApplicationException("Imposible cancelar. Tarea finalizada");
      case EstadosTarea.Cancelada:
      throw new ApplicationException("Tarea ya cancelada");
      case EstadosTarea.Pospuesta:
      // cancelamos
      _estadoTarea = EstadosTarea.Cancelada;
      break;
      default:
      throw new ArgumentOutOfRangeException();
   }
}

En definitiva, por cada nuevo estado que implementemos tendremos que identificar todas las clases que lo utilizan (tanto la clase Tarea como las clases lógicamente involucradas) y modificarlas, violando no únicamente OCP sino también el Principio DRY ("Don’t Repeat Yourself", "No te repitas"), otro principio que pretende reducir al máximo cualquier tipo de duplicación. En este tipo de modificaciones existe una alta probabilidad de olvidar modificar algún método relacionado con el nuevo estado implementado en el enumerador EstadosTarea, lo que elevaría la probabilidad de aparición de un nuevo bug.

En el siguiente artículo sobre el principio Open/Closed continuamos con la explicación y finalizamos con un ejemplo.

José Miguel Torres

MVP de Device Application Development

Manual