-
Las aplicaciones crecen en tamaño y complejidad
-
Se requiere algún tipo de organización
-
La clase es de un grano demasiado fino como unidad de organización
-
En UML, Java y otros lenguajes se da el concepto de paquete (package)
-
En general puede hablarse de "agrupaciones lógiclas de declaraciones que pueden importarse en otros programas" (componentes)
- ¿Mejor criterio de partición?
- ¿Qué relaciones hay entre paquetes y qué principios de diseño gobiernan su uso?
- ¿Los paquetes deben diseñarse antes que las clases, o al revés?
- ¿Cómo se representan físicamente los paquetes?
- ¿Para qué usaremos esos paquetes?
La arquitectura de un sistema software es la forma dada al sistema por aquellos que lo construyen. La forma es la división del sistema en componentes, la disposición de esos componentes y la manera en que se comunican entre sí.
Bob Martin
La finalidad de la arquitectura es facilitar el desarrollo, despliegue, operación y mantenimiento del sistema software.
Al pricipio de la OO, el principio LSP nació ligado al uso de la herencia para el diseño software. Hoy día, se considera más ligado al diseño de interfaces e implementaciones:
- Una interfaz Java implementada por varias clases
- Varias clases Ruby con las mismas signaturas
- Un conjunto de servicios con la misma interfaz ReST
LSP es aplicable porque habrá usuarios que dependan de interfaces bien definidas y en la posibilidad de sustitución de sus implementaciones.
- Un servicio agregador de taxis (uber, cabify, etc)
- La URI con que se llama al taxi se guarda en la base de datos en formato ReST:
purplecab.com/driver/Bob
- El agregador añade la información de pedido y hace PUT a la URI:
purplecab.com/driver/Bob /pickupAddress/24 Maple St. /pickupTime/153 /destination/ORD
- Todos los taxis de diferentes compañías deben cumplir la misma interfaz ReST.
- La compañía ACME abrevia el destino con
dest
- Ahora la petición hay que construirla siguiendo una reglas distintas para cada conductor:
if (driver.getDispatchUri().startsWith("acme.com")) ...
- Pero meter "acme.com" en el código puede ser origen de fallos y brechas de seguridad. El arquitecto define entonces un módulo adicional que decide el formato en función de la URI:
URI | Dispatch format |
---|---|
acme.com |
pickupAddress/%s/pickupTime/%s/dest/%s |
*.* |
pickupAddress/%s/pickupTime/%s/destination/%s |
La LSP debería extenderse hasta el nivel arquitectónico. Una violación de la posibilidad de sustitución puede contaminar la arquitectura de un sistema con mecanismos extra.
ISP = es pernicioso depender de módulos que contienen más de lo que se necesita. Esto es cierto también a nivel arquitectónico
Supongamos que un sistema
Si
- los cambios en esas características de
$D$ forzarán un redespliegue de$F$ y de$S$ ; - los fallos en
$D$ causarán fallos en$F$ y$S$ .
Depender de algo que trae "equipaje" que no se necesita puede ser origen de problemas.
REP = Reuse/Release Equivalente Principle CCP= Common Closure Principle CRP= Common Reuse Principle
The granule of reuse is the granule of release. Only components that are released through a tracking system can be effectively reused.
Robert C. Martin
¿Se reutiliza código cuando se hace copia-pega? ¡No!
- Se reutiliza código si y sólo si no hace falta mirar el código fuente más allá de la parte pública del componente/biblioteca (ficheros de cabecera, etc.)
- Hay muchas herramientas de gestión de módulos y paquetes (v.g. Maven, Apache Ivy, RubyGems, etc.) que permiten reutilizar componentes y bibliotecas creadas por otros
- El código a reutilizar se debe tratar como un producto en sí mismo, que no es mantenido ni distribuido por el consumidor o cliente, sino por el autor (o alguien delegado) responsable.
- No se puede reutilizar algo que no haya sido liberado/entregado.
- Un cliente de una biblioteca liberada lo es de toda la biblioteca, no de parte de ella.
- El grano de reutilización no puede ser menor que el grano de entrega
Reunir en componentes aquellas clases que cambian por los mismos motivos y al mismo tiempo. Separar en componentes distintos aquellas clases que cambian en momentos distintos y por motivos diferentes.
Robert C. Martin
The classes in a package should be closed together against the same kinds of changes. A change that affects a package affects all the classes in that package.
Robert C. Martin
La mantenibilidad suele ser más importante que la reusabilidad
- Si hay que cambiar el código, ¿dónde es mejor que estén los cambios? ¿en un mismo componente, o repartidos por varios componentes?
CCP es un intento de tener junto todo lo que es probable que cambie por una misma razón
- Es el SRP de los componentes
- CCP está conectado con OCP
Como no es posible garantizar al 100% el cierre de OCP... el cierre debe ser estratégico
- CCP amplía la estrategia de cierre, juntando en un mismo componente todas las clases cerradas para un mismo tipo de cambios.
No obligar a los usuarios de un componente a depender de cosas que no necesitan
Robert C. Martin
The classes in a package are reused together. If you reuse one of the classes in a package, you reuse them all.
Robert C. Martin
¿Qué clases ubicar en cada paquete?
- Las clases casi no se usan por separado, sino como parte de una abstracción reutilizable (de colaboración entre clases)
- La abstracción donde ubicar dichas clases debe ser un mismo componente
Ejemplo: contenedor + iteradores
Cuando alguien decide usar un paquete, se crea una dependencia sobre todo el paquete.
- No es bueno tener que revalidar una aplicación porque hay que liberar o entregar una versión del paquete provocada por cambios hechos en clases que no importaban.
- CRP dice más sobre qué clases no deben ir juntas que sobre las que deben ir juntas.
CRP es la versión genérica del ISP:
- ISP aconseja no depender de clases con métodos que no usamos
- CRP aconseja no depender de componentes con clases que no usamos
- REP y CCP son inclusivos: incrementan el tamaño de los componentes
- CRP es exclusivo: reduce el tamaño de los componentes
Cada arco describe el coste de abandonar el principio del vértice opuesto:
- ¿Hay demasiados componentes cambiando al mismo tiempo?
$\implies$ consolidar mejor las clases (CCP) - ¿Hay demasiadas versiones/entregas?
$\implies$ minimizar las dependencias (CRP) - ¿Es difícil reutilizar el código?
$\implies$ mejorar los artefactos que se liberan (REP)
La postura en el triángulo de tensiones debe ser coyuntural, pues las preocupaciones del equipo de desarrollo pueden cambiar con el tiempo.
- CCP es más importante que REP al principio de un desarrollo (cuando liberar es más importante que reutilizar) - YAGNI
ADPP: Acyclic Dependencies Principle
There must be no cycles in the coimponent dependency graph
Robert C. Martin
El grafo de dependencias entre components debe ser un DAG (Directed Acyclic Graph)
Hay que particionar el entorno de desarrollo en componentes "liberables" por separado.
Varios equipos de trabajo distribuidos pueden hacer cambios a cada componente por separado, lo que puede inducir nuevas dependencias entre componentes.
- La estructura es un DAG:
@startuml
skinparam component {
backgroundColor white
borderColor #FF6655
FontName Consolas
ArrowColor #FF6655
}
[Main] as Main
[Database] as Database
[View] as View
[Presenters] as Presenters
[Interactors] as Interactors
[Controllers] as Controllers
[Authorizer] as Authorizer
[Entities] as Entities
Main --> Database
Main --> Presenters
Main --> View
View --> Presenters
Main --> Interactors
Main --> Controllers
Main --> Authorizer
Presenters --> Interactors
Authorizer --> Interactors
Database --> Interactors
Database --> Entities
Interactors --> Entities
Controllers --> Interactors
@enduml
-
¿A quién afecta la liberación de una nueva versión de
Presenters
por sus responsables?
SoloMain
yView
se ven afectadas -
¿A quiénes afecta la ejecución de un test en
Presenters
?
Interactors
yEntities
(deberían ser mocked) -
¿En qué orden se libera una nueva versión del sistema?
Entities
,Database
&Interactors
,Presenters
,View
&Controllers
&Authorizer
- Un cambio de requisitos fuerza a introducir una nueva dependencia
Entities
$\rightarrow$ Authorizer
(por ejemplo, la claseEntities::User
usa la claseAutohizer::Permissions
)
Problemas:
- Los desarrolladores de
Database
saben que para liberarla, deben hacerla compatible conEntities
Database
debe ser ahora compatible también conAuthorizer
- Pero
Authorizer
depende deInteractors
1️⃣ Database
es mucho más difícil de liberar, porque:
Entities
+Authorizer
+Interactors
se convierten en un solo componente- Los cambios que haga cualquier responsable de alguno de estos tres componentes afectarán al resto
2️⃣
- si se quiere probar
Entities
, antes hay que construir e integrarAuthorizer
yInteractors
(más mocks!)
Dos posibles mecanismos:
- Aplicar el principio DIP
- Crear una interfaz con los métodos que necesita la clase
User
- Incluir la interfaz en
Entities
- Heredar de la interfaz desde
Authorizer
- Crear una interfaz con los métodos que necesita la clase
- Crear un componente nuevo (
Permissions
) del que dependan ambos (Authorizer
yEntities
)
La segunda alternativa puede provocar jitters (lo que hacen los gremlins) en presencia de requisitos cambiantes: la estructura de dependencia jitters and grows
Leer más sobre continuous delivery y herramientas de continuous integration:
- Ejemplo: Jenkins, etc.