Principios de una arquitectura limpia: mantenible y testeable

Estoy convencido de que si te dedicas a programar, conoces a Robert Uncle Martin. Su libro Clean Code es uno de los más recomendados en la lista de libros que todo desarrollador debería leer. Martin, con sus cosas buenas y malas, es uno de los desarrolladores más influyentes del panorama ingenieril. Fuerte defensor de TDD, de la cobertura de tests y otras buenas prácticas, y además de esto cuenta con bastantes personas que prosiguen sus enseñanzas a rajatabla.

Últimamente, Bob Martin, ha publicado un nuevo libro llamado Clean Architecture. ¿Mas qué se comprende por arquitectura limpia?

Clean Code

Como comentaba ya antes, Clean Code es un libro muy aconsejable, que desgrana ciertas ideas esenciales para poder redactar código limpio.

El código limpio es aquel código que está estructurado de forma compresible, que es claro en sus pretensiones, simple de leer, que es de manera fácil mantenible y que está testado. En el libro se marchan dando ciertas ideas para lograr redactar código limpio, hablando de principios SOLID, de la relevancia de dar nombres a variables y clases etcétera En turincon.netDev ya hemos hablado del libro y de sus ideas en alguna ocasión

Principios de una arquitectura limpia

Si bien seamos capaces de redactar código limpio, podemos toparnos que al medrar nuestro sistema, la arquitectura del mismo sea un lastre. Y es que no es exactamente lo mismo redactar código limpio para un proyecto fácil, que para un proyecto complejo compuesto de múltiples componentes obligados a colaborar. En ocasiones las arquitecturas son demasiado complejas, nos fuerzan a reiterar código, o bien nos hacen tener demasiadas dependencias entre componentes, ocasionándonos muchos inconvenientes.

Los conceptos de cohesión y acoplamiento, asimismo pueden aplicarse a nivel de arquitectura.

Si usáis programación orientada a objetos, seguro que conocéis los conceptos de cohesión y acoplamiento. Esos conceptos asimismo pueden aplicarse de forma semejante a los componentes de un sistema, así sean dlls o bien ficheros jar, estos deben colaborar unos con otros. Y la forma en la que colaboren, pueden hacer un sistema fallar. Mas si proseguimos una serie de principios para supervisar estas 2 variables, nuestra arquitectura va a ser más limpia y manejable.

Cohesión

The Reuse/Release Equivalence Principle: que nos afirma que los componentes deben poder ser desplegados de forma independiente sin afectar a el resto. Las clases, o bien código que van en ese componente, deben tener una relación, y por ende deben poderse desplegar conjuntamente.
The common closure principle: podría decirse que charlamos del principio de responsabilidad única (SRP) aplicado a componentes. La idea es reunir clases que puedan mudar por exactamente la misma razón en un componente. Si debemos hacer un cambio, y hay que tocar múltiples componentes, esto va a suponer tener que desplegarlos todos, en vez de solo uno.
The common reuse principle: este principio nos habla de eludir a aquellos que usan un componente depender de cosas que no precisan. Si un componente depende de otro, hay que procurar que sea pues precisa todas y cada una de las clases que lo componen. Lo opuesto nos forzará a trabajar más en el momento en que nos toque hacer el despliegue. Así va a ser más simple volver a usar componentes.
Lograr cumplir estos 3 principios al unísono es algo difícil, con lo que en ocasiones hay que admitir compromisos. Por servirnos de un ejemplo es común sacrificar un tanto la reusabilidad, para lograr que los componentes sean simples de desplegar.

Acoplamiento

The Acyclic Dependencies Principle: si trazamos líneas entre los componentes para representar las dependencias entre ellos, debemos procurar que no existan ciclos. O sea, que el cambio en un componente, no acabe provocando en la necesidad de hacer cambios en cadena en el resto componentes, que fuercen a regresar a alterar el componente inicial. Cuando eso sucede, es bastante difícil lograr una versión estable del sistema, en tanto que hay que hacer multitud de cambios en los diferentes componentes hasta el momento en que todo vuelve a marchar.
The stable dependencies Principle: todo sistema tiende a mudar y evolucionar, mas no todos y cada uno de los componentes cambian con exactamente la misma frecuencia, ni es igualmente simple alterarlos. Este principio nos afirma que un componente que cambia frecuentemente no debería depender de otro que es bastante difícil alterar, en tanto que entonces va a ser asimismo bastante difícil de alterar.
The stable Abstractions Principle: este principio nos afirma que si un componente de nuestro sistema va a mudar poco en tanto que es bastante difícil alterarlo, ha de estar compuesto mayoritariamente por interfaces y clases abstractas. De este modo el componente va a ser sencillamente extensible, y no afectará tanto al resto de la arquitectura.

Características de una arquitectura limpia

Aparte de cumplir los principios previamente descritos, una arquitectura limpia se caracteriza por:

Independiente de los frameworks. Los frameworks habrían de ser herramientas, y no obligarnos a actuar de una determinada forma debido a sus limitaciones.
Testable. Debemos poder probar nuestras reglas de negocio sin meditar en base de datos, interfaz gráfica o bien otros componentes no esenciales de nuestro sistema.
Independiente de la UI. Si la UI cambia frecuentemente esto no puede afectar al resto de nuestro sistema, que debe ser independiente.
Independiente de la base de datos. Deberíamos poder mudar de Oracle, a SQL Server, a MongoDB, a Casandra o bien a cualquier otra base de datos sin que afectase demasiado a nuestro sistema.
Independiente de cualquier entidad externa. No deberíamos saber nada de entidades externas, con lo que no vamos a deber depender de ellas.
Todas y cada una estas peculiaridades, conforme Bob Martin, se reúnen en el próximo gráfico:

Partes de una arquitectura limpia

Entidades

Las entidades son las que incluyen las reglas de negocio críticas para el sistema. Estas entidades pueden ser usadas por diferentes componentes de la arquitectura, con lo que son independientes, y no deben mudar como consecuencia de otros elementos externos.

Una entidad va a deber abarcar un término crítico para el negocio, y deberemos separarlo lo más posible del resto de conceptos. Esa entidad va a recibir los datos precisos, y efectuará operaciones sobre ellos para lograr el propósito deseado.

Casos de uso

En un caso así nos hallamos con las reglas de negocio aplicables a una aplicación específica. Estos casos de empleo prosiguen un flujo para lograr que las reglas definidas por las entidades se cumplan. Los casos de empleo, solo definen como se comporta nuestro sistema, definiendo los datos de entrada precisos, y como va a ser su salida. Los cambios en esta capa no deberían afectar a las entidades, del mismo modo que los cambios en otras capas externas no deberían afectar a los casos de empleo.

Es esencial que no pensemos en como los datos que produce un caso de empleo van a ser presentados al usuario. No vamos a deber meditar en código HTML, o bien en SQL. Un caso de empleo recibe datos estructurados y devuelve más datos estructurados.

Adaptadores de interfaz

Los datos generados por los casos de empleo y las entidades, deben convertirse en algo comprensible por la próxima capa que los va a usar y de eso se encarga esta capa. Pensando en MVC por poner un ejemplo, los controladores y las vistas, pertenecerían a esta capa, y el modelo, serían los datos que se pasan entre los casos de empleo y los controladores para entonces poder presentar las vistas.

Lo mismo aplicaría para por servirnos de un ejemplo, presentar información a un servicio externo, en tanto que en esta capa definiríamos la forma en la que los datos de las capas internas se presenta al exterior.

Frameworks y drivers

En la capa más externa es, como afirma Bob Martin, donde van los detalles. Y la base de datos es un detalle, nuestro framework web, es un detalle etcétera

Fronteras o bien límites

Una frontera (o bien como afirman los aglosajones, boundaries) es una separación que definimos en nuestra arquitectura para dividir componentes y acotar dependencias. Estas fronteras debemos decidir dónde ponerlas, y en qué momento ponerlas. Esta resolución es esencial puesto que puede condicionar el buen desempeño del proyecto. Una mala resolución sobre los límites puede complicar el desarrollo de nuestra aplicación o bien su mantenimiento futuro.

Una mala resolución sobre los límites entre componentes puede complicar el desarrollo de nuestra aplicación o bien su mantenimiento futuro

Por poner un ejemplo, podemos sentirnos tentados de meditar que las reglas de negocio deben poder guardar información de manera directa en la base de datos. Como ya hemos visto ya antes, la base de datos es un detalle, con lo que esto deberíamos evitarlo. En ese punto deberíamos trazar una frontera. Nuestras reglas de negocio, se comunicarían siempre y en todo momento con una interfaz, sin saber nada sobre la base de datos. La base de datos en cambio, si va a saber cosas sobre las reglas de negocio, puesto que debe convertir los datos en sentencias SQL que puedan guardar la información.

Otra ventaja auxiliar de este enfoque, es que podemos retrasar ciertas resoluciones. Podemos comenzar a desarrollar todas y cada una nuestras reglas de negocio, sin tomar en consideración su persistencia, puesto que esa parte se efectúa por medio de una interfaz. Primero podemos usar objetos en memoria, y conforme avancemos, ir agregando sistemas más complejos. Al final vamos a poder seleccionar entre utilizar una base de datos relacional, NoSQL, o bien aun guardar la información en ficheros.

En suma, debemos meditar en nuestro sistema, como un sistema de complementos, de manera que los componentes estén apartados y podamos substituir unos por los otros sin demasiados inconvenientes.

Las fronteras de una arquitectura limpia

En el esquema de arquitectura limpia que hemos visto previamente, podemos ver dónde se han trazado las fronteras o bien límites. Entre entidades y casos de empleo, hay una frontera. Lo mismo con los adaptadores de interfaz, o bien los frameworks y drivers. Las fronteras son esenciales, por el hecho de que incorporarlas cuando no las precisamos pude crearnos muchos inconvenientes, mas no incorporarlas cuando las precisamos pude producir otros tantos (agregarlas después, es siempre y en todo momento es considerablemente más costoso).

La separación en fronteras es esencial, mas mucho más esencial es la administración que hagamos de las dependencias entre estas capas. Para esto hay siempre y en todo momento que continuar la regla de las dependencias.

La regla de las dependencias

Esta regla es fundamental, puesto que sin ella, nuestra arquitectura no sería más que un bonito diagrama. Las capas interiores de una arquitectura limpia, no deben saber nada de las capas exteriores. Por servirnos de un ejemplo la capa de entidades, no puede saber de la existencia de los casos de empleo, y los casos de empleo no deben saber nada de la existencia de los adaptadores de interfaz. De esta manera las dependencias están bajo control y van siempre y en todo momento en un sentido.

Estructuras de datos simples

En el momento de traspasar una frontera, vamos a deber usar estructuras de datos simples, eludiendo usar conceptos como DatabaseRows o bien afines. Pensando en los casos de empleo, estos deben percibir estructuras de datos como datos de entradas, y deben devolver estructuras de datos como salida. Como afirmaba ya antes, no nos interesa que un caso de empleo tenga conocimientos sobre HTML o bien SQL. Lo opuesto nos lleva a una falta de independencia, con todo cuanto eso acarrea (despliegue, actualización, tests etcétera)

Las capas interiores de una arquitectura limpia, no deben saber nada de las capas exteriores

A veces al pasar datos a los casos de empleo, podemos meditar que es buena idea emplear las entidades como datos de entrada o bien salida. Al fin y al postre comparten mucha información. Mas esto no deja de ser un fallo, en tanto que si bien al comienzo la información parezca afín, en el futuro los casos de empleo y las entidades cambiarán de muy, muy diferentes formas, obligándonos a tratar con la dependencia que hemos creado.

Fronteras parciales

En ocasiones, por motivos de organización y mantenimiento, nos interesa crear fronteras parciales. Esta clase de fronteras las debemos planear de forma afín a una frontera real, mas en vez de empaquetarla en un componente apartado, la dejamos que forme una parte de otro componente. De esta forma nos ahorramos una parte del esmero de crear un componente nuevo, que no estamos seguros de que vaya a precisarse. Conseguimos ciertas de sus ventajas, dejando todo preparado por si acaso es preciso dar ese último paso.

Conclusión

Si bien es posible que no sea la parte más esencial de un proyecto de software, la arquitectura juega siempre y en todo momento