DDD 汉堡包是我最喜欢的 Go 架构类型。为什么?DDD 汉堡完美地结合了领域驱动设计的六边形架构和分层架构的优点。了解 Go 的 DDD 汉堡架构,也许它也会成为您的最爱!
DDD 汉堡概述
DDD 汉堡包是一个真正的汉堡包,从上到下都有明确定义的层次:
上半面包
上半面包是汉堡包的顶部,是表示层。表示层包含 REST API 和 Web 界面。
沙拉
面包下面是沙拉,也就是涂抹层。应用程序层包含应用程序的用例逻辑。
肉
接下来是汉堡包的肉,即域层。就像肉一样,域层是汉堡包中最重要的部分。域层包含域逻辑以及域的所有实体、聚合和值对象。
包子下半部分
包子下半部分是基础设施层。基础设施层包含 Postgres 数据库存储库的具体实现。基础设施层实现领域层中声明的接口。
明白了吗?太好了,让我们来看看细节。
DDD 汉堡包的 Go 示例
现在我们将使用 DDD 汉堡包编写 Go 应用程序。我们将使用一个简单的时间跟踪示例应用程序和活动来展示实际的 Go 实现。使用 REST API 添加新活动,然后将其存储在 Postgres 数据库中。
应用于Go的DDD汉堡架构如下所示。我们很快就会详细介绍汉堡包的所有层。
表示层作为上发髻
表示层包含 REST API 的 HTTP 处理程序。HTTP 处理程序喜欢HandleCreateActivity创建简单的处理程序函数。活动的所有处理程序都挂在一个 struct 上ActivityRestHandlers,如下面的代码所示。
type ActivityRestHandlers struct {
actitivityService *ActitivityService
}
func (a ActivityRestHandlers) HandleCreateActivity() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ...
}
}
创建新活动的实际逻辑由
ActivityService底层应用程序层的服务处理。
HTTP 处理程序不使用域层的活动实体作为 JSON 表示。他们使用自己的 JSON 模型,其中包含结构标记以将其正确序列化为 JSON,如下所示。
type activityModel struct {
ID string `json:"id"`
Start string `json:"start"`
End string `json:"end"`
Duration durationModel `json:"duration"`
}
这样我们的表示层只依赖于应用层和领域层,而不是更多。我们允许宽松的层,这意味着可以跳过应用程序层并直接使用领域层的内容。
应用层就像沙拉
应用程序层包含实现我们应用程序用例的服务。一个用例是创建一项活动。CreateActivity该用例是在应用程序服务结构体的挂起方法中实现的ActitivityService。
type ActitivityService struct {
repository ActivityRepository
}
func (a ActitivityService) CreateActivity(ctx context.Context, activity *Activity) (*Activity, error) {
savedActivity, err := a.repository.InsertActivity(ctx, activity)
// ... do more
return savedActivity, nil
}
ActivityRepository应用程序服务使用用例的存储库接口。然而它只知道在域层中声明的存储库的接口。该接口的实际实现与应用层无关。
应用程序服务还处理事务边界,因为一个用例应在一个原子事务中处理。例如,创建具有初始活动的新项目的用例必须在一个事务中完成,尽管它将使用一个存储库用于活动,一个存储库用于项目。
领域层就像肉
最重要的部分是领域层,它是 DDD 汉堡的肉。领域层包含领域实体Activity、计算活动持续时间等领域逻辑以及活动存储库的接口。
// Activity Entity
type Activity struct {
ID uuid.UUID
Start time.Time
End time.Time
Description string
ProjectID uuid.UUID
}
// -- Domain Logic
// DurationDecimal is the activity duration as decimal (e.g. 0.75)
func (a *Activity) DurationDecimal() float64 {
return a.duration().Minutes() / 60.0
}
// ActivityRepository
type ActivityRepository interface {
InsertActivity(ctx context.Context, activity *Activity) (*Activity, error)
// ... lot's more
}
领域层是唯一不允许依赖于其他层的层。它还应该主要使用 Go 标准库来实现。这就是为什么我们既不使用 json 的结构标签,也不使用任何数据库访问代码。
基础设施层作为下层 Bun
ActivityRepository基础设施层包含struct 中存储库域接口的具体实现DbActivityRepository。此存储库实现使用 Postgres 驱动程序pgx和纯 SQL 将活动存储在数据库中。它使用上下文中的数据库事务,因为该事务是由应用程序服务启动的。
// DbActivityRepository is a repository for a SQL database
type DbActivityRepository struct {
connPool *pgxpool.Pool
}
func (r *DbActivityRepository) InsertActivity(ctx context.Context, activity *Activity) (*Activity, error) {
tx := ctx.Value(shared.ContextKeyTx).(pgx.Tx)
_, err := tx.Exec(
ctx,
`INSERT INTO activities
(activity_id, start_time, end_time, description, project_id)
VALUES
($1, $2, $3, $4, $5, $6, $7)`,
activity.ID,
activity.Start,
activity.End,
activity.Description,
activity.ProjectID,
)
if err != nil {
return nil, err
}
return activity, nil
}
基础设施层依赖于领域层,并且可以使用来自领域层的所有实体、聚合和存储库接口。但仅限于领域层。
在主函数中组装汉堡
不,我们面前摆着肉、沙拉和小圆面包。是时候用这些碎片制作一个合适的汉堡了。我们在主函数中组装汉堡包,如下所示。
为了将依赖关系正确连接在一起,我们从下到上工作:
- 首先,我们创建数据库活动存储库的新实例DbActivityRepository并传入数据库连接池。
- 接下来我们创建应用程序服务ActivityService并传入存储库。
- 现在我们创建ActivityRestHandlers并传入应用程序服务。现在,我们向 HTTP 路由器注册 HTTP 处理程序函数。
组装 DDD 汉堡架构的代码如下:
func main() {
// ...
// Infrastructure Layer with concrete repository
repository := tracking.NewDbActivityRepository(connectionPool)
// Application Layer with service
appService := tracking.NewActivityService(repository)
// Presentation Layer with handlers
restHandlers := tracking.NewActivityRestHandlers(appService)
router.HandleFunc("/api/activity", restHandlers.HandleCreateActivity())
}
组装汉堡包的代码简单明了,非常容易理解。我喜欢这样,这就是我通常所需要的。
DDD 汉堡包的包结构
仍然存在一个问题:我们的 Go 包的哪种结构最适合 DDD 汉堡包?
我通常从所有层的单个包开始。因此,单个包包含用于其余处理程序、应用程序服务、域层和数据库存储库的tracking文件。activity_rest.goactivity_service.goactivity_domain.goactivity_repository_db.go
下一步是将除域层之外的所有层分离为单独的包。所以我们会有一个根包tracking。根包包含域层。根包每层都有子包,如application, infrastructure, presentation。为什么域层放在根包中?这样我们就可以使用域层及其正确的名称。因此,如果我们在某处使用域实体Activity,代码就会读取tracking.Activity,这非常好读。
哪种包结构最好取决于您的项目。我建议您从小而简单地开始,并随着项目的不断发展而调整它。
DDD 汉堡包的总结
DDD 汉堡包是完全基于领域驱动设计的分层架构。它非常容易理解和遵循。这就是为什么 DDD 汉堡包是我最喜欢的架构风格。一般来说,尤其是在 Go 中。
正如您所看到的,在 Go 应用程序中使用 DDD 汉堡包非常简单。您可以从小处开始,但也可以根据需要进行成长。