发布于 2026-01-06 2 阅读
0

Go Baralga 的 DDD 汉堡

DDD汉堡外卖

巴拉尔加

DDD Hamburger 是我最喜欢的 Go 架构。为什么呢?DDD Hamburger 完美地融合了领域驱动设计 (DDD)六边形架构和分层架构的优点。了解一下 Go 的 DDD Hamburger 架构,也许它也会成为你的最爱!

DDD汉堡概览🍔

DDD汉堡概览

DDD汉堡是一款真正的汉堡,从上到下层次分明:

汉堡面包的上半部分🍞
汉堡面包的上半部分是汉堡的顶部,也是展示层。展示层包含您的 REST API 和 Web 界面。

沙拉层🥗
面包下面是沙拉层,也就是应用层。应用层包含了应用程序的用例逻辑。


接下来是汉堡包的“肉”——领域就像肉一样,领域层是汉堡包最重要的部分。领域层包含领域逻辑以及领域内的所有实体、聚合和值对象。

下半部分面包🍞
下半部分面包是基础设施层。基础设施层包含Postgres数据库存储库的具体实现。基础设施层实现了领域层中声明的接口。

明白了吗?太好了,那我们来仔细看看细节。

DDD汉堡包示例

现在我们将使用我们的领域驱动设计(DDD)汉堡包来编写一个 Go 应用程序。我们将使用一个简单的带有 Activity 的时间跟踪示例应用程序来展示 Go 的实际实现。新的 Activity 通过 REST API 添加,然后存储在 Postgres 数据库中。

下面展示的是应用于 Go 语言的 DDD 汉堡包架构。我们稍后会详细介绍汉堡包架构的每一层。

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

基础设施层依赖于领域层,并且可以使用领域层的所有实体、聚合和存储库接口。但仅限于领域层。

在主功能区组装汉堡

不,我们现在面前摆着肉、沙拉和面包,它们都是单独的。是时候用这些食材做一个像样的汉堡了。我们在主操作台组装汉堡,如下图所示。

为了正确地连接各个依赖项,我们从下往上进行操作:

  1. 首先,我们创建一个新的数据库活动存储库实例DbActivityRepository,并将数据库连接池传入。
  2. 接下来,我们创建应用程序服务ActivityService并传入存储库。
  3. 现在我们创建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。根包包含领域层。根包中还有每个层的子包,例如 `<层名>` applicationinfrastructure`<presentation层名>`、`<层名>` 等。为什么要把领域层放在根包里呢?这样我们就可以使用领域层的正确名称。因此,如果我们在某个地方使用领域实体Activity,代码会变成tracking.Activity这样,非常易于阅读。

最佳的包结构取决于您的项目。我建议您从小规模、简单的方案开始,然后随着项目的发展逐步调整。

DDD汉堡包的总结🍔

DDD Hamburger 是一种完全基于领域驱动设计的分层架构。它非常容易理解和遵循。这就是为什么 DDD Hamburger 是我最喜欢的架构风格,尤其是在 Go 语言中。

如您所见,在 Go 应用中使用 DDD Hamburger 非常简单。您可以从小规模开始,并根据需要进行扩展。

以下是 DDD Hamburger 的一个应用示例:

GitHub 标志 Baralga / baralga-app

简单轻便的时间跟踪工具,适用于个人和团队,云端部署。

巴拉尔加

具有 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