El enfoque de diseño por capas en Go
(jerf.org)- Como el lenguaje Go prohíbe estrictamente las referencias circulares entre paquetes, esto lleva de forma natural a un diseño por capas (layered design)
- Este artículo explica la estructura en capas que los proyectos Go terminan teniendo por obligación y sostiene que ya es suficientemente válida, incluso sin imponer una arquitectura adicional encima
- Cuando aparecen dependencias circulares, presenta estrategias de refactorización concretas y prácticas para resolverlas paso a paso
- Cada paquete se diseña para tener una unidad funcional con significado propio, lo que también favorece las pruebas, el mantenimiento y la separación en microservicios
- En consecuencia, este enfoque evita el problema frecuente en el diseño de código de "querías un plátano y te trajeron toda la selva"
Enfoque de diseño por capas en Go
Principios básicos
- Go prohíbe las referencias circulares entre paquetes
- La relación de imports de todo programa Go debe formar un grafo acíclico dirigido (DAG)
- Esta estructura no es una opción, sino una regla de diseño impuesta a nivel del lenguaje
Formación automática de capas de paquetes
- Excluyendo los paquetes externos, los paquetes internos del proyecto pueden organizarse automáticamente en capas según la profundidad de sus referencias
- Como en la figura de abajo, en la base se ubican paquetes utilitarios esenciales como metrics, logging y estructuras de datos compartidas
- Después, los paquetes superiores van combinando funcionalidad y apilándose hacia arriba
Características de este enfoque de diseño
- Las capas se basan en la dirección de las referencias, no en una abstracción jerárquica
- Un paquete puede referenciar varios paquetes de niveles inferiores
- Enfoques de diseño existentes como MVC o la arquitectura hexagonal también pueden "aplicarse" sobre esta estructura
→ Eso sí, siempre hay que considerar las restricciones estructurales de Go
Estrategias para resolver referencias circulares
Si aparece una referencia circular, intenta refactorizar en este orden:
1. Mover funcionalidad
- La opción más recomendada
- Analiza con precisión la funcionalidad que provoca el ciclo y muévela al lugar lógicamente adecuado
- No se usa con mucha frecuencia, pero es la que más mejora la claridad conceptual
2. Separar la funcionalidad compartida en otro paquete
- Mueve a un tercer paquete los tipos o funciones que ambos lados usan en común (
Username, por ejemplo) - Aunque el paquete parezca pequeño, sepáralo sin miedo
→ Con el tiempo, es muy probable que ese paquete crezca
3. Crear un paquete superior de composición
- Crea un tercer paquete que combine los dos paquetes que están en ciclo
- Ejemplo: separar en un paquete superior la dependencia bidireccional entre
CategoryyBlogPost
→ Los paquetes inferiores se mantienen como dumb struct, y la funcionalidad real se compone en el paquete superior
4. Introducir interfaces
- Sustituye la dependencia por una interfaz que solo tenga los métodos que necesita la estructura o función
- Esto elimina dependencias innecesarias y mejora la facilidad de prueba
- Pero si se usa en exceso, el diseño puede volverse más complejo
5. Copiar (Copy)
- Si aquello de lo que dependes es muy pequeño, simplemente cópialo y úsalo
- Puede parecer una violación de DRY, pero en muchos casos en la práctica ayuda a aclarar el diseño
6. Unirlo en un solo paquete
- Si ninguno de los métodos anteriores es posible, fusiona los dos paquetes
- Es aceptable siempre que no se convierta en un paquete demasiado grande
→ Aun así, evita fusionar por inercia y tómalo con cuidado
Ventajas prácticas de este enfoque de diseño
- Cada paquete tiene una unidad funcional significativa por sí misma y puede probarse de manera independiente
- Como las referencias dentro del paquete están limitadas, es posible entender un paquete individual sin comprender todo el código
- Evita conexiones de dependencias globales no intencionales (= el problema de la selva) y fomenta escribir código que use solo lo necesario
- También es fácil de extraer al separar microservicios
→ La mayoría de las dependencias ya están definidas con claridad
Conclusión
- Las restricciones de diseño de paquetes en Go no son una molestia, sino un mecanismo que guía hacia un buen diseño
- Incluso sin una arquitectura especial, una estructura sólida puede lograrse solo con la forma en que se referencian los paquetes entre sí
- El análisis cuidadoso y las estrategias de refactorización frente a referencias circulares son útiles no solo en Go, sino también en otros lenguajes
4 comentarios
Es divertido cuando lo armas rápido y funciona,
pero cuando empiezas a meter tests,
te pones a pensar por qué lo hiciste así en ese momento.
La frase "quería una banana, pero trajo toda la jungla" está buenísima.
Creo que una de las cosas más difíciles al desarrollar con Spring era la dependencia circular..
Esa frustración de que se inicialicen mutuamente de forma infinita y terminen colapsando por una fuga de memoria...
Opiniones en Hacker News
No permitir dependencias circulares es una excelente decisión de diseño al construir programas grandes
Es una excelente entrada de blog
Una técnica extra relacionada con el consejo de "moverlo a un tercer paquete"
Parece que está leyendo un libro sobre el método estructurado de Yourdon
Los paquetes no pueden referenciarse circularmente entre sí
go:linknameMe recuerda al concepto concreto de randomizer
Una característica curiosa de Golang es que no puede tener dependencias circulares a nivel de paquetes, pero sí en
go.modEs una buena explicación de cómo Jerf piensa sobre los paquetes y cómo maneja las dependencias circulares