手把手教你学会写DDD(领域驱动设计)项目代码

微服务治理数据库管理服务开发与运维

写作是为了让过去的自己有迹可循

人总是习惯按自己的想法去思考,

而不是按照别人的想法去思考


领域驱动设计在20多年前就已经提了出来,现在也有很多人提到DDD,但是很多都是理论分析,没有手把手的去在项目里面实践DDD。这次开始从0写一个DDD的项目来学习DDD是如何构建项目的。

还没读过DDD相关的书的同学建议多去读一下,里面有许多的专业术语,不需要读的太懂。、因为单纯的概念解释不能形成一个具象的如何实践代码的指导。

简单来讲和DDD对应的常用的一个模式是MVC。无论是哪类模式,其实都是对完成一个业务需求的代码如何组织的问题。

MVC把整个代码分为三层,每一层负责自己的职责,这个模式最大的特点是把业务逻辑都放在service,在一个业务复杂的项目里面,有的service可能会写到3000行甚至更多。市面上也有不少的基于MVC模式开发的框架,如前面介绍的beego。那么DDD作为一种组织业务代码的思想,它和MVC的组织方式有什么不同呢?

picture.image

DDD简介

DDD是一种以domain领域作为我们编写软件的核心概念,我们的代码围绕着领域展开,同时我们的代码也是来表达领域的。关于领域,知道的最多的是领域专家,这些领域专家所知道的关于领域的知识就是开发的软件需要表达实现的内容。所以开发软件的开始并不是开发人员开始写下项目的第一行代码,而是开始于专家所知道的领域知识。

下面以做一个花店app的例子,来说明如何写一个DDD的项目。我们的项目就命名为flower。

现在有一个领域专家叫玛丽,你作为开发人员。玛丽分析出来有如下domain,Customer(顾客),Product(商品),Banking(存款),Supplier(供应商)。注意这个命名是从专家的角度命名的,也许从你的角度会命名为其他名称。这样专家和开发人员在沟通的时候表达就会统一,从而减少沟通成本。

上面提到的domain就是下面代码需要操作的对象。同时需要注意上面的domain是核心domain,对于Customer里面可能会有子领域。

现在开始创建一个项目

  
mkdir flower  
go mod init github.com/myname/flower

接下来需要创建 entity 包,这里面存放子领域subdomain,然后创建一个 domain 包存放核心领域core domain。

entity:一类可变的对象

picture.image

创建entity

接下来我们在entity里面创建2个实体Person和Item

  
package entity  
  
import (  
 "github.com/google/uuid"  
)  
  
  
type Person struct {  
 // ID is the identifier of the Entity   
 ID uuid.UUID  
 // Name is the name of the person  
 Name string  
 // Age is the age of the person  
 Age int  
}
  
package entity  
  
import "github.com/google/uuid"  
  
// Item represents a Item for all sub domains  
// entity 具有唯一标识和可以修改的特点  
type Item struct {  
 ID uuid.UUID   
 Name string   
 Description string   
}

于entity对应的概念是值对象(Value Object)。简称VO,没有唯一标识和不可变。

创建Value Object

下面也来创建一个VO

  
package valueobject  
  
import (  
 "time"  
)  
  
// 一次交易  
type Transaction struct {  
 // 所有属性都是小写开头 因为它们不可变  
 amount int  
 from uuid.UUID  
 to uuid.UUID  
 createdAt time.Time  
}

在有了entity和value object,可以根据它们组成一个聚合体Customer,在聚合体里面存放domain的业务逻辑。一个聚合体一般只包含一个根entity,聚合体里面的字段也是小写的,因为它不会参与json的转换。

创建聚合(aggregate)

下面定义创建 aggregate 包 创建customer.go 文件,定义 Cusotmer聚合

  
package aggregate  
  
import (  
  "github.com/percybolmer/flower/entity"  
  "github.com/myname/flower/valueobject"  
)  
  
// Customer 聚合  
type Customer struct {  
  // person 是根entity,定义为指针类型是为了可以更改person信息  
 person *entity.Person   
 // 多个商品  
 products []*entity.Item   
 // 多个交易 非指针类型 vo不需要更新  
 transactions []valueobject.Transaction   
}

用工厂模式创建聚合

目前聚合是我们遇到的最复杂的一个组件,因为它会包含业务逻辑。DDD建议使用工厂模式来创建复杂的聚合。

  
var (  
 //错误定义  
 ErrInvalidPerson = errors.New("a customer has to have an valid person")  
)  
// 会做些创建时候的校验  
func NewCustomer(name string) (Customer, error) {  
   
 if name == "" {  
 return Customer{}, ErrInvalidPerson  
 }  
  
 // 创建person  
 person := &entity.Person{  
 Name: name,  
 ID: uuid.New(),  
 }  
 // 创建 Customer  
 return Customer{  
 person: person,  
 products: make([]*entity.Item, 0),  
 transactions: make([]valueobject.Transaction, 0),  
 }, nil  
}

仓储模式(Repository Pattern)

DDD里面把repository描述为存储聚合和管理聚合的地方。这里一般是定义一个XxRepository 接口,然后再定义具体的存储方案。这样设计的好处是可以灵活切换为其他的存储方式,不会破坏其他代码功能。

在这里我们先使用内存的方式存储,后面可以添加mysql等的存储实现。

现在在domain/customer 创建一个repository.go 文件。

  
package customer  
  
import (  
 "github.com/google/uuid"  
  "github.com/myname/flower/aggregate"  
)  
var (   
 ErrCustomerNotFound = errors.New("the customer was not found in the repository")  
  //  repository相关的错误  
  ErrFailedToAddCustomer = errors.New("failed to add the customer to the repository")  
 ErrUpdateCustomer = errors.New("failed to update the customer in the repository")  
)  
   
// CustomerRepository 这里以Get Add Update 为例子  
// 实际情况根据domain的业务定义业务方法  
type CustomerRepository interface {  
 Get(uuid.UUID) (aggregate.Customer, error)  
 Add(aggregate.Customer) error  
 Update(aggregate.Customer) error  
}

关于存储方案的实现的存放位置有2类观点,一个是把实现放在repository的同一个包下面,方面新人熟悉代码;一个是把实现放在更外面的包,以便它作为一个独立的小模块。

这里采用第一种方案。

接下来实现repository。

  
package memory  
  
import (  
 "sync"  
  
 "github.com/google/uuid"  
 "github.com/percybolmer/ddd-go/aggregate"  
)  
  
// 先定义MemoryRepository 结构体,定义需要操作的对象  
type MemoryRepository struct {  
 customers map[uuid.UUID]aggregate.Customer  
 //加锁  
 sync.Mutex  
}  
  
// 工厂模式  
func New() *MemoryRepository {  
 return &MemoryRepository{  
 customers: make(map[uuid.UUID]aggregate.Customer),  
 }  
}  
  
// 实现方法Get  
func (mr *MemoryRepository) Get(uuid.UUID) (aggregate.Customer, error) {  
 return aggregate.Customer{}, nil  
}  
  
// 实现方法Add  
func (mr *MemoryRepository) Add(aggregate.Customer) error {  
 return nil  
}  
  
// 实现方法Update  
func (mr *MemoryRepository) Update(aggregate.Customer) error {  
 return nil  
}

因为这里需要访问聚合Cusotmer的信息,所以需要给聚合定义一些getter/setter

  
  
func (c *Customer) GetID() uuid.UUID {  
 return c.person.ID  
}  
  
func (c *Customer) SetID(id uuid.UUID) {  
 if c.person == nil {  
 c.person = &entity.Person{}  
 }  
 c.person.ID = id  
}  
  
  
func (c *Customer) SetName(name string) {  
 if c.person == nil {  
 c.person = &entity.Person{}  
 }  
 c.person.Name = name  
}  
  
func (c *Customer) GetName() string {  
 return c.person.Name  
}

定义了repository之后,需要有其他组件来调用,这就是下一个组件Service。

service 是业务垃圾的主战场。可以灵活的组合已经存在的repository。一个service也可以组合其他的service。

创建service

service不同于MVC的service,DDD的service负责编排、转发、校验

下面来定义一个OrderService

  
package services  
  
import (  
  "github.com/myname/flower/domain/customer"  
)  
  
// 用来配置 orderservice  
type OrderConfiguration func(os *OrderService) error  
  
// OrderService   
type OrderService struct {  
 customers customer.CustomerRepository  
}  
  
//   
func NewOrderService(cfgs ...OrderConfiguration) (*OrderService, error) {  
 // 创建service  
 os := &OrderService{}  
    
 for _, cfg := range cfgs {  
 // 去配置 orderservice  
 err := cfg(os)  
 if err != nil {  
 return nil, err  
 }  
 }  
 return os, nil  
}

这里的OrderConfiguration一般是为了给OrderService4设置他自身需要包含的字段。

配置service

定义一个OrderConfiguration

  
// WithCustomerRepository 设置 a given customer repository to the OrderService  
func WithCustomerRepository(cr customer.CustomerRepository) OrderConfiguration {  
   
 return func(os *OrderService) error {  
 os.customers = cr  
 return nil  
 }  
}  
  
// WithMemoryCustomerRepository 设置 memory customer repository to the OrderService  
func WithMemoryCustomerRepository() OrderConfiguration {  
   
 cr := memory.New()  
 return WithCustomerRepository(cr)  
}

调用上面的方法是的配置生效

  
NewOrderService(WithMemoryCustomerRepository())

现在OrderService已经配置好了,开始写业务方法。

  
func (o *OrderService) CreateOrder(customerID uuid.UUID, productIDs []uuid.UUID) error {  
 // Get the customer  
 c, err := o.customers.Get(customerID)  
 if err != nil {  
 return err  
 }  
  
 // 获取商品 需要用到 ProductRepository  
  
 return nil  
}

到目前为止,orderservice的业务逻辑使用DDD的方式、已经基本写完了。到这里我们还缺少 Product 领域的DDD代码。

按照前面的顺序,我们开始写代码

  
package aggregate  
  
import (  
 "errors"  
  
 "github.com/google/uuid"  
  "github.com/myname/flower/entity"  
)  
  
var (  
   
 ErrMissingValues = errors.New("missing values")  
)  
  
// Product   
type Product struct {  
   
 item *entity.Item  
 price float64  
   
 quantity int  
}  
  
// NewProduct   
func NewProduct(name, description string, price float64) (Product, error) {  
 if name == "" || description == "" {  
 return Product{}, ErrMissingValues  
 }  
  
 return Product{  
 item: &entity.Item{  
 ID: uuid.New(),  
 Name: name,  
 Description: description,  
 },  
 price: price,  
 quantity: 0,  
 }, nil  
}  
  
func (p Product) GetID() uuid.UUID {  
 return p.item.ID  
}  
  
func (p Product) GetItem() *entity.Item {  
 return p.item  
}  
  
func (p Product) GetPrice() float64 {  
 return p.price  
}

PrductRepository

  
package product  
  
import (  
 "errors"  
  
 "github.com/google/uuid"  
  "github.com/myname/flower/aggregate"  
)  
  
var (  
   
 ErrProductNotFound = errors.New("the product was not found")  
   
 ErrProductAlreadyExist = errors.New("the product already exists")  
)  
  
// ProductRepository   
type ProductRepository interface {  
 GetAll() ([]aggregate.Product, error)  
 GetByID(id uuid.UUID) (aggregate.Product, error)  
 Add(product aggregate.Product) error  
 Update(product aggregate.Product) error  
 Delete(id uuid.UUID) error  
}

具体的存储实现

  
package memory  
  
import (  
 "sync"  
  
 "github.com/google/uuid"  
  "github.com/myname/flower/aggregate"  
  "github.com/myname/flower/domain/product"  
)  
  
type MemoryProductRepository struct {  
 products map[uuid.UUID]aggregate.Product  
 sync.Mutex  
}  
  
   
func New() *MemoryProductRepository {  
 return &MemoryProductRepository{  
 products: make(map[uuid.UUID]aggregate.Product),  
 }  
}  
  
   
func (mpr *MemoryProductRepository) GetAll() ([]aggregate.Product, error) {  
   
 var products []aggregate.Product  
 for _, product := range mpr.products {  
 products = append(products, product)  
 }  
 return products, nil  
}  
  
// GetByID   
func (mpr *MemoryProductRepository) GetByID(id uuid.UUID) (aggregate.Product, error) {  
 if product, ok := mpr.products[uuid.UUID(id)]; ok {  
 return product, nil  
 }  
 return aggregate.Product{}, product.ErrProductNotFound  
}  
  
// Add   
func (mpr *MemoryProductRepository) Add(newprod aggregate.Product) error {  
 mpr.Lock()  
 defer mpr.Unlock()  
  
 if _, ok := mpr.products[newprod.GetID()]; ok {  
 return product.ErrProductAlreadyExist  
 }  
  
 mpr.products[newprod.GetID()] = newprod  
  
 return nil  
}  
  
// Update   
func (mpr *MemoryProductRepository) Update(upprod aggregate.Product) error {  
 mpr.Lock()  
 defer mpr.Unlock()  
  
 if _, ok := mpr.products[upprod.GetID()]; !ok {  
 return product.ErrProductNotFound  
 }  
  
 mpr.products[upprod.GetID()] = upprod  
 return nil  
}  
  
// Delete   
func (mpr *MemoryProductRepository) Delete(id uuid.UUID) error {  
 mpr.Lock()  
 defer mpr.Unlock()  
  
 if _, ok := mpr.products[id]; !ok {  
 return product.ErrProductNotFound  
 }  
 delete(mpr.products, id)  
 return nil  
}

把 ProductRepository 配置到 orderservice

  
func WithMemoryProductRepository(products []aggregate.Product) OrderConfiguration {  
 return func(os *OrderService) error {  
 // Create the memory repo, if we needed parameters, such as connection strings they could be inputted here  
 pr := prodmemory.New()  
  
 // Add Items to repo  
 for _, p := range products {  
 err := pr.Add(p)  
 if err != nil {  
 return err  
 }  
 }  
 os.products = pr  
 return nil  
 }  
}

修改orderservice

  
func (o *OrderService) CreateOrder(customerID uuid.UUID, productIDs []uuid.UUID) (float64, error) {  
 // Get the customer  
 c, err := o.customers.Get(customerID)  
 if err != nil {  
 return 0, err  
 }  
  
  // Get  Product  
 var products []aggregate.Product  
 var price float64  
 for _, id := range productIDs {  
 p, err := o.products.GetByID(id)  
 if err != nil {  
 return 0, err  
 }  
 products = append(products, p)  
 price += p.GetPrice()  
 }  
  
   
 log.Printf("Customer: %s has ordered %d products", c.GetID(), len(products))  
  
 return price, nil  
}

到此,一个使用DDD方式组织的一个业务逻辑就完成了。

最后

为了不陷进前面对多个概念的模糊理解。上一张传统架构图和DDD架构图对比

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
抖音连麦音画质体验提升与进阶实践
随着互娱场景实时互动创新玩法层出不穷,业务伙伴对 RTC「体验」和「稳定」的要求越来越高。火山引擎 RTC 经历了抖音 6 亿 DAU 的严苛验证和打磨,在架构设计、音画质提升、高可靠服务等方面沉淀了丰富的经验,本次演讲将和大家分享火山引擎 RTC 在直播连麦等场景中的技术优化及其带来的新玩法。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论