蚂蚁 Golang 领域驱动设计(DDD)极简最佳实践

2023-05-0112:27:31软件工程与架构Comments2,596 views字数 7646阅读模式

蚂蚁 Golang 领域驱动设计(DDD)极简最佳实践文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

在本文中你可以看到一套较为完整的仓储层 => 领域层 => 表现层的 Golang 代码实现,但是肯定不会覆盖全部 DDD 概念,各位可以将它看作一种 Golang 中 DDD 的最佳实践来参考。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

01 背景

因为业务需求,我们当时正在用 Golang 从 0 到 1 构建一个Web 系统,由于是个新系统,所以很多东西要自己摸索,当时摆在面前的实际问题是如何更好的组织数据访问层和核心领域模型的代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

相比 Java 构建的系统,代码组织方式、分层等最佳实践已经固化在 Sprint Boot、Sofa Boot 等框架中,Golang 缺乏这种标准化的最佳实践。这个时候我们想到领域驱动设计(DDD),看起来可以解决我们的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

02整体设计

指导思想(DDD):重点借鉴了 DDD 中的表现层(User Interface)、领域层(Domain)和基础设施层(Infrastructure)解决我们的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

蚂蚁 Golang 领域驱动设计(DDD)极简最佳实践文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

03代码实现

领域层

领域层(Domain)是系统的核心,负责表达业务概念,业务状态信息以及业务规则,即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

本文中放置核心模型定义(实体)和仓储抽象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

什么是实体(Entity)?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

一个实体是一个唯一的东西,并且可以在相当长的一段时间内持续地变化。唯一的身份标识和可变性(mutability)特征将实体对象和值对象(Value Object)区分开来。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

——《实现领域驱动设计》文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

package entity
import (  "fmt"  "net/url"  "time")
// Source represents the specific configuration code source,// which should be a specific instance of the source provider.type Source struct {  // ID is the id of the source.  ID uint  // SourceProvider is the type of the source provider.  SourceProvider SourceProviderType  // Remote is the source URL, including scheme.  Remote *url.URL  // CreatedAt is the timestamp of the created for the source.  CreatedAt time.Time  // CreatedAt is the timestamp of the updated for the source.  UpdatedAt time.Time}

仓储(Repository)接口定义文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

package repository
import (  "context"  "github.com/elliotxx/ddd-demo/pkg/domain/entity")
// SourceRepository is an interface that defines the repository operations for sources.// It follows the principles of domain-driven design (DDD).type SourceRepository interface {  // Create creates a new source.  Create(ctx context.Context, source *entity.Source) error  // Delete deletes a source by its ID.  Delete(ctx context.Context, id uint) error  // Update updates an existing source.  Update(ctx context.Context, source *entity.Source) error  // Get retrieves a source by its ID.  Get(ctx context.Context, id uint) (*entity.Source, error)  // Find returns a list of specified sources.  Find(ctx context.Context, query Query) ([]*entity.Source, error)  // Count returns the total of sources.  Count(ctx context.Context) (int, error)}

基础设施层

基础设施层(Infrastructure)主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现。本篇中主要负责放置仓储实现。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

仓储(Repository)接口实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

package persistenceimport (  "context"  "github.com/elliotxx/ddd-demo/pkg/domain/entity"  "github.com/elliotxx/ddd-demo/pkg/domain/repository"  "github.com/elliotxx/errors"  "gorm.io/gorm")// The sourceRepository type implements the repository.SourceRepository interface.// If the sourceRepository type does not implement all the methods of the interface,// the compiler will produce an error.var _ repository.SourceRepository = &sourceRepository{}
// sourceRepository is a repository that stores sources in a gorm database.type sourceRepository struct {  // db is the underlying gorm database where sources are stored.  db *gorm.DB}
// NewSourceRepository creates a new source repository.func NewSourceRepository(db *gorm.DB) repository.SourceRepository {  return &sourceRepository{db: db}}
// Create saves a source to the repository.func (r *sourceRepository) Create(ctx context.Context, dataEntity *entity.Source) error {    // ......    // do something    // ......}
// Delete removes a source from the repository.func (r *sourceRepository) Delete(ctx context.Context, id uint) error {    // ......    // do something    // ......}
// Update updates an existing source in the repository.func (r *sourceRepository) Update(ctx context.Context, dataEntity *entity.Source) error {    // ......    // do something    // ......}
// Find retrieves a source by its ID.func (r *sourceRepository) Get(ctx context.Context, id uint) (*entity.Source, error) {    // ......    // do something    // ......}
// Find returns a list of specified sources in the repository.func (r *sourceRepository) Find(ctx context.Context, query repository.Query) ([]*entity.Source, error) {    // ......    // do something    // ......}
// Count returns the total of sources.func (r *sourceRepository) Count(ctx context.Context) (int, error) {    // ......    // do something    // ......}

数据对象文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

由于 Entity 领域层的定义,是流通在领域层的结构,所以不适合直接用于和数据库直接打交道,所以还需要一个和数据库表完全映射的数据对象(DO),参考社区的做法,找到一种最佳实践,即:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

https://hindenbug.io/the-power-of-generics-in-go-the-repository-pattern-for-gorm-7f8891df0934文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

具体思路是定义一个数据库对象,比如数据源(Source)对应的数据库对象是 SourceModel,它具有 FromEntity 和 ToEntity 方法,用于进行领域实体和数据库对象之间的相互转换。同时它也实现了 GORM 的 TableName 方法,用于指定对应的数据库表名。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

SourceModel 实现:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

package persistence
import (  "net/url"  "github.com/elliotxx/ddd-demo/pkg/domain/entity"  "github.com/elliotxx/errors"  "gorm.io/gorm")
// SourceModel is a DO used to map the entity to the database.type SourceModel struct {  gorm.Model  // SourceProvider is the type of the source provider.  SourceProvider string  // Remote is the source URL, including scheme.  Remote string}
// The TableName method returns the name of the database table that the struct is mapped to.func (m *SourceModel) TableName() string {  return "source"}
// ToEntity converts the DO to a entity.func (m *SourceModel) ToEntity() (*entity.Source, error) {  if m == nil {    m = &SourceModel{}  }      sourceProvider, err := entity.ParseSourceProviderType(m.SourceProvider)  if err != nil {    return nil, errors.Wrap(err, "failed to parse source provider type")  }      remote, err := url.Parse(m.Remote)  if err != nil {    return nil, errors.Wrap(err, "failed to parse remote into URL structure")  }      return &entity.Source{    ID:             m.ID,    SourceProvider: sourceProvider,    Remote:         remote,    CreatedAt:      m.CreatedAt,    UpdatedAt:      m.UpdatedAt,  }, nil}
// FromEntity converts a entity to a DO.func (m *SourceModel) FromEntity(e *entity.Source) error {  if m == nil {    m = &SourceModel{}  }  if err := e.Validate(); err != nil {    return err  }  m.ID = e.ID  m.SourceProvider = string(e.SourceProvider)  m.Remote = e.Remote.String()  m.CreatedAt = e.CreatedAt  m.UpdatedAt = e.UpdatedAt  return nil}

事务文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

// Create saves a source to the repository.func (r *sourceRepository) Create(ctx context.Context, dataEntity *entity.Source) error {  // Map the data from Entity to DO  var dataModel SourceModel  err := dataModel.FromEntity(dataEntity)  if err != nil {    return err  }  return r.db.Transaction(func(tx *gorm.DB) error {    // Create new record in the store    err = tx.WithContext(ctx).Create(&dataModel).Error    if err != nil {      return err    }    // Map fresh record's data into Entity    newEntity, err := dataModel.ToEntity()    if err != nil {      return err    }    *dataEntity = *newEntity    return nil  })}

表现层

表现层(User Interface):用户界面层,或者表现层,负责向用户显示解释用户命令。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

RESTFul API 实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

比较薄,纯逻辑编排,包括检验、CURD 等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

//  @Summary    Update source//  @Description  Update the specified source//  @Accept      json//  @Produce    json//  @Param      source  body    UpdateSourceRequest  true  "Updated source"//  @Success    200    {object}  entity.Source    "Success"//  @Failure    400    {object}  errors.DetailError  "Bad Request"//  @Failure    401    {object}  errors.DetailError  "Unauthorized"//  @Failure    429    {object}  errors.DetailError  "Too Many Requests"//  @Failure    404    {object}  errors.DetailError  "Not Found"//  @Failure    500    {object}  errors.DetailError  "Internal Server Error"//  @Router      /api/v1/source [put]func (h *Handler) UpdateSource(c *gin.Context, log logrus.FieldLogger) error {    // ......    // do something    // ......}

OpenAPI文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

作用:自动生成 Swagger 接口文档 + 多语言 SDK;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

效果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

Swagger 接口文档:
LiveDemo:https://petstore.swagger.io/?_ga=2.268933875.36106021.1681113818-3234578.1681113817文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

蚂蚁 Golang 领域驱动设计(DDD)极简最佳实践文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

多语言 SDK:https://openapi-generator.tech/文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

蚂蚁 Golang 领域驱动设计(DDD)极简最佳实践文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

04 一些技巧

分享本 PR 中使用到的一些 Golang 奇巧:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

检查结构体是否实现了某接口的防御代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

如下图示例,结构体 sourceRepository 如果没有实现 repository.SourceRepository 接口,IDE 会直接飘红,或者在编译阶段报错。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

写法一:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

// The sourceRepository type implements the repository.SourceRepository interface.// If the sourceRepository type does not implement all the methods of the interface,// the compiler will produce an error.var _ repository.SourceRepository = &sourceRepository{}

写法二:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

var _ repository.SourceRepository = (*sourceRepository)(nil)

适应于 Go Web 应用的错误处理文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

Golang 标准库中的 error 类型只包含错误信息,我们自定义了适应于 Go Web 应用的错误包(elliotxx/errors),可以包含更多错误信息,比如错误堆栈、错误码、错误原因等;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

示例1:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

if err := c.ShouldBindJSON(&requestPayload); err != nil {    return errcode.ErrDeserializedParams.Causewf(err, "failed to decode json")}

示例2:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

// Get the existed source by idupdatedEntity, err := h.repo.Get(context.TODO(), requestEntity.ID)if err != nil {    if errors.Is(err, gorm.ErrRecordNotFound) {        return errcode.NotFound.Causewf(err, "failed to update source")    }    return errcode.InvalidParams.Cause(err)}

对象拷贝文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

大部分字段相似的对象可以使用社区的 copier 轮子直接深度拷贝,一行搞定,不用写多行赋值语句了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

// Overwrite non-zero values in request entity to existed entitycopier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true})

05 心得分享

当 Coding 时遇到迷茫,第一选择应该是到社区寻找最佳实践,比如到 Github、SourceGraph 等平台搜索代码实现,不要闭门造车;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

代码实现的时候可以尽量考虑开放性,比如引入 OpenAPI 声明接口;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

DDD 不是银弹,需要结合实际项目需要按需结合,生搬硬套容易事倍功半。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/arc/38101.html

  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/arc/38101.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定