写作是为了让过去的自己有迹可循
人总是习惯按自己的想法去思考,
而不是按照别人的想法去思考
领域驱动设计在20多年前就已经提了出来,现在也有很多人提到DDD,但是很多都是理论分析,没有手把手的去在项目里面实践DDD。这次开始从0写一个DDD的项目来学习DDD是如何构建项目的。
还没读过DDD相关的书的同学建议多去读一下,里面有许多的专业术语,不需要读的太懂。、因为单纯的概念解释不能形成一个具象的如何实践代码的指导。
简单来讲和DDD对应的常用的一个模式是MVC。无论是哪类模式,其实都是对完成一个业务需求的代码如何组织的问题。
MVC把整个代码分为三层,每一层负责自己的职责,这个模式最大的特点是把业务逻辑都放在service,在一个业务复杂的项目里面,有的service可能会写到3000行甚至更多。市面上也有不少的基于MVC模式开发的框架,如前面介绍的beego。那么DDD作为一种组织业务代码的思想,它和MVC的组织方式有什么不同呢?
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:一类可变的对象
创建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架构图对比