Golang和DDD域建模



我最近一直在研究以域驱动的设计,并且必须说这种类型的建筑设计触发了我的东西。当我尝试将其概念应用于我的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/

所有方法常见:

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"

方法#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
}

使用此方法,我认为从长远来看会有很多重复的代码,但是至少根据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
}

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

方法#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
}

在这里,友好的名称类型只是一个字符串,但不可变。这个结构使我想起了Java代码...在查找一个领域时,存储库是否应该使用域模型中的设置器方法来构建领域的聚合?我尝试了将DTO实现放置在同一软件包(DTO_SQL.GO)中的DTO实现,该软件包编码/解码为/从Realm聚合中进行了解码,但是在域软件包中放置了关注的问题。

如果您面对与我相同的问题,知道任何其他方法或有任何要指出的方法,我将对您的听力非常感兴趣!

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

在许多情况下,更简单的设计,例如一种CRUD方法,效果最好。在功能性和/或特征量随着时间的推移会显着增长的应用程序中,DDD闪耀的应用在自己中更为复杂。技术优势可以从模块化,可扩展性和可检验性方面产生,但是 - 最重要的是恕我直言 - 提供一个过程,在此过程中,您可以将非技术利益相关者带入并将他们的愿望转化为代码而不会沿途失去代码。

有一系列很棒的博客文章,《野外锻炼》的DDD示例,它带您进行了传统的基于GO CRUD CRUD的REST API设计的重构过程,以进行多个步骤。

该系列的作者罗伯特·拉斯卡克(Robert Laszczak)定义了DDD如下:

确保您在最佳方式中解决有效问题。之后,以您的业务理解的方式实现解决方案,而无需从需要技术语言的任何额外翻译

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

关键在这里要确定您想走多远(没有双关语)。重构逐渐引入了新的体系结构概念,在这些步骤中,您应该确定它是否足以适合您的用例,权衡进一步的利弊。他们从DDD Lite版本开始非常亲吻,然后随后使用CQR,干净的体系结构,微服务甚至活动采购。

我在许多项目中看到的是,它们立即遍及整个蒙蒂,造成过度杀伤。特别是微服务和事件采购添加(偶然)复杂性。


我还不是Go(实际上是该语言的新手),但会刺伤您的选择并提供一些考虑。也许更多经验丰富的Go开发人员可以纠正我,我越来越明显:)

对于我自己的项目,我正在研究一个干净的体系结构(端口;适配器,控制倒置) CQRS DDD组合。

狂野的锻炼示例提供了足够的灵感,但在这里和那里需要一些调整和补充。

我的目标是,在代码库的文件夹结构中,开发人员应立即识别出在哪里/用例(史诗,用户故事,方案),并且具有独立的,完全一致的域,这些域直接反映了无处不在的语言并被分别测试。。测试的一部分将是仅文本的BDD脚本,客户和最终用户很容易理解。

将涉及一些样板,但是 - 给定 - 我认为优点超过了缺点(如果您的申请值为DDD)。

您的选项#1对我来说看起来最好,但是有了一些其他观察结果(注意:我会坚持您的命名,这会使其中的一些看起来很过分。计数)。

  • 而不是Entity我会说Realm代表AggregateRoot
  • 这可能是隐性的,也可以嵌入base.AggregateRoot
  • 汇总根是对域的访问点,并确保其状态始终保持一致。
  • 因此,Realm的内部状态应不变。状态变化通过功能发生。
  • 除非真的很琐碎并且不太可能更改,否则我会在单独的文件中实现FriendlyName值对象。
  • 域的一部分也是RealmRepository,但这不过是一个接口。

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

  • 应用程序层中可能有一个ChangeFriendlyName命令处理程序。
  • 处理程序可以访问存储库实现,例如InMemRealmRepository
  • 可能将CreateRealmParams传递给命令,然后进行验证。
  • 处理程序逻辑可能首先从数据库中获取Realm汇总。
  • 然后构造一个新的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
}

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
}

相关内容

  • 没有找到相关文章

最新更新