La forma correcta de versionar y establecer dependencias

earroyoron

Ernesto λrroyo

Posted on September 1, 2019

La forma correcta de versionar y establecer dependencias

A la hora de definir la estructura de un proyecto la forma de establecer dependencias y relaciones es un paso crítico. Debes conocer y respetar algunas reglas importantes y, cuando no las cumplas, debes ser consciente de porque has tomado esa decisión y los posibles problemas que enfrentaras.

Montar un proyecto "de cualquier forma" puede llevar a problemas de dependencias cruzadas, extraños flujos de construcción, código repetido por no poder reutilizar o incluso, a que un servicio dependa de cosas de las que no debería provocando errores posteriores muy complicados de resolver.

En este artículo uso las definiciones del libro "Clean Architecture" de
Robert C. Martin".

Tres principios

  • Mantener coherencia en el versionado del despliegue
  • Mantener un conjunto común (common-closure)
  • El principio de la reutilización

Mantener la equivalencia por uso en el despliegue

(Reuse/Release Equivalence Principle)

The granule of reuse is the the granule of release

Como primer principio general debemos decir que "las clases y módulos que van en el mismo despligue deben tener la misma versión y también deben tenerse juntos en la misma versión de la documentación porque eso mantiene la coherencia para el autor y los usuarios".

Debes observar que estoy hablando de un "principio" de forma que no es una ley cerrada, y puede tener sus excepciones pero ten en cuenta que las excepciones al Principio de uso y despliegue se ven rapidamente y "cantan" mucho, así que debes estar preparado para saber explicar tus razones para hacerlo de esa forma.

Si construyes un proyecto maven donde el parent es:

io.earroyoron:parent:1.0.0
|
 \__ io.earroyoron:moduleA:1.3.3
| 
 \__ io.earroyoron:moduleB:1.5.0

pero que incluye dos módulos y que al contruirlo se generan dos artefactos:

io.earroyoron:moduleA:1.3.3
y
io.earroyoron:moduleB:1.5.0

estás incumpliendo este bello principio y montando un hermoso cacao de versionado.

Porque si tienes un cambio en el móduloB tendrás que desplegar una versión nueva del módulo B (quizás 2.0.0 porque era un cambio que rompia compatibilidad) y ¿qué versión tendrás en el módulo parent? ¿mantienes como 1.0.0? no tiene sentido porque moduloB está en un major distinto ¿y 1.x.x? no, el módulo A no está en ese major version

En general, y salvo casos muy, muy, pero muy claros, el parent y sus hijos deben tener la misma version:

io.earroyoron:parent:1.0.0
|
 \__ io.earroyoron:moduleA:<parent-version>
| 
 \__ io.earroyoron:moduleB:<parent-version>

Mantener un conjunto común

(Common Closure Principle)

Gather into components those classes that change for the same reason and the same times. Separate into different components those classes that change at different times for different reasons

Puesto que para la mayoría de las aplicaciones es más importante el mantenimiento y la facilidad de despliegue que la reutilización es mejor tener en un sitio común el código que queremos cambiar.

No es buena idea que un cambio en un módulo X nos obligue a contruir tres librerias que hemos construido de forma aislada cuando ese cambio sólo afecta al módulo X y es provocado por un requisito del módulo X. Lo correcto es que el contenido de ese otro módulo esté junto en el mismo sitio.

Volviendo a nuestro anterior ejemplo, moduleA y moduleB están juntos en el mismo sitio y bajo el mismo parent porque cambian a la vez de versión por la misma razón. Es decir, dado que un cambio en moduleA de la versión 1.3.4 a, digamos, la versión 1.4.0 obliga a que el móduloB también pase a la versión 1.4.0 este segundo principio nos dice que es correcto porque el cambio afecta a ambos módulos y su versión.

Separar para priorizar la reutilización

Don't force users of a component to depend on things they don't need

El tercer principio es contrario a los dos anteriores. Basicamente indica que separes en módulos diferentes las cosas que son diferentes para que los clientes que declaran una determinada dependencia sólo se traigan lo que realmente requieren.

O dicho de otro modo, no tener que importar un enorme librería de 7 terabytes si sólo necesito una función muy concreta de esa librería.

Es un principio contrario a los anteriores porque nos está empujando a romper y separar en muchas piezas nuestros componentes para tener un grano muy fino a la hora de declarar las dependencias.

En el ejemplo, si otro módulo necesita importar moduleA pero sólo necesita una mínima parte de él quizas la estructura debería separar en otro módulo esa pequeña parte para facilitar la reutilización:

io.earroyoron:parent:1.0.0
|
 \__ io.earroyoron:module-min-A1:<parent-version>
|
 \__ io.earroyoron:module-A2:<parent-version>
| 
 \__ io.earroyoron:moduleB:<parent-version>

El problema es que llevado al extremo este principio genera más problemas en los despliegues que los beneficios que trae por reutilización.

Tensiones

Los 3 principios o reglas establecen un modelo de lucha y tensión.

Tensiones

  • Si nos enfocamos a separar en muchos componentes pequeños (el 3er principio) agrupados en un versionado común de despliegue (1) vamos estar sacando versiones innecesarias de librerias que no lo necesitan ante el más mínimo y superfluo cambio en alguno de sus elementos:

Demasiado enfoque en reutilizar:

io.earroyoron:parent:1.0.0
|
 \__ io.earroyoron:module-A:<parent-version>
|
 \__ io.earroyoron:module-B1:<parent-version>
|
 \__ io.earroyoron:module-B2:<parent-version>
|
 \__ io.earroyoron:module-C1:<parent-version>
|
 \__ io.earroyoron:module-C2:<parent-version>
|
 \__ io.earroyoron:module-D:<parent-version>
|
 \__ io.earroyoron:module-E1:<parent-version>
| 
 \__ io.earroyoron:module-E2:<parent-version>
  • Si obviamos el principio de mantener el versionado coherente en el despliegue y sólo nos enfocamos en los otros dos principios (2,3)d nuestras releases tendrán versiones "de su padre y de su madre" y haremos muy complicado el modelo de dependencias.
io.earroyoron:parent:1.0.0
|
 \__ io.earroyoron:module-A:1.2.3
|
 \__ io.earroyoron:module-B1:2.5.3
|
 \__ io.earroyoron:module-B2:2.5.3
|
 \__ io.earroyoron:module-C1:2.0.0
|
 \__ io.earroyoron:module-C2:2.0.0
|
 \__ io.earroyoron:module-D:1.1.1
|
 \__ io.earroyoron:module-E1:1.1.1
| 
 \__ io.earroyoron:module-E2:1.1.1
  • Finalmente, si sólo nos centramos en mantener un versionado coherente (1) y agrupar las cosas (2) vamos a tener más complicado o imposible la reutilización por terceros de nuestros componentes.
io.earroyoron:parent:1.0.0
|
 \__ io.earroyoron:module-A:<parent-version>
|
 \__ io.earroyoron:module-B:<parent-version>

La mayoría de los casos deben primar los principios de versionado común y agrupar por responsabilidades comunes (1 y 2) y dejar la reutización para más adelante ya que suele ser un anti-patrón de optimización prematura que lleva a muchos problemas.

💖 💪 🙅 🚩
earroyoron
Ernesto λrroyo

Posted on September 1, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related