Principios de Diseño Orientado a Objetos

Señales de un diseño orientado a objetos deficiente

Si el código de un sistema/software empieza a exhibir algunos de las siguientes características, es una clara señal de que su diseño está empezando a deteriorarse; en estas circunstancias es altamente recomendable reorganizar el código de acuerdo a los principios de diseño detallados en el presente artículo a la brevedad posible mucho antes de que estos problemas empiecen a propagarse por  todo  el sistema.

Rigidez. Un diseño es rígido cuando es difícil de cambiar. La dificultad usualmente se manifiesta por que es necesario cambiar varios archivos en diversos módulos o capas del sistema; mientras más módulos hay que tocar para implementar el cambio,  más rígido es el sistema. Por ejemplo, si en un sistema de ventas un cambio en la forma de guardar los datos implica un cambio en el módulo que calcula el importe del impuesto de la venta, el diseño es rígido.

Fragilidad. Un diseño es frágil cuando un cambio provoca que el sistema falle en varios módulos incluso en aquellos que no están conceptualmente relacionados al cambio realizado.

Inmovilidad. Un diseño es inmóvil cuando contiene partes que podrían ser útiles en otros sistemas pero el esfuerzo y riesgo necesarios para separar dichas partes del resto del sistema son muy elevados.

Viscosidad. Un diseño es viscoso cuando es muy difícil realizar cambios que preserven el diseño o las buenas prácticas de desarrollo. Por ejemplo, si en el sistema mencionado es necesario cargar datos de prueba e iniciar una BD o un servidor web para probar la lógica del cálculo del impuesto de una venta, los desarrolladores tratarán de evitar escribir dichas pruebas. En un sistema viscoso es muy complicado “hacer lo correcto”.

Complejidad innecesaria. Un diseño tiene este rasgo cuando contiene elementos que no se necesitan (métodos, clases, interfaces, relaciones entre clases, etc.) Usualmente es resultado sobre-diseñar el sistema con la intención facilitar la implementación de futuros cambios en los requerimientos. En general, es recomendable cambiar y ajustar el diseño sólo cuando haya una evidencia real de que el diseño actual es insatisfactorio.

Repetición innecesaria. Bloques de código casi idéntico dispersos  en diversos archivos incrementan significativamente el esfuerzo necesario para implementar cambios o corregir errores y son una señal de que hay una abstracción que no está adecuadamente capturada en el diseño y un excesivo uso de la función “copiar y pegar” del editor.

Opacidad. Un diseño es opaco/turbio cuando es difícil de entender. Es recomendable realizar los esfuerzos necesarios para mantener la expresividad y claridad del código al máximo de manera permanente.

Estas señales o criterios de calidad de diseño están relacionadas; por ejemplo, un sistema opaco es usualmente rígido: la poca claridad del código dificulta la implementación de cambios. Finalmente, la forma usual de abordar  estos problemas sigue este esquema general:

  1. Una vez que se evidencia la presencia de algunos de los síntomas de un diseño deficiente, es necesario identificar la causa del problema en términos de los principios de diseño que se han violado.
  2. Una vez identificadas las causas del problema, se reorganiza el código aplicando los refactorings y/o patrones de diseño apropiados de manera tal que se eliminen los síntomas identificados en el paso anterior.

El principio de única responsabilidad (SRP)

Una clase debería tener un solo motivo para cambiar. Por ejemplo, si la clase “Rectangulo” se encarga de calcular el área o perímetro y de mostrar la figura en algún dispositivo de salida, es probable que tenga muchos motivos para cambiar; podría cambiar la forma en la que se muestra y la gestión de sus atributos geométricos.. Combinar varias responsabilidades en la misma clase implica al menos las siguientes desventajas:

  • Dificulta la compresión del código y por lo tanto su mantenimiento.
  • Dificulta la reutilización de código; por ejemplo, una aplicación interesada únicamente en los aspectos geométricos estaría obligada a depender indirectamente de librerías relacionadas con la presentación en algún dispositivo de salida.
  • Genera acoplamientos innecesarios. Si realiza un cambio o actualización en la forma de presentación incluso las aplicaciones que no emplean esa funcionalidad se verán afectadas por el cambio

srp-potencial-violacion

Más aún, los requerimientos de cambio podrían llegar de fuentes y en momentos distintos. Para el caso del “Rectangulo” la gestión de la presentación en dispositivos de salida y el cálculo de sus atributos geométricos deberían residir en clases separadas:

srp-potencial-solucion

Aunque existen escenarios de separación de responsabilidades aceptados mayoritariamente por la industria como la separación de la presentación, lógica/reglas de negocio y persistencia de datos, en muchas ocasiones ésta separación podría no ser tan obvia. En estos casos se recomienda aplicar el principio SRP únicamente cuando existe evidencia de que una clase tiene más de una responsabilidad; es decir, cuando efectivamente se tengan requerimientos que impliquen cambios sólo en ciertos aspectos funcionales de una clase y estos requerimientos provengan de diferentes fuentes en momentos diferentes.

El principio abierto/cerrado (OCP)

Las entidades de software (clases, módulos, funciones, etc.) deberían estar abiertos para su extensión pero cerradas para su modificación.

Cuando este principio se aplica de manera apropiada, idealmente cualquier extensión al funcionamiento del sistema se implementa aumentando código no modificando código existente. Es decir, una entidad que sigue el principio OCP permite la extensión de su funcionalidad sin que esto implique la modificación de su código fuente.

La manera usual de lograr este objetivo es mediante la creación de mecanismos de abstracción que permitan separar el comportamiento esperado de cierta entidad de los detalles concretos de su implementación. Las clases abstractas e interfaces son los mecanismos de abstracción definen puntos de extensión; las extensiones se proveen en nuevos archivos de código fuente. Los patrones de diseño “Strategy” y “Template Method” son estrategias ampliamente utilizadas para satisfacer el principio OCP.

Por ejemplo, una clase que accede directamente a la funcionalidad de otra clase difícilmente estará cerrada a los cambios en la clase que usa.

ocp-potencial-violacion

Si en cambio, el cliente accede a funcionalidad de la clase por medio de una abstracción (una interfaz en este caso) siempre que la interfaz no cambie la clase cliente está cerrada a los cambios en las clases que implementan la interfaz.

ocp-potencial-solucion

Un aspecto muy importante a tomar en cuenta sobre el principio OCP es que no es posible cerrar una entidad a todos posibles cambios que pueden estar implícitos en los cambios de requerimientos del sistema. Entonces, para no complicar innecesariamente el diseño será necesario incluir las abstracciones que nos protejan de cambios que efectivamente han sucedido.

Principio de inversión de dependencias (DIP)

Dependencias

Entre las entidades de los sistemas de software existen dos tipos de dependencias:

Las dependencias de ejecución. Cuando una entidad A (clase o método) emplea los servicios (datos o métodos) de una entidad B cuando el programa se está ejecutando, A depende de B en tiempo de ejecución o, dicho de otra manera, B es una dependencia en tiempo de ejecución requerida por A.

Las dependencias de compilación. Cuando el código fuente de una entidad A (clase o método) menciona los servicios (datos o métodos) de una entidad B, A depende de B en tiempo de compilación, dicho de otra manera, B es una dependencia en tiempo de compilación requerida por A. Como consecuencia de esta dependencia, B debe compilarse antes que A y el resultado de la compilación de B debe estar presente para compilar A; cuando el código fuente de B se modifica, B debe compilarse nuevamente antes de compilar A.

Para facilitar el desarrollo y despliegue independiente de los diversos módulos de un sistema deben gestionarse adecuadamente las dependencias en tiempo de compilación.

Inversión de las dependencias

En los lenguajes no orientados a objetos usualmente las dependencias de tiempo de ejecución tenían el mismo sentido que las dependencias en tiempo de compilación. Una de las mayores ventajas de los lenguajes orientados a objetos es que facilitan, gracias al polimorfismo, invertir las dependencias de manera que el sentido de las dependencias de compilación vaya en el sentido contrario a las dependencias en tiempo de ejecución.

La posibilidad de invertir fácilmente las dependencias de compilación es el mecanismo fundamental que hace posible establecer el Principio de Inversión de Dependencias que formalmente establece que:

El código que implementa reglas/lógica de alto nivel (como los casos de uso de una aplicación) no debe tener dependencias de compilación con el código que implementa detalles de bajo nivel (como el mecanismo para persistir el estado de los objetos); el código que implementa detalles de bajo nivel debe depender del código de las reglas/lógica de alto nivel tal cual se nuestra en la siguiente figura (donde además se muestra que la inversión ocurre en los límites entre diferentes módulos):

dip-esquema

Entre las mayores ventajas de aplicar este principio de diseño están:

  • La posibilidad de desarrollar los módulos con los detalles de implementación de manera independiente una vez definidas las abstracciones en los módulos de alto nivel.
  • Es posible cambiar los módulos con los detalles de implementación sin modificar los módulos de alto nivel; por ejemplo, cambiar el almacenamiento en archivos por almacenamiento en una BD.

Referencias

[1] Agile Principles, Patterns, and Practices in C#

[2] Code as Design: Three Essays by Jack W. Reeves

[3] The Clean Architecture

[4] Introducción a Clean Architecture

[5] The 7 Virtues of Good Software Design

[6] 6 Reasons to Start Managing Technical Debt in 2021

Comments are closed.