Switzerland's Citroen replica watches uk, although very good, but in replica watches popularity and Tissot is rolex replica immeasurably, but in fact it and Tissot belong to the rolex replica uk same Swatch Group, its positioning higher than the Tissot, lower than Hamilton.
Principio Open/Closed « josemiguel.torres

Principio Open/Closed


Artículo con copyright. Se permite su reproducción citando al autor. Publicado en dotNetMania

Este es el segundo de un total de cinco artículos sobre programación con los principio SOLID. Después de ver en profundidad el Principio de Responsabilidad Única, vamos a adentrarnos en conocer otro de los principios que además guarda una estrecha relación con el primero: el Principio Open/Closed.

Introducción

Todas las aplicaciones cambian durante su ciclo de vida y siempre vendrán nuevas versiones tras la primera release y 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 fallaríamos seguro y probablemente desarrollaríamos características que el cliente nunca necesitaría. El principio YAGNI (You aint’t gonna to 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 actuales requisitos funcionales, 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 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á unos cambios en cascada sobre las clases dependientes que derivará 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 de Open/Closed

El Principio Open/Closed u Open/Closed Principle en inglés, OCP en adelante, fue acuñado por el Dr. Bertrand Meyer en el año 1988 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 cuando un requisito lo hace debemos extender el comportamientos de dichas clases añadiendo código, no modificando el existente.

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

  • Son Abiertas para la extensión, es decir que la lógica o el comportamiento de la clase pueden ser extendidas en nuevas clases.
  • Son Cerradas para la modificación y por tanto el código fuente de dicha clase debería permanecer inalterado.

Sin embargo, y aparentemente, parece que ambas características sean incompatibles y eso no es así. Veamos un ejemplo de clase que rompe con OCP.

Supongamos un sistema de gestión de proyectos estilo Microsoft Project. Obviemos la complejidad real que existe en dicho sistema y centrémonos únicamente en la entidad Tareas tal y como muestra la Figura 1. Dicha clase viene determinada por los estados: Pendiente, Finalizada o Cancelada representados por el enumerador TareasEstadosEnum. Además implementa dos métodos Cancelar y Finalizar que cambian, si es posible, el nuevo estado de la tarea. En la Fuente 2 podemos la implementación del método Finalizar.

Figura 1.

1:
public
void Finalizar()

2: {

3:
switch (_estadoTarea)

4: {

5:
case TareasEstadosEnum.Pendiente:

6:
//finalizamos

7:
break;

8:
case TareasEstadosEnum.Finalizada:

9:
throw
new ApplicationException(“Ya esta finalizada”);

10:
case TareasEstadosEnum.Cancelada:

11:
throw
new ApplicationException(“Imposible finalizar. Tarea cancelada”);

12:
default:

13:
throw
new ArgumentOutOfRangeException();

14: }

15: }

Fuente 2.

Un cambio típico solicitado por el cliente de la aplicación seria la agregación de un nuevo estado para controlar las tareas que se han pospuesto, con lo que la adaptación a esta modificación seria la expuesta en la Fuente 3.

   1:
						public
								void Finalizar()

   2: {

   3:
						switch (_estadoTarea)

   4:     {

   5:
						case TareasEstadosEnum.Pendiente:

   6:
						//finalizamos
							

   7:
						break;

   8:
						case TareasEstadosEnum.Finalizada:

   9:
						throw
								new ApplicationException("Ya esta finalizada");

  10:
						case TareasEstadosEnum.Cancelada:

  11:
						throw
								new ApplicationException("Imposible finalizar. Tarea cancelada");

  12:
						case TareasEstadosEnum.Pospuesta:

  13:
						throw
								new ApplicationException("Imposible finalizar. Tarea no completada");

  14:
						default:

  15:
						throw
								new ArgumentOutOfRangeException();

  16:     }

  17: }

 

Fuente 3.

Aparentemente parece una modificación trivial, sin embargo este cambio puede replicarse en otros métodos o clases que utilicen el enumerador TareasEstadosEnum de forma que en nuestro caso también deberíamos modificar el método Cancelar (ver Fuente 4).

   1:
						public
								void Cancelar()

   2: {

   3:
						switch (_estadoTareas)

   4:     {

   5:
						case TareasEstadosEnum.Pendiente:

   6:
						//cancelamos
							

   7:             _estadoTareas = TareasEstados.Cancelada;

   8:
						break;

   9:
						case TareasEstadosEnum.Finalizada:

  10:
						throw
								new ApplicationException("Imposible cancelar. Tarea finalizada");

  11:
						case TareasEstadosEnum.Cancelada:

  12:
						throw
								new ApplicationException("Tarea ya cancelada");

  13:
						case TareasEstadosEnum.Pospuesta:

  14:
						//cancelamos
							

  15:             _estadoTareas = TareasEstados.Cancelada;

  16:
						break;

  17:
						default:

  18:
						throw
								new ArgumentOutOfRangeException();

  19:     }

  20: }

 

Fuente 4.

En definitiva, por cada nuevo estado que implementemos tenemos que identificar todas las clases que lo implementan y modificarlas tanto en la clase Tareas como en las clases lógicamente involucradas violando no únicamente OCP sino también el 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 TareasEstadosEnum, lo que elevaría la probabilidad de aparición de un nuevo bug.

Fundamentos de la Orientación a Objetos

La cuestión se centra en cómo minimizamos 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 Tareas 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 TareasEstadoEnum.Pospuesta implementaríamos un nuevo método llamado Posponer. La lógica del método 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 e implementar los métodos (Finalizar, Cerrar y Posponer) mediante los cuales definimos el comportamiento y por otro lado delegamos los métodos del objeto Tarea hacia dicha clase tal y como se muestra en la Fuente 5.

   1:
						class EstadosTareas

   2: {

   3:
						public
								virtual
										void Finalizar(EstadoTareasEnum estadoTareasEnum)

   4:     {

   5:
						switch (estadoTareasEnum) {

   6:
						case EstadoTareasEnum.Pendiente:

   7:
						//finalizamos
							

   8:
						case EstadoTareasEnum.Pospuesta:

   9:
						throw
								new ApplicationException("Imposible finalizar. Tarea no completada");

  10:                 . . . .

  11:
						default:

  12:
						throw
								new ArgumentOutOfRangeException();

  13:         }

  14:     }

  15:
					

  16:
						public
								virtual
										void Cancelar(EstadoTareasEnum estadoTareasEnum)

  17:     {

  18:
						switch (estadoTareasEnum){

  19:                 . . . .

  20:
						//cancelamos
							

  21:         }

  22:     }

  23:
					

  24:
						public
								virtual
										void Posponer(EstadoTareasEnum estadoTareasEnum)

  25:     {

  26:
						switch (estadoTareasEnum){

  27:                 . . . .

  28:
						//posponemos
							

  29:         }

  30:     }

  31: }

Fuente 5.

Pese a que hayamos extraído y aislado el estado de la entidad Tareas aún no hemos resuelto el problema; de hecho ahora hemos aislado la responsabilidad a la clase EstadosTareas, sin embargo estamos algo más cerca de la solución. Estudiemos de nuevo los estados -métodos- de la clase EstadoTareas; la lógica de cada uno está escrita en todos los métodos y por tanto se repite, es decir, todos los estados -métodos- contemplan la opción de Finalizar una tarea y en base a ello actúa de una forma y otra. La operación Posponer no podrá ejecutarse si el estado de la tarea es cancelado o la operación Cancelar únicamente podrá ejecutarse si el estado es Pendiente. A través de este razonamiento hemos detectado un patrón: un mismo contrato -los métodos- y diferentes comportamientos en base un estado. Esto en OO puede ser solucionado mediante polimorfismo. (Ver Fuente 6)

   1:
						abstract
								class EstadosTareasBase

   2: {

   3:
						protected Tareas _tarea;

   4:
					

   5:
						public
								abstract
										void Finalizar();

   6:
						public
								abstract
										void Cancelar();

   7:
						public
								abstract
										void Posponer();

   8: }

   9:
					

  10:
						class EstadoTareaPendiente : EstadosTareasBase

  11: {

  12:
					

  13:
						public
								override
										void Finalizar()

  14:     {

  15:
						//finalizamos
							

  16:     }

  17:
					

  18:
						public
								override
										void Cancelar()

  19:     {

  20:
						//cancelamos
							

  21:     }

  22:
					

  23:
						public
								override
										void Posponer()

  24:     {

  25:
						//posponemos
							

  26:     }

  27: }

  28:
					

  29:
						class EstadoTareaFinalizada : EstadosTareasBase

  30: {

  31:
					

  32:
						public
								override
										void Finalizar()

  33:     {

  34:
						throw
								new ApplicationException("Tarea ya Finalizada");

  35:     }

  36:
					

  37:
						public
								override
										void Cancelar()

  38:     {

  39:
						throw
								new ApplicationException("Imposible cancelar. Tarea finalizada");

  40:     }

  41:
					

  42:
						public
								override
										void Posponer()

  43:     {

  44:
						throw
								new ApplicationException("Imposible posponer. Tarea finalizada");

  45:     }

  46: }

  47:
					

  48:
						class EstadoTareaCancelada : EstadosTareasBase

  49: {

  50:
					

  51:
						public
								override
										void Finalizar()

  52:     {

  53:
						throw
								new ApplicationException("Imposible finalizar. Tarea Cancelada");

  54:     }

  55:
					

  56:
						public
								override
										void Cancelar()

  57:     {

  58:
						throw
								new ApplicationException("Tarea ya Cancelada");

  59:     }

  60:
					

  61:
						public
								override
										void Posponer()

  62:     {

  63:
						throw
								new ApplicationException("Imposible posponer. Tarea Cancelada");

  64:     }

  65: }

  66:
					

  67:
						class EstadoTareaPospuesta : EstadosTareasBase

  68: {

  69:
						public
								override
										void Finalizar()

  70:     {

  71:
						throw
								new ApplicationException("Imposible posponer. Tarea finalizada");

  72:     }

  73:
					

  74:
						public
								override
										void Cancelar()

  75:     {

  76:
						//cancelamos
							

  77:     }

  78:
					

  79:
						public
								override
										void Posponer()

  80:     {

  81:
						throw
								new ApplicationException("Tarea ya Pospuesta");

  82:     }

  83: }

  84:
					

  85:
						class Tareas

  86: {

  87:
						private EstadosTareasBase _estadosTareas;

  88:
					

  89:
						public Tareas()

  90:     {

  91:         _estadosTareas = new EstadoTareaPendiente();

  92:     }

  93:
					

  94:
						public
								void Finalizar()

  95:     {

  96:         _estadosTareas.Finalizar();

  97:     }

  98:
					

  99:
						public
								void Cancelar()

 100:     {

 101:         _estadosTareas.Cancelar();

 102:     }

 103:
					

 104:
						public
								void Posponer()

 105:     {

 106:         _estadosTareas.Posponer();

 107:     }

 108: }

Fuente 6.

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 (con Switch o if). Además, con esta nueva implementación hemos delegado la responsabilidad de Finalizar, Cancelar o Posponer a una nueva clase EstadosTareasBase y la hemos marcado como abstracta. La clase Tareas implementará sus propios métodos y delegará la responsabilidad a través de las clases estados que implementan EstadosTareasBase. 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 una de los estados como entidades independientes. Ante un nuevo requisito en la que intervenga un nuevo estado, lo único que deberemos hacer es crear una nueva clase que herede de EstadosTareasBase e implementar las acciones, extendiendo el comportamiento de la aplicación si comprometer el código existente.

Conclusión

Cuando hablamos 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 en dicho sistema y este mismo argumento es la línea que pretende seguir el Principio Open/Closed. La clave para la correcta aplicación de este principio es la abstracción y el polimorfismo, como hemos podido ver en el ejemplo. Pese a que conceptualmente es relativamente sencillo de comprender, no sucede lo mismo cuando se aplica.

Comments are closed.