在使用 Kratos 框架开发云服务的过程中,渐渐理解和感受到“领域层”这个概念和抽象的强大之处,它可以将业务和存储细节解耦、将业务和开发初期频繁变更的API结构,让Mock单元测试变得更加容易、对细节的变化更鲁棒。让业务代码摆脱技术细节依赖,使系统变更成本与业务复杂度解耦,领域层将领域层的放置位置应当遵循 “靠近使用者” 原则。以下是常见的分层方案和决策依据:
一、推荐架构(领域层独立)
.
├── internal
│ ├── domain # 核心领域层(独立包)
│ │ ├── user.go # 聚合根/值对象定义
│ │ ├── repository.go # 仓储接口
│ │ └── service.go # 领域服务
│ │
│ ├── service # 应用服务层
│ │ └── user.go # 依赖domain包
│ │
│ └── data # 数据层
│ └── user_repo.go # 实现domain.Repository
二、分层决策依据
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
领域层独立 | 1. 双向解耦 2. 明确分层界限 | 1. 多一个包目录 2. 需严格依赖管理 | 中大型项目 复杂业务逻辑 |
领域层在service | 1. 减少包数量 2. service直接使用 | 1. data层需反向依赖service 2. 易产生循环引用 | 小型项目 快速原型开发 |
领域层在data | 1. 数据层自包含 | 1. service层被迫依赖data 2. 破坏分层架构 | 不推荐 |
三、具体实施示例
1. 独立领域层(推荐)
// internal/domain/user.go
package domaintype User struct {ID UserIDName string
}type UserRepository interface {FindByID(context.Context, UserID) (*User, error)
}// internal/service/user.go
package servicetype UserService struct {repo domain.UserRepository // 依赖抽象
}// internal/data/user_repo.go
package datatype userRepo struct {db *gorm.DB
}func (r *userRepo) FindByID(ctx context.Context, id domain.UserID) (*domain.User, error) {// 实现domain.Repository
}
2. 领域层在service(备选)
// internal/service/domain/user.go
package servicetype User struct {ID UserIDName string
}type UserRepository interface {FindByID(context.Context, UserID) (*User, error)
}// internal/data/user_repo.go
package dataimport "project/internal/service"type userRepo struct {db *gorm.DB
}func (r *userRepo) FindByID(ctx context.Context, id service.UserID) (*service.User, error) {// 需要依赖service包
}
四、关键设计原则
-
依赖方向不可逆
高层模块(service) → 领域抽象(domain) ← 实现细节(data)
- 永远保持
data
依赖domain
,而不是相反 - 禁止
domain
导入data
或service
的任何代码
- 永远保持
-
接口所有权
- 领域接口(如
Repository
)由 领域层定义 data
层仅负责实现这些接口
- 领域接口(如
-
类型系统控制
// 正确定义(领域层拥有类型) type UserID string// 错误定义(data层定义类型) type User struct {gorm.Model // 基础设施细节泄露 }
-
依赖注入管理
// wire.go 中明确依赖关系 func newService(repo domain.UserRepository) *service.UserService {return &service.UserService{repo: repo} }
五、Kratos 特化建议
1. 项目结构优化
.
├── api # proto定义
├── internal
│ ├── biz # 领域层(原domain)
│ │ ├── user.go
│ │ └── repo.go # 仓储接口
│ ├── service # 应用服务
│ └── data # 数据实现
└── pkg # 通用领域组件
2. 接口定义示例
// internal/biz/repo.go
package biztype UserRepo interface {Save(context.Context, *User) errorFindByID(context.Context, int64) (*User, error)
}// internal/data/user.go
package datatype userRepo struct {data *Data
}func (r *userRepo) FindByID(ctx context.Context, id int64) (*biz.User, error) {// 转换data -> biz
}
3. Wire 注入配置
// internal/data/data.go
func NewData() (*Data, func(), error) {// 初始化数据库等
}// internal/data/user.go
func NewUserRepo(data *Data) biz.UserRepo {return &userRepo{data: data}
}// internal/service/service.go
func NewService(repo biz.UserRepo) *Service {return &Service{repo: repo}
}
六、何时打破原则
在以下场景可考虑将领域模型放在 service
层:
- 原型验证阶段:快速迭代时减少目录切换
- 简单CRUD服务:无复杂业务规则
- 独立微服务:服务边界即领域边界
但需保证:
data
层不包含业务逻辑- 随时可以重构为独立领域层
七、架构验证检查表
-
编译时验证
# 确保data不依赖service grep -r "import.*service" internal/data/
-
循环依赖检测
go mod graph | awk '{print $1}' | sort | uniq -c | sort -nr
-
接口所有权检查
# 确认Repository接口定义在domain/biz ls internal/domain/repo.go
-
类型定义位置
# 核心类型不应包含gorm标签 grep -r "gorm:" internal/domain/
通过这种分层,您将获得:
- 明确架构边界:各层职责清晰
- 测试友好:可轻松Mock仓储实现
- 演进自由:数据源切换不影响业务逻辑
- 团队协作:领域专家可独立设计模型
- 附录
Kratos 官方架构中与传统DDD概念的对应
Kratos 结构 | 传统DDD概念 | 核心职责 |
---|---|---|
internal/biz | 领域层 (Domain) | 定义聚合根/实体/值对象/领域服务 |
internal/data | 基础设施层 (Infra) | 实现仓储接口、外部API适配 |
internal/service | 应用层 (Application) | 协调领域对象、处理业务流程 |
在下篇文章我们讨论在同时对容变API和数据层同时应如领域层隔离的案例。Golang Kratos 系列:业务分层的若干思考(二)