domingo, 12 de enero de 2020

Capítulo 19: Política y nivel - Clean Architecture


Los sistemas de software son declaraciones de política. De hecho, todos los programas son una detallada descripción de una política en los cuales las entradas son transformadas en salidas.

En sistemas no triviales la política se puede dividir en distintas partes de manera que habrá ciertas declaraciones que definen una regla de negocio, el formateo de unos datos, la validación de unos datos, etc.

Parte del arte de desarrollar una arquitectura software es separar cuidadosamente aquellas políticas de otras agrupandolas en base a la manera en la que cambian. Las políticas que cambian por la misma razón y al mismo tiempo están al mismo nivel y deberían pertenecer al mismo componente. Las políticas que cambian por diferentes razones o en diferentes tiempos están en diferentes niveles y deberían estar separados en distintos componentes. 

El arte de la arquitectura a veces implica agrupar los componentes en un gráfico acíclico dirigido. Los nodos del gráfico son los componentes que contienen políticas al mismo nivel. Los bordes dirigidos son las dependencias entre esos componentes.  Estas conectan componentes que están en distintos niveles.

Dependiendo del lenguaje de programación las dependencias pueden ser import, require, use, etc. Estas son las dependencias que son necesarias para que funcione el sistema.

En una buena arquitectura, la dirección de aquellas dependencias está basada en el nivel de componentes que ellos conectan. En cada caso, los componentes de bajo nivel están diseñados de manera que estos dependan de los componentes de alto nivel. 
¿Qué es el nivel?
La estricta definición de nivel es la distancia de los «inputs» y los «outputs». Cuando más lejos esté la política del los «inputs» y los «outputs» más alto será su nivel. Las políticas que administran los «inputs» y los «outputs» son las políticas con el nivel más bajo en el sistema.

El siguiente diagrama muestra un simple programa de encriptación que lee caracteres desde un dispositivo de entrada, traduce esos caracteres usando una tabla para más tarde escribir esos caracteres traducidos en el dispositivo de salida. Los flujos de datos se muestran como una línea curva sólida. Las dependencias de código fuente diseñadas propiamente se muestran como líneas discontinuas rectas.
El componente «Translate» es el componente de más alto nivel dado que es el que más lejos está de los «inputs» y «outputs». 

Se puede apreciar que los flujos de datos y las dependencias de código no siempre apuntan en la misma dirección. Esto, de nuevo, es parte de la arquitectura del software. Se quiere que las dependencias de código fuente estén desacopladas del flujo de datos y acopladas al nivel.

Sería fácil crear una arquitectura incorrecta escribiendo el programa de la siguiente manera.

function encrypt() {  
    while(true)    
        writeChar(translate(readChar())); 

Esta es una arquitectura incorrecta dado que la función «encrypt» depende de las funciones de bajo nivel «WriteChar» y «ReadChar».

Una arquitectura más flexible podría ser la mostrada en la siguiente figura
Esta arquitectura marca un límite entre la clase «Encrypt» y las interfaces «Char Reader» y «Char Writer». Todas las dependencias apuntan hacia el interior del límite. Esta unidad es la más  de más alto nivel en este sistema.

Las clases «Console Reader» y «Console Writer» son las clases de más bajo nivel dado que son las que más cerca están a los «inputs» y «outputs». 

Se puede apreciar que con esta estructura se desacopla la política de encriptación de alto nivel con la política de entrada y salida de bajo nivel. Esto lleva a que la política de encriptación se pueda usar en un amplio abanico de contextos. Cuando los cambios se producen en la política de entrada y la salida de datos no influyen en la política de encriptación.

Hay que recordar que las políticas se agrupan en componentes según la forma en la que cambian. Las políticas que cambian por la misma razón y al mismo tiempo deberían estar juntas por el «Single Responsibility Principle» y «Common Closure Principle». 

Las políticas de alto nivel tienden a cambiar menos frecuentemente y por razones más importantes que las políticas de bajo nivel mientras que las políticas de bajo nivel tienden a cambiar más frecuentemente, con más urgencia y por razones no tan importantes.
Por ejemplo, en el caso del sistema de encriptación es más probable que cambien los dispositivos de entrada y salida que el algoritmo de encriptación cambie. Si el algoritmo de encriptación cambia será más probable que haya una razón sustancial que un cambio en uno de los dispositivos.

Mantener separadas estas políticas con todas las dependencias del código apuntando a las políticas de alto nivel reduce el impacto de los cambios. Los cambios en las políticas de bajo nivel tendrán poca o ningún impacto en las políticas de alto nivel. 

Otra manera de enfocar este problema es establecer que los componentes de bajo nivel sean plugins de los componentes de alto nivel. El siguiente diagrama muestra esta ordenación. 

El componente «Encryption» no debería conocer nada sobre el componente «IO Devices» mientras que el componente «IO Devices» depende del componente «Encryption».
Conclusión
En este punto, una discusión de políticas ha envuelto una mezcla de principio de responsabilidad única (SRP), el principio Abierto-Cerrado (OCP), el principio de cierre común (CCP), el principio de inversión de dependencias (DIP), el Principio de Dependencias Estable (SDP) y el Principio de Abstracción Estable (SAP). Se puede revisar y verificar donde se usa cada principio.

No hay comentarios:

Publicar un comentario