DDD汉堡外卖
巴拉尔加
DDD Hamburger 是我最喜欢的 Go 架构。为什么呢?DDD Hamburger 完美地融合了 领域驱动设计 (DDD) 的 六边形架构 和分层架构的优点。了解一下 Go 的 DDD Hamburger 架构,也许它也会成为你的最爱!
DDD汉堡概览🍔
DDD汉堡是一款真正的汉堡,从上到下层次分明:
汉堡面包的上半部分 🍞 汉堡面包的上半部分是汉堡的顶部,也是 展示层 。展示层包含您的 REST API 和 Web 界面。
沙拉层 🥗 面包下面是沙拉层,也就是 应用层 。应用层包含了应用程序的用例逻辑。
接下来是汉堡包的“肉”—— 领域 层 。 就像肉一样,领域层是汉堡包最重要的部分。领域层包含领域逻辑以及领域内的所有实体、聚合和值对象。
下半部分面包 🍞 下半部分面包是 基础设施层 。基础设施层包含Postgres数据库存储库的具体实现。基础设施层实现了领域层中声明的接口。
明白了吗?太好了,那我们来仔细看看细节。
DDD汉堡包示例
现在我们将使用我们的领域驱动设计(DDD)汉堡包来编写一个 Go 应用程序。我们将使用一个简单的带有 Activity 的时间跟踪示例应用程序来展示 Go 的实际实现。新的 Activity 通过 REST API 添加,然后存储在 Postgres 数据库中。
下面展示的是应用于 Go 语言的 DDD 汉堡包架构。我们稍后会详细介绍汉堡包架构的每一层。
展示层作为上层小面包🍞
表示层包含 REST API 的 HTTP 处理程序。这些 HTTP 处理程序会 HandleCreateActivity创建简单的处理函数。所有 Activity 的处理程序都隶属于同一个结构体 ActivityRestHandlers,如下面的代码所示。
type ActivityRestHandlers struct {
actitivityService * ActitivityService
}
func ( a ActivityRestHandlers ) HandleCreateActivity () http . HandlerFunc {
return func ( w http . ResponseWriter , r * http . Request ) {
// ...
}
}
Enter fullscreen mode
Exit fullscreen mode
ActivityService创建新活动的实际逻辑由底层应用层的 服务处理。
HTTP 处理程序不使用域层的 activity 实体作为 JSON 表示。它们使用自己的 JSON 模型,该模型包含结构标签,以便将其正确序列化为 JSON,如下所示。
type activityModel struct {
ID string `json:"id"`
Start string `json:"start"`
End string `json:"end"`
Duration durationModel `json:"duration"`
}
Enter fullscreen mode
Exit fullscreen mode
这样一来,我们的表示层就只依赖于应用层和领域层,不多不少。我们允许使用宽松的层结构,这意味着可以跳过应用层,直接使用领域层中的代码。
应用层就像沙拉🥗
应用层包含实现应用程序用例的服务。其中一个用例是创建 Activity。该用例在 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
}
Enter fullscreen mode
Exit fullscreen mode
应用程序服务使用存储库接口 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
}
Enter fullscreen mode
Exit fullscreen mode
领域层是唯一不允许依赖其他层的层。它也应该主要使用 Go 标准库来实现。这就是为什么我们既不使用 JSON 结构体标签,也不使用任何数据库访问代码的原因。
基础设施层就像下面的面包🍞
ActivityRepository基础架构层包含结构体中 存储库域接口的具体实现 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)` ,
activity . ID ,
activity . Start ,
activity . End ,
activity . Description ,
activity . ProjectID ,
)
if err != nil {
return nil , err
}
return activity , nil
}
Enter fullscreen mode
Exit fullscreen mode
基础设施层依赖于领域层,并且可以使用领域层的所有实体、聚合和存储库接口。但仅限于领域层。
在主功能区组装汉堡
不,我们现在面前摆着肉、沙拉和面包,它们都是单独的。是时候用这些食材做一个像样的汉堡了。我们在主操作台组装汉堡,如下图所示。
为了正确地连接各个依赖项,我们从下往上进行操作:
首先,我们创建一个新的数据库活动存储库实例 DbActivityRepository,并将数据库连接池传入。
接下来,我们创建应用程序服务 ActivityService并传入存储库。
现在我们创建 ActivityRestHandlers并传入应用程序服务。接下来,我们将HTTP处理函数注册到HTTP路由器中。
用于构建我们 DDD Hamburger 架构的代码如下:
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 ())
}
Enter fullscreen mode
Exit fullscreen mode
用于构建汉堡包的代码简洁明了,非常容易理解。我很喜欢这一点,而且通常来说,这已经足够了。
DDD汉堡的包结构
还有一个问题:我们的 Go 包的哪种结构最适合 DDD Hamburger?
我通常会先创建一个包含所有层的包。所以一个包里 tracking包含 activity_rest.goREST 处理程序、 activity_service.go应用程序服务、领域层 activity_domain.go和数据库存储库的文件 activity_repository_db.go。
下一步是将所有层分离成单独的包,除了领域层。这样我们就有了根包 tracking。根包包含领域层。根包中还有每个层的子包,例如 `<层名>` application、 infrastructure`< presentation层名>`、`<层名>` 等。为什么要把领域层放在根包里呢?这样我们就可以使用领域层的正确名称。因此,如果我们在某个地方使用领域实体 Activity,代码会变成 tracking.Activity这样,非常易于阅读。
最佳的包结构取决于您的项目。我建议您从小规模、简单的方案开始,然后随着项目的发展逐步调整。
DDD汉堡包的总结🍔
DDD Hamburger 是一种完全基于领域驱动设计的分层架构。它非常容易理解和遵循。这就是为什么 DDD Hamburger 是我最喜欢的架构风格,尤其是在 Go 语言中。
如您所见,在 Go 应用中使用 DDD Hamburger 非常简单。您可以从小规模开始,并根据需要进行扩展。
以下是 DDD Hamburger 的一个应用示例:
简单轻便的时间跟踪工具,适用于个人和团队,云端部署。
巴拉尔加
具有 Web 前端和 API 的多用户时间跟踪应用程序。
用户指南
键盘快捷键
追踪活动
捷径
行动
Alt + Shift + n
添加活动
Alt + Shift + p
项目管理
报告活动
捷径
行动
Shift + Arrow Left
显示上一个时间段
Shift + Arrow Down
显示当前时间段
Shift + Arrow Right
显示下一个时间段
行政
访问 Web 用户界面
网页用户界面可通过以下网址访问 http://localhost:8080/:。您可以使用管理员账号登录 admin/adm1n,或使用用户账号登录 user1/us3r。
配置
后端配置使用以下环境变量:
环境变量
默认值
描述
BARALGA_DB
postgres://postgres:postgres@localhost:5432/baralga
PostgreSQL 数据库连接字符串
BARALGA_DBMAXCONNS
3
连接池中数据库连接的最大数量。
PORT
8080
http 服务器端口
BARALGA_WEBROOT
http://localhost:8080
Web 服务器根目录
BARALGA_JWTSECRET
secret
用于生成 JWT 的随机密钥
BARALGA_CSRFSECRET
CSRFsecret
用于 CSRF 保护的随机密钥
BARALGA_ENV
dev
用于 production生产模式
BARALGA_SMTPSERVERNAME
…
鸣谢
DDD汉堡架构是由 Henning Schwentner提出的,在此感谢他。另外, Mat Ryer 对HTTP处理程序的结构设计也产生了很大的影响 。
文章来源:https://dev.to/remast/the-ddd-hamburger-for-go-2156