Cómo estructurar una aplicación Go, diseñada de acuerdo con la arquitectura limpia

9

Estoy tratando de construir un proyecto usando la arquitectura limpia, como se describe aquí . Encontré un gran artículo sobre cómo hacer esto en Go .

El ejemplo es muy simple, y el autor coloca su código en paquetes nombrados en función de la capa en la que se encuentran. Me gusta la idea del tío Bob de que la arquitectura de una aplicación debe comunicar claramente su intención . Por lo tanto, me gustaría que mi aplicación tenga paquetes de nivel superior basados ​​en áreas de dominio. Entonces mi estructura de archivos se vería así:

/Customers
    /domain.go
    /interactor.go
    /interface.go
    /repository.go
/... the same for other domain areas

El problema con esto es que varias capas comparten el mismo paquete. Por lo tanto, no está del todo claro cuando se viola la regla de dependencia, porque no tiene importaciones que muestren qué depende de qué.

Vengo de un fondo de Python, donde esto no sería un gran problema, porque puedes importar archivos individuales, por lo que customers.interactorpodría importar customers.domain.

Podríamos lograr algo similar en gO al anidar paquetes, de modo que el paquete de los clientes contenga un paquete de dominio y un paquete de interacción, y así sucesivamente. Esto se siente torpe, y los paquetes con nombres idénticos pueden ser molestos de tratar.

Otra opción sería hacer múltiples paquetes por área de dominio. Uno llamado customer_domain, otro llamado customer_interactor, etc. Pero esto también se siente sucio. No encaja bien con las pautas de nomenclatura de paquetes de Go, y parece que todos estos paquetes separados deberían agruparse de alguna manera, ya que sus nombres tienen un prefijo común.

Entonces, ¿cuál sería un buen diseño de archivo para esto?

gran ciego
fuente
Como alguien que ha sufrido bajo una arquitectura que organizó paquetes exclusivamente por nivel arquitectónico, permítame expresar mi apoyo a los paquetes basados ​​en funciones . Estoy a favor de comunicar la intención, pero no me hagas arrastrarme por todos los rincones oscuros solo para agregar una nueva característica.
candied_orange
Por intención, quiero decir que los paquetes deben comunicar de qué se trata la aplicación. Entonces, si está creando una aplicación de biblioteca, tendría un paquete de libros, un paquete de prestamistas, etc.
bigblind

Respuestas:

5

Hay algunas soluciones a esto:

  1. Paquete de separación
  2. Análisis de revisión
  3. Análisis estático
  4. Análisis de tiempo de ejecución

Cada uno tiene sus pros / contras.

Paquete de separación

Esta es la forma más fácil que no requiere construir nada extra. Viene en dos sabores:

// /app/user/model/model.go
package usermodel
type User struct {}

// /app/user/controller/controller.go
package usercontroller
import "app/user/model"
type Controller struct {}

O:

// /app/model/user.go
package model
type User struct {}

// /app/controller/user.go
package controller
import "app/user/model"

type User struct {}

Sin embargo, esto rompe la totalidad del concepto de User. Para entender o modificar Usernecesita tocar varios paquetes.

Sin embargo, tiene una buena propiedad que es más obvia cuando se modelimporta controller, y en cierta medida se aplica mediante la semántica del lenguaje.

Análisis de revisión

Si la aplicación no es grande (menos de 30 KLOC) y tiene buenos programadores, generalmente no es necesario construir nada. Organizar estructuras basadas en el valor será suficiente, por ejemplo:

// /app/user/user.go
package user
type User struct {}
type Controller struct {}

A menudo, las "violaciones de restricciones" tienen poca importancia o son fáciles de solucionar. Daña la claridad y la comprensión: siempre y cuando no permita que se salga de control, no tiene que preocuparse por eso.

Análisis estático / tiempo de ejecución

También puede usar el análisis estático o en tiempo de ejecución para encontrar estas fallas, mediante anotaciones:

Estático:

// /app/user/user.go
package user

// architecture: model
type User struct {}

// architecture: controller
type Controller struct {}

Dinámica:

// /app/user/user.go
package user

import "app/constraint"

var _ = constraint.Model(&User{})
type User struct {}

var _ = constraint.Controller(&Controller{})
type Controller struct {}

// /app/main.go
package main

import "app/constraint"

func init() { constraint.Check() }

Tanto estático / dinámico también se puede hacer a través de campos:

// /app/user/user.go
package user

import "app/constraint"

type User struct {   
    _ constraint.Model
}

type Controller struct {
    _ constraint.Controller
}

Por supuesto, la búsqueda de tales cosas se vuelve más complicada.

Otras versiones

Dichos enfoques se pueden usar en otros lugares, no solo las restricciones de tipo, sino también los nombres de funciones, API-s, etc.

https://play.golang.org/p/4bCOV3tYz7

Egon
fuente