DDD 分层架构
1. DDD 分层架构
问题:请描述 DDD 的四层架构
参考答案:
DDD 分层架构包含四个层次:用户接口层、应用层、领域层和基础设施层。
各层职责:
- 用户接口层:处理HTTP请求、参数验证、数据序列化
- 应用层:协调业务流程、事务管理、权限控制,不包含业务逻 辑
- 领域层:核心业务逻辑,包含实体、值对象、聚合、领域服务
- 基础设施层:技术实现,如数据库访问、外部服务调用
问题:在 Go 项目中如何组织 DDD 的目录结构?
参考答案:
project/
├── interfaces/ # 用户接口层
│ ├── http/ # HTTP 接口
│ ├── grpc/ # gRPC 接口
│ └── message/ # 消息队列
├── application/ # 应用层
│ ├── service/ # 应用服务
│ └── dto/ # 数据传输对象
├── domain/ # 领域层
│ ├── user/ # 用户聚合
│ │ ├── entity/ # 实体
│ │ ├── service/ # 领域服务
│ │ ├── repository/ # 仓储接口
│ │ └── event/ # 领域事件
│ └── order/ # 订单聚合
└── infrastructure/ # 基础设施层
├── repository/ # 仓储实现
├── config/ # 配置
└── client/ # 外部客户端
2. 聚合和实体
问题:什么是聚合?聚合根的作用是什么?请用 Go 代码示例说明
参考答案:
聚合是一组相关对象的集合,用于维护数据一致性和业务规则。聚合根是聚合的入口点,负责管理聚合内对象的一致性。
// 订单聚合根
type Order struct {
id OrderID
customerID CustomerID
items []*OrderItem
status OrderStatus
totalPrice Money
createdAt time.Time
}
// 订单项实体
type OrderItem struct {
id OrderItemID
productID ProductID
quantity int
price Money
}
// 聚合根方法 - 添加订单项
func (o *Order) AddItem(productID ProductID, quantity int, price Money) error {
if o.status != OrderStatusDraft {
return errors.New("只能向草稿状态的订单添加商品")
}
item := &OrderItem{
id: NewOrderItemID(),
productID: productID,
quantity: quantity,
price: price,
}
o.items = append(o.items, item)
o.calculateTotalPrice()
return nil
}
// 内部方法维护一致性
func (o *Order) calculateTotalPrice() {
total := Money(0)
for _, item := range o.items {
total += Money(item.quantity) * item.price
}
o.totalPrice = total
}
问题:聚合设计的边界原则是什么?
参考答案:
聚合边界设计原则:
- 事务边界:一个聚合内的修改在同一个事务中完成
- 不变性约束:聚合内的业务规则必须始终保持一致
- 小聚合原则:聚合应该尽可能小,避免性能问题
- 通过ID引用:聚合间通过ID引用,不直接持有对象引用
3. 值对象和实体
问题:值对象和实体的区别是什么?请用 Go 代码举例说明
参考答案:
实体:有唯一标识,可变,有生命周期 值对象:无标识,不可变,通过属性值判断相等性
// 实体 - 用户
type User struct {
id UserID // 唯一标识
username string
email Email // 值对象
address Address // 值对象
version int // 乐观锁版本号
}
func (u *User) ID() UserID {
return u.id
}
func (u *User) ChangeEmail(newEmail Email) error {
if !newEmail.IsValid() {
return errors.New("无效的邮箱地址")
}
u.email = newEmail
return nil
}
// 值对象 - 邮箱
type Email struct {
value string
}
func NewEmail(email string) (Email, error) {
if !isValidEmail(email) {
return Email{}, errors.New("无效的邮箱格式")
}
return Email{value: email}, nil
}
func (e Email) String() string {
return e.value
}
func (e Email) Equals(other Email) bool {
return e.value == other.value
}
func (e Email) IsValid() bool {
return isValidEmail(e.value)
}
// 值对象 - 地址
type Address struct {
province string
city string
district string
detail string
}
func (a Address) Equals(other Address) bool {
return a.province == other.province &&
a.city == other.city &&
a.district == other.district &&
a.detail == other.detail
}
4. 领域服务
问题:什么时候需要使用领域服务?请举例说明
参考答案:
当业务逻辑不适合放在单个实体或值对象中时,需要使用领域服务:
- 跨多个聚合的业务逻辑
- 复杂的业务规则
- 无状态的领域操作
// 领域服务 - 转账服务
type TransferService struct {
accountRepo AccountRepository
}
func (s *TransferService) Transfer(
fromAccountID, toAccountID AccountID,
amount Money,
) error {
// 跨聚合的业务逻辑
fromAccount, err := s.accountRepo.FindByID(fromAccountID)
if err != nil {
return err
}
toAccount, err := s.accountRepo.FindByID(toAccountID)
if err != nil {
return err
}
// 业务规则检查
if fromAccount.Balance().LessThan(amount) {
return errors.New("余额不足")
}
if fromAccount.IsBlocked() || toAccount.IsBlocked() {
return errors.New("账户被冻结")
}
// 执行转账
if err := fromAccount.Withdraw(amount); err != nil {
return err
}
if err := toAccount.Deposit(amount); err != nil {
// 回滚
fromAccount.Deposit(amount)
return err
}
return nil
}
5. 领域事件
问题:领域事件的作用是什么?如何在 Go 中实现领域事件?
参考答案:
领域事件用于:
- 解耦聚合间的通信
- 实现最终一致性
- 支持事件溯源
- 触发副作用操作
// 领域事件接口
type DomainEvent interface {
EventType() string
AggregateID() string
OccurredOn() time.Time
}
// 用户注册事件
type UserRegisteredEvent struct {
userID UserID
email Email
occurredOn time.Time
}
func (e UserRegisteredEvent) EventType() string {
return "UserRegistered"
}
func (e UserRegisteredEvent) AggregateID() string {
return e.userID.String()
}
func (e UserRegisteredEvent) OccurredOn() time.Time {
return e.occurredOn
}
// 聚合根基类
type AggregateRoot struct {
events []DomainEvent
}
func (ar *AggregateRoot) RaiseEvent(event DomainEvent) {
ar.events = append(ar.events, event)
}
func (ar *AggregateRoot) GetEvents() []DomainEvent {
return ar.events
}
func (ar *AggregateRoot) ClearEvents() {
ar.events = nil
}
// 用户聚合
type User struct {
AggregateRoot
id UserID
email Email
}
func (u *User) Register(email Email) {
u.email = email
// 发布事件
u.RaiseEvent(UserRegisteredEvent{
userID: u.id,
email: email,
occurredOn: time.Now(),
})
}
事件发布流程:
6. 限界上下文
问题:什么是限界上下文?如何在微服务架构中应用?
参考答案:
限界上下文定义了统一语言的边界,每个上下文内有自己的 领域模型:
在 Go 中的实现:
// 订单上下文中的客户概念
package order
type Customer struct {
ID CustomerID
Name string
}
// 支付上下 文中的客户概念
package payment
type Customer struct {
ID CustomerID
PaymentMethod string
CreditLimit Money
}
// 上下文间通过事件通信
type OrderCreatedEvent struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
Amount int64 `json:"amount"`
}
7. 贫血模型 vs 充血模型
问题:贫血模型和充血模型的区别?在 Go 项目中如何选择?
参考答案:
贫血模型:数据和行为分离
// 贫血模型 - 只有数据
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Status int `json:"status"`
}
// 行为在服务层
type UserService struct {
userRepo UserRepository
}
func (s *UserService) ActiveUser(userID int64) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return err
}
user.Status = 1 // 激活状态
return s.userRepo.Save(user)
}
充血模型:数据和行为封装在一起
// 充血模型 - 包含业务逻辑
type User struct {
id UserID
username string
email Email
status UserStatus
}
func (u *User) Activate() error {
if u.status == UserStatusBlocked {
return errors.New("被封禁的用户无法激活")
}
u.status = UserStatusActive
return nil
}
func (u *User) Block(reason string) error {
if u.status == UserStatusBlocked {
return errors.New("用户已被封禁")
}
u.status = UserStatusBlocked
// 可以发布领域事件
return nil
}
选择建议:
- 简单CRUD项目:贫血模型,开发快速
- 复杂业务逻辑:充血模型,更好的封装性
- Go项目特点:Go的组合特性适合充血模型
8. 仓储模式
问题:Repository 模式的作用是什么?如何在 Go 中实现?
参考答案:
Repository 封装了数据访问逻辑,提供类似集合的接口:
// 领域层定义接口
type UserRepository interface {
Save(user *User) error
FindByID(id UserID) (*User, error)
FindByEmail(email Email) (*User, error)
Delete(id UserID) error
}
// 基础设施层实现
type MySQLUserRepository struct {
db *sql.DB
}
func (r *MySQLUserRepository) Save(user *User) error {
query := `
INSERT INTO users (id, username, email, status)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
username=VALUES(username),
email=VALUES(email),
status=VALUES(status)
`
_, err := r.db.Exec(query,
user.ID().String(),
user.Username(),
user.Email().String(),
user.Status(),
)
return err
}
func (r *MySQLUserRepository) FindByID(id UserID) (*User, error) {
query := "SELECT id, username, email, status FROM users WHERE id = ?"
var userID, username, email string
var status int
err := r.db.QueryRow(query, id.String()).Scan(
&userID, &username, &email, &status,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
}
return nil, err
}
emailVO, _ := NewEmail(email)
return ReconstructUser(
NewUserID(userID),
username,
emailVO,
UserStatus(status),
), nil
}