Golang 和 DDD 域建模

Kar*_*sen 8 domain-driven-design go value-objects cqrs

我最近一直在研究领域驱动设计,并且必须说这种类型的架构设计触发了我的某些东西。当我尝试将其概念应用于我的 Go 项目时,我遇到了一些障碍。以下是一些示例方法,但我非常不确定要使用哪种方法。

项目结构摘录:

??? api/
??? cmd/
??? internal/
|   ??? base/
|   |   ??? eid.go
|   |   ??? entity.go
|   |   ??? value_object.go
|   ??? modules/
|   |   ??? realm/
|   |   |   ??? api/
|   |   |   ??? domain/
|   |   |   |   ??? realm/
|   |   |   |   |   ??? service/
|   |   |   |   |   ??? friendly_name.go
|   |   |   |   |   ??? realm.go
|   |   |   |   |   ??? realm_test.go
|   |   |   |   ??? other_subdomain/
|   |   |   ??? repository/
|   |   |       ??? inmem/
|   |   |       ??? postgres/
Run Code Online (Sandbox Code Playgroud)

所有方法通用:

package realm // import "git.int.xxxx.no/go/xxxx/internal/modules/realm/domain/realm"

// base contains common elements used by all modules
import "git.int.xxxx.no/go/xxxx/internal/base"
Run Code Online (Sandbox Code Playgroud)

方法#1:

type Realm struct {
   base.Entity

   FriendlyName FriendlyName
}

type CreateRealmParams struct {
    FriendlyName string
}

func CreateRealm(id base.EID, params *CreateRealmParams) (*Realm, error) {
   var err error
   var r = new(Realm)

   r.Entity = base.NewEntity(id)
   r.FriendlyName, err = NewFriendlyName(params.FriendlyName)

   return r, err
}

type FriendlyName struct {
    value string
}

var ErrInvalidFriendlyName = errors.New("invalid friendly name")

func (n FriendlyName) String() string { return n.value }

func NewFriendlyName(input string) (FriendlyName, error) {
    if input == "" {
        return ErrInvalidFriendlyName
    }
    // perhaps some regexp rule here...

    return FriendlyName{value: input}, nil
}
Run Code Online (Sandbox Code Playgroud)

使用这种方法,我认为从长远来看会有很多重复的代码,但至少 FriendlyName 值对象根据 DDD 要求是不可变的,并为附加更多方法打开了大门。

方法#2:

type Realm struct {
    base.Entity

    FriendlyName string
}

type CreateRealmParams struct {
    FriendlyName string
}

func CreateRealm(id base.EID, params *CreateRealmParams) (*Realm, error) {
    var err error

    if err = validateFriendlyName(params.FriendlyName); err != nil {
        return nil, err
    }

    entity := base.NewEntity(id)

    return &Realm{
        Entity: entity,
        FriendlyName: params.FriendlyName,
    }, nil
}
Run Code Online (Sandbox Code Playgroud)

这一定是我遇到过的最常见的例子,除了很多例子缺乏的验证。

方法#3:

type Realm struct {
    base.Entity

    friendlyName string
}

type CreateRealmParams struct {
    FriendlyName string
}

func CreateRealm(id base.EID, params *CreateRealmParams) (*Realm, error) {
    var err error

    if err = validateFriendlyName(friendlyName); err != nil {
        return nil, err
    }

    entity := base.NewEntity(id)

    return &Realm{
        Entity: entity,
        friendlyName: friendlyName,
    }, nil
}

func (r *Realm) FriendlyName() string { return r.friendlyName }
func (r *Realm) SetFriendlyName(input string) error {
    if err := validateFriendlyName(input); err != nil {
        return err
    }
    r.friendlyName = input
    return nil
}
Run Code Online (Sandbox Code Playgroud)

这里的友好名称类型只是一个字符串,但不可变。这种结构让我想起了 Java 代码......在查找领域时,存储库层是否应该使用领域模型中的 setter 方法来构造领域聚合?我尝试将 DTO 实现放置在同一个包 (dto_sql.go) 中,该包对领域聚合进行编码/解码,但将这个问题放在域包中感觉有点不对。

如果您遇到和我一样的问题,了解任何其他方法或有什么要指出的,我将非常有兴趣收到您的来信!

Arn*_*ver 6

首先,正如其他评论者正确指出的那样,您必须查看DDD 的目标并决定该方法是否有价值。DDD 给架构增加了一些复杂性(大部分是在构建项目和基本类型的初始阶段)以及之后您必须处理的样板文件和仪式的数量。

在许多情况下,更简单的设计(例如 CRUD 方法)效果最好。DDD 的亮点在于应用程序本身在功能方面更加复杂和/或功能数量预计会随着时间的推移而显着增长。技术优势可以体现在模块化、可扩展性和可测试性方面,但 - 最重要的是,恕我直言 - 提供一个过程,您可以在其中带领非技术利益相关者并将他们的愿望转化为代码,而不会在此过程中丢失他们。

有一系列很棒的博客文章,Wild Workouts Go DDD 示例,它通过几个步骤引导您将传统的基于 Go CRUD 的 REST API 设计重构为成熟的 DDD 架构。

该系列作者Robert Laszczak将 DDD 定义如下:

确保您以最佳方式解决有效问题。之后,以您的企业能够理解的方式实施解决方案,而无需对所需的技术语言进行任何额外的翻译

他认为 Golang + DDD 是编写业务应用程序的绝佳方式。

理解这里的关键是决定你的设计想要走多远(没有双关语)。重构逐渐引入新的架构概念,在每个步骤中,您都应该决定它是否足以满足您的用例,权衡利弊以进一步进行。他们从DDD Lite版本开始非常 KISS ,然后进一步使用 CQRS、清洁架构、微服务甚至事件溯源。

我在许多项目中看到的是,他们立即全力以赴,造成了过度杀戮。特别是微服务和事件溯源增加了很多(意外的)复杂性。


我还不太熟悉 Go(实际上对这门语言还很陌生),但我会尝试一下您的选择并提供一些注意事项。也许更有经验的 Go 开发人员可以纠正我的错误:)

对于我自己的项目,我正在研究一种干净的架构(端口和适配器控制反转)+ CQRS + DDD组合。

Wild Workouts 示例提供了充足的灵感,但需要在这里或那里进行一些调整和添加。

我的目标是,在代码库的文件夹结构中,开发人员应该立即识别功能/用例(史诗、用户故事、场景)所在的位置,并拥有独立的、完全一致的域,直接反映通用语言并且可以单独测试。测试的一部分将是客户和最终用户可以轻松理解的纯文本BDD脚本。

将会涉及一些样板文件,但是 - 鉴于上述情况 - 我认为利大于弊(如果您的应用程序需要 DDD)。

你的选项#1对我来说看起来最好,但有一些额外的观察(注意:我会坚持你的命名,这会让其中一些看起来有点矫枉过正..再次重要的是想法

  • 相反,Entity我会说Realm代表一个AggregateRoot.
  • 这可以是隐式的,也可以内联base.AggregateRoot.
  • 聚合根是域的访问点,并确保其状态始终一致。
  • 因此 的内部状态Realm应该是不可变的。状态变化通过函数发生。
  • 除非真的很微不足道并且不太可能改变,否则我会FriendlyName在单独的文件中实现值对象。
  • a 也是域的一部分,RealmRepository但这仅提供了一个接口。

现在我正在使用 CQRS,它是代码片段中显示的内容的扩展。在此:

  • ChangeFriendlyName应用层可能有一个命令处理程序。
  • 处理程序可以访问存储库实现,例如InMemRealmRepository
  • 可能会将 a 传递CreateRealmParams给命令,然后命令进行验证。
  • Realm处理程序逻辑可能首先从数据库中获取聚合。
  • 然后构造一个new FriendlyName(也可以封装在Realm函数调用中)。
  • Realm用于更新状态并对事件进行排队的函数调用FriendlyNameChanged
  • 命令处理程序将更改保存到 InMemory 数据库中。
  • Commit()仅当命令处理程序在聚合上调用没有错误时。
  • 现在,例如通过 发布一个或多个排队事件EventBus,并在需要时进行处理。

至于选项#1的代码进行了一些更改(希望我做对了)..

realm.go - 聚合根

type Realm struct {
   base.AggregateRoot

   friendlyName FriendlyName
}

// Change state via function calls. Not shown: event impl, error handling.
// Even with CQRS having Events is entirely optional. You might implement
// it solely to e.g. maintain an audit log.
func (r *Realm) ChangeFriendlyName(name FriendlyName) {
   r.friendlyName = name
   
   var ev = NewFriendlyNameChanged(r.id, name)

   // Queue the event.
   r.Apply(ev)
}

// You might use Params types and encapsulate value object creation,
// but I'll pass value objects directly created in a command handler.
func CreateRealm(id base.AID, name FriendlyName) (*Realm, error) {
   ar := base.NewAggregateRoot(id)

   // Might do some param validation here.

   return &Realm{
       AggregateRoot: ar,
       friendlyName: name,
   }, nil
}
Run Code Online (Sandbox Code Playgroud)

FriendlyName.go - 值对象

type FriendlyName struct {
    value string
}

// Domain error. Part of ubiquitous language.
var FriendlyNameInvalid = errors.New("invalid friendly name")

func (n FriendlyName) String() string { return n.value }

func NewFriendlyName(input string) (FriendlyName, error) {
    if input == "" {
        return FriendlyNameInvalid
    }
    // perhaps some regexp rule here...

    return FriendlyName{value: input}, nil
}
Run Code Online (Sandbox Code Playgroud)