领域驱动设计(DDD)
领域驱动设计
在软件所属领域之后对软件进行结构化和建模的一种方法,首先考虑编写软件的领域,软件的编写应该反映该领域
领域:软件处理的主题或问题
DDD主张工程团队必须与主题专家(SME)交谈,将知识反映在软件中
实体
创建entity,存放实体
实体:一个结构体包含标志符,状态可能会改变(实体的值可以改变)
// entity包保存所有子领域共享的所有实体
// 一个结构体具有唯一标识符来引用,状态可变package entityimport "github.com/google/uuid"// 实体
// Item表示所有子领域的Item
// bson表示存储在mongodb中的结构体type Item struct {ID uuid.UUID `json:"id" bson:"id"`Name string `json:"name" bson:"name"`Description string `json:"description" bson:"description"`
}
package entityimport "github.com/google/uuid"// 实体
// Person 在所有领域中代表人
type Person struct {// ID是实体的标识符,该ID为所有子领域共享ID uuid.UUID `json:"id" bson:"id"`//Name就是人的名字Name string `json:"name" bson:"name"`// 人的年龄Age int `json:"age" name:"age"`
}
值对象
结构体不可变,不需要唯一标识符
结构体在创建时没有标识符和持久化值
值对象通常位于领域内,用于描述该领域的某些方面
package valueobjectimport ("github.com/google/uuid""time"
)// 值对象结构体不可变,不需要唯一标识符和持久化值
// 持久化值:没有唯一标识,但本身具有完整意义的对象
// 相等值通过值判断而不是身份
// 只要Transaction的字段值是都相同的,那么就认为两个Transaction是相等的,不依赖唯一id
// 即时存储在数据库中,只需保存这些字段值,不需要通过id识别// Transaction 表示双方用于支付
// 一旦事务被执行,它就不能改变状态
type Transaction struct {Amount int `json:"amount" bson:"amount"`From uuid.UUID `json:"from" bson:"from"`To uuid.UUID `json:"to" bson:"to"`CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
}
聚合
一组实体和值对象的组合
聚合时领域概念
聚合的目的:
业务逻辑将应用于聚合,而不是每个持有该逻辑的实体
聚合不允许直接访问底层实体
聚合中应该只有一个实体作为根实体,意味着根实体的引用也用于聚合
package aggregateimport ("errors""github.com/google/uuid""test/DDD/entity""test/DDD/valueobject"
)// 聚合
// 一组实体和值对象的组合,用来表示一个业务实体
// 只有一个实体作为根实体
// 对于一个聚合来说实体的id是唯一标识符var (// 当person在newcustom工厂中无效时返回ErrInvalidPersonErrInvalidPerson = errors.New("a customer has to have an valid person")
)// 因为要实现聚合可序列化,所以会选择跳过不能访问底层实体的规则
// Customer 聚合包含了代表一个客户所需的所有实体
// 将实体设为指针,是因为实体可以改变状态
// 想反映在运行时所有访问它的实例中
// 保存为非指针不能改变状态
type Customer struct {// Person是客户的根实体// person.ID是聚合的主标识符Person *entity.Person `bson:"person"`//一个客户可以持有许多产品Products []*entity.Item `bson:"products"`// 一个客户可以执行许多事务Transactions []valueobject.Transaction `bson:"transactions"`
}
工厂函数
工厂模式是一种设计模式,在创建所需实例的函数中封装复杂逻辑,调用者不需要知道其中细节
DDD建议使用工厂创建复杂的聚合,仓库和服务
(在实际应用中,会在领域中包含聚合的Customer和工厂)
// NewCustomer 是创建新的Customer聚合的工厂
// 它将验证名称是否为空
func NewCustomer(name string) (Customer, error) {// 验证Name不是空的if name == "" {return Customer{}, ErrInvalidPerson}// 创建一个新person并生成IDperson := &entity.Person{Name: name,ID: uuid.New(),}// 创建一个customer对象并初始化所有的值以避免空指针异常return Customer{Person: person,Products: make([]*entity.Item, 0),Transactions: make([]valueobject.Transaction, 0),}, nil
}
开启单元测试
package aggregate_testimport ("test/DDD/aggregate""testing"
)func TestCustomer_NewCustomer(t *testing.T) {// 构建我们需要的测试用例数据结构type testCase struct {test stringname stringexpectedErr error}//创建新的测试用例testCases := []testCase{{test: "Empty Name validation",name: "",expectedErr: aggregate.ErrInvalidPerson,}, {test: "Valid Name",name: "Percy Bolmer",expectedErr: nil,},}for _, tc := range testCases {// Run Testst.Run(tc.test, func(t *testing.T) {//创建新的customer_, err := aggregate.NewCustomer(tc.name)//检查错误是否与预期的错误匹配if err != tc.expectedErr {t.Errorf("Expected error %v, got %v", tc.expectedErr, err)}})}}