前言
最近在看 coze studio 的源码,在它的开发规范里,写着 coze studio 基于领域驱动设计(DDD)原则架构实现的,DDD是个啥?我结合2021年我在内部多次分享后的总结再结合coze的源码详解下。之前的分享链接DDD应用架构内部分享 (之前是基于java的应用实践)
想要二开coze studio,最好了解下其架构模式,以及使用方法,和传统开发的区别还是比较大的。
从代码结构上看,coze studio 严格按照 DDD 的分层架构进行组织:
├── backend/ # 后端服务
│ ├── api/ # API 处理器和路由
│ ├── application/ # 应用层,组合领域对象和基础设施实现
│ ├── domain/ # 领域层,包含核心业务逻辑
│ ├── infra/ # 基础设施实现层
│ ├── crossdomain/ # 跨领域防腐层
│ ├── pkg/ # 无外部依赖的工具方法
│ └── types/ # 类型定义
想要了解DDD,让我们从传统开发的弊病说起。
传统开发的弊病
在分析 coze studio 的 DDD 实践之前,先看看传统开发模式的问题:
1. 事务脚本模式的局限
传统开发往往采用面向过程的事务脚本模式,按照请求流程组织代码:
// 传统的事务脚本模式
func CreateUser(req *CreateUserRequest) (*CreateUserResponse, error) {
// 1. 参数校验
if req.Name == "" {
returnnil, errors.New("name is required")
}
// 2. 查询数据库
user, err := userDAO.GetByName(req.Name)
if err != nil {
returnnil, err
}
// 3. 业务逻辑处理
if user != nil {
returnnil, errors.New("user already exists")
}
// 4. 数据入库
newUser := &User{
Name: req.Name,
CreateTime: time.Now(),
}
err = userDAO.Create(newUser)
return &CreateUserResponse{UserID: newUser.ID}, err
}
这种模式虽然简单直接,但随着业务复杂度增长,会出现严重问题:
-
• 业务逻辑散落各处:同样的业务规则在多个地方重复实现 -
• 代码变成"大泥球":各种逻辑混杂在一起,难以维护 -
• 技术和业务耦合:一个场景一个服务,代码像流水线 -
• 缺乏业务语言:技术人员和产品人员无法对齐
2. 以数据库为中心的设计
传统开发过于重视数据库,围绕数据库和数据模型进行建模:
// 传统的数据驱动设计
type User struct {
ID int64 `gorm:"primary_key"`
Name string `gorm:"column:name"`
Email string `gorm:"column:email"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
}
func (u *User) CreateUser() error {
return db.Create(u).Error
}
这种设计的问题:
-
• 贫血模型:对象只有数据,没有行为 -
• 业务逻辑外泄:核心业务逻辑散落在 Service 层 -
• 难以测试:业务逻辑与数据库紧耦合
DDD 是个啥
解决了什么
DDD (Domain-Driven Design) 领域驱动设计是对面向对象设计的改进,专门用于开发复杂业务逻辑的一种方式。它主要解决:
-
1. 通用语言问题:让开发和业务在语言上统一 -
2. 边界划分问题:通过限界上下文来划分,在不同的情景下域的作用是不同 -
3. 领域模型问题:业务模型独立且模型具备可测试性 -
4. 技术独立性:技术实现独立,可以随着业务的发展不断地更迭
战略设计 vs 战术设计
DDD 分为两个层面的设计方法:
战略设计 (Strategic Design):关注业务架构和领域边界
-
• 限界上下文:定义领域模型的边界,在边界内保持模型的一致性 -
• 领域划分:区分核心域、子域、支撑域的重要性 -
• 上下文映射:定义不同限界上下文之间的关系
战术设计 (Tactical Design):关注领域内部的实现细节
-
• 实体、值对象、聚合根:领域模型的基本构建块 -
• 领域服务、仓储、工厂:领域逻辑的组织方式 -
• 领域事件:领域间的解耦通信
在 coze studio 中:
-
• 战略设计体现:按业务能力划分的 8 个领域 (Agent、app、knowledge 等) -
• 战术设计体现:每个领域内部的实体、服务、仓储实现
在coze studio中的应用
让我们看看 coze studio 是如何用DDD解决问题的
1. 业务和技术能够通过领域模型建立通用语言
在 coze studio 中,每个领域都有清晰的业务术语:
// backend/domain/knowledge/entity/knowledge.go
type Knowledge struct {
*knowledge.Knowledge
}
// backend/domain/workflow/entity/workflow.go
type Workflow struct {
ID int64
CommitID string
*vo.Meta
*vo.CanvasInfo
*vo.DraftMeta
*vo.VersionMeta
}
// backend/domain/app/entity/app.go
type APP struct {
ID int64
SpaceID int64
Name *string
Desc *string
// ...业务属性
}
这些实体名称直接对应业务概念,产品和技术可以用同样的语言交流。
2. 业务边界清晰
coze studio 按照业务领域划分了清晰的模块:
domain/
├── agent/ # 智能体领域
├── app/ # 应用领域
├── knowledge/ # 知识库领域
├── workflow/ # 工作流领域
├── plugin/ # 插件领域
├── memory/ # 记忆领域
├── user/ # 用户领域
└── conversation/ # 对话领域
每个领域都有独立的实体、服务和仓储,边界清晰。
3. 业务模型独立且具备可测试性
以 knowledge 领域为例,业务逻辑封装在领域服务中:
// backend/domain/knowledge/service/knowledge.go
func (k *knowledgeSVC) CreateKnowledge(ctx context.Context, request *CreateKnowledgeRequest) (response *CreateKnowledgeResponse, err error) {
// 业务规则验证
iflen(request.Name) == 0 {
returnnil, errorx.New(errno.ErrKnowledgeInvalidParamCode, errorx.KV("msg", "knowledge name is empty"))
}
if request.CreatorID == 0 {
returnnil, errorx.New(errno.ErrKnowledgeInvalidParamCode, errorx.KV("msg", "knowledge creator id is empty"))
}
// 生成业务ID
id, err := k.idgen.GenID(ctx)
if err != nil {
returnnil, errorx.New(errno.ErrKnowledgeIDGenCode)
}
// 创建领域对象
if err = k.knowledgeRepo.Create(ctx, &model.Knowledge{
ID: id,
Name: request.Name,
CreatorID: request.CreatorID,
// ... 其他业务属性
}); err != nil {
returnnil, errorx.New(errno.ErrKnowledgeDBCode, errorx.KV("msg", err.Error()))
}
return &CreateKnowledgeResponse{
KnowledgeID: id,
CreatedAtMs: now,
}, nil
}
这个服务可以独立测试,不依赖具体的技术手段实现(就是一段逻辑,因为是在go中所以是用go代码实现,但是不管哪个语言,逻辑是不变的)。
名词概念解析
事务脚本 vs 领域模型
事务脚本:根据接口请求,将业务逻辑通过面向过程组织为解决方案的过程。
-
• 特点:简单、快速、但扩展性差,业务复杂后代码容易变成"大泥球"
领域模型:通过面向对象的设计,将业务实现模型化(将类的状态和行为分离)。 -
• 特点:还原现实世界、责任清晰、边界清晰、扩展性强、测试性强
四种领域模型
失血模型:领域对象只包含 getter/setter 方法,业务逻辑放到 service
// 失血模型 - 只有数据,没有行为
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func (u *User) GetID() int64 { return u.ID }
func (u *User) SetName(name string) { u.Name = name }
贫血模型:包含属性的 getter/setter 和部分业务逻辑,但核心逻辑在 service 层
// 贫血模型 - 有基本验证,核心逻辑外泄
type User struct {
ID int64
Name string
}
func (u *User) IsValid() bool {
return u.Name != "" // 简单验证
}
// 核心业务逻辑在 UserService 中
充血模型:包含属性的 getter/setter 和所有业务逻辑,这是 DDD 提倡的模型
// 充血模型 - coze studio 的实践
type Workflow struct {
ID int64
CommitID string
*vo.Meta
*vo.CanvasInfo
}
func (w *Workflow) GetBasic() *WorkflowBasic {
// 业务逻辑封装在实体内部
var version string
if w.VersionMeta != nil {
version = w.VersionMeta.Version
}
return &WorkflowBasic{
ID: w.ID,
Version: version,
CommitID: w.CommitID,
}
}
胀血模型:删除 Service 层,所有逻辑都放到模型中,模型直接对接 web 层(不推荐)
实体 (Entity)
具有持久化 ID 的对象,能唯一标识一个记录。在 coze studio 中:
// backend/domain/user/entity/user.go
type User struct {
UserID int64 // 唯一标识
Name string // 业务属性
UniqueName string
Email string
// ... 其他属性
CreatedAt int64 // 生命周期属性
UpdatedAt int64
}
特点:
-
• 标识唯一不可变:UserID 贯穿整个生命周期 -
• 连续性:可以追踪整个生命周期 -
• 包含行为:不仅仅是数据,还有业务方法
值对象 (Value Object)
用于描述状态的属性,脱离了主体对象就没有任何意义。在 coze studio 中:
// backend/domain/memory/variables/entity/variable_meta.go
type VariableMeta struct {
Keyword string
DefaultValue string
VariableType project_memory.VariableType
Channel project_memory.VariableChannel
Description string
Enable bool
// ...
}
func (v *VariableMeta) ToProjectVariable() *project_memory.Variable {
// 值对象的转换方法
}
值对象特点:
-
• 不可变性:一旦创建不可修改 -
• 无标识:通过属性值判断相等性 -
• 描述性:描述实体的某个方面
聚合根 (Aggregate Root)
把一组有相同生命周期、在业务上不可分离的实体和值对象放在一起。在 coze studio 中:
// backend/domain/workflow/entity/workflow.go
type Workflow struct {
ID int64 // 聚合根标识
CommitID string
*vo.Meta // 值对象:元数据
*vo.CanvasInfo // 值对象:画布信息
*vo.DraftMeta // 值对象:草稿元数据
*vo.VersionMeta // 值对象:版本元数据
}
func (w *Workflow) GetBasic() *WorkflowBasic {
// 聚合根提供的业务方法
var version string
if w.VersionMeta != nil {
version = w.VersionMeta.Version
}
return &WorkflowBasic{
ID: w.ID,
Version: version,
SpaceID: w.SpaceID,
AppID: w.AppID,
CommitID: w.CommitID,
}
}
聚合根特点:
-
• 边界清晰:定义了一致性边界 -
• 访问入口:外部只能通过聚合根访问内部对象 -
• 事务单元:作为数据修改的一个单元
领域事件
由特定领域触发的已发生的行为事件。coze studio 中的事件处理:
// backend/domain/knowledge/internal/events/events.go
type IndexDocumentEvent struct {
KnowledgeID int64
Document *entity.Document
}
type DeleteKnowledgeDataEvent struct {
KnowledgeID int64
SliceIDs []int64
}
事件特点:
-
• 过去时态:描述已经发生的事情 -
• 领域相关:属于特定的业务领域 -
• 解耦工具:不同领域通过事件通信
工厂 (Factory)
负责聚合根、实体的创建。在 coze studio 中通过转换函数实现:
// backend/application/knowledge/convertor.go
func convertDocument2Model(document *entity.Document) *dataset.DocumentInfo {
if document == nil {
return &dataset.DocumentInfo{}
}
return &dataset.DocumentInfo{
DocumentID: document.ID,
Name: document.Name,
CreateTime: int32(document.CreatedAtMs / 1000),
UpdateTime: int32(document.UpdatedAtMs / 1000),
// ... 其他转换逻辑
}
}
工厂的作用:
-
• 封装创建逻辑:复杂对象的创建过程 -
• 数据转换:不同层次间的数据转换 -
• 保证一致性:确保创建的对象是有效的
建模
什么是建模?
建模分为数据建模和业务建模。在 coze studio 中,业务建模体现在:
1. 领域划分
按照业务能力划分领域
// 知识库领域的核心业务
func (k *knowledgeSVC) CreateKnowledge(ctx context.Context, request *CreateKnowledgeRequest)
func (k *knowledgeSVC) CreateDocument(ctx context.Context, request *CreateDocumentRequest)
func (k *knowledgeSVC) CreateSlice(ctx context.Context, request *CreateSliceRequest)
// 工作流领域的核心业务
func (s *serviceImpl) Create(ctx context.Context, meta *vo.MetaCreate) (int64, error)
func (s *serviceImpl) Save(ctx context.Context, id int64, schema string) error
func (s *serviceImpl) Publish(ctx context.Context, policy *vo.PublishPolicy) error
这块是有领域专家和技术专家共同协作来落地的。在DDD中,技术人员需要懂业务,懂业务以后才能更好的做好领域的划分。
2. 通用语言提取
从代码中可以看到清晰的业务概念:
-
• Knowledge(知识库)、Document(文档)、Slice(切片) -
• Workflow(工作流)、Node(节点)、Edge(边) -
• Agent(智能体)、Plugin(插件)、Memory(记忆)
这些概念直接对应业务术语也是由业务专家和技术共同协作,确保技术和产品认知一致,降低后续的沟通成本。
架构
DDD 分层架构
coze studio 严格按照 DDD 分层架构实现:
┌─────────────────────────────────────┐
│ ④ API 层 │ ← 处理 HTTP 请求,协议转换
├─────────────────────────────────────┤
│ ③ Application 层 │ ← 应用服务,组装领域对象
├─────────────────────────────────────┤
│ ① Domain 层 │ ← 核心业务逻辑,领域模型
├─────────────────────────────────────┤
│ ① CrossDomain 层 │ ← 跨领域防腐层
├─────────────────────────────────────┤
│ ② Infrastructure 层 │ ← 基础设施实现
└─────────────────────────────────────┘
按照正常的分层结构,有严格的依赖关系的,我把正常的依赖关系做了一个顺序标注。并按照顺序从底向上讲解。
1. Domain 层
包含核心业务逻辑,是整个系统的核心。
// 业务模型
// backend/domain/knowledge/service/knowledge.go
type knowledgeSVC struct {
knowledgeRepo repository.KnowledgeRepo // 仓储接口
documentRepo repository.KnowledgeDocumentRepo
sliceRepo repository.KnowledgeDocumentSliceRepo
idgen idgen.IDGenerator // 基础设施接口
storage storage.Storage
producer eventbus.Producer
// ...
}
// 给业务模型增加业务实现
func (k *knowledgeSVC) CreateKnowledge(ctx context.Context, request *CreateKnowledgeRequest) (response *CreateKnowledgeResponse, err error) {
// 业务规则验证
iflen(request.Name) == 0 {
returnnil, errorx.New(errno.ErrKnowledgeInvalidParamCode, errorx.KV("msg", "knowledge name is empty"))
}
// 业务逻辑处理
now := time.Now().UnixMilli()
id, err := k.idgen.GenID(ctx)
if err != nil {
returnnil, errorx.New(errno.ErrKnowledgeIDGenCode)
}
// 创建领域对象
if err = k.knowledgeRepo.Create(ctx, &model.Knowledge{
ID: id,
Name: request.Name,
CreatorID: request.CreatorID,
AppID: request.AppID,
SpaceID: request.SpaceID,
CreatedAt: now,
UpdatedAt: now,
Status: int32(knowledgeModel.KnowledgeStatusEnable),
Description: request.Description,
IconURI: request.IconUri,
FormatType: int32(request.FormatType),
}); err != nil {
returnnil, errorx.New(errno.ErrKnowledgeDBCode, errorx.KV("msg", err.Error()))
}
return &CreateKnowledgeResponse{
KnowledgeID: id,
CreatedAtMs: now,
}, nil
}
这段代码是在业务模型knowledgeSVC中有一个CreateKnowledge的方法,在CreateKnowledge 有一套业务逻辑,也就是DDD中的充血模型。
在这里经常做一些必要逻辑,也就是不怎么变的逻辑。
所以整个Domain 层特点:
-
• 业务规则:包含所有核心业务逻辑 -
• 技术无关:不依赖具体技术实现 -
• 可测试性:可以独立进行单元测试 -
• 稳定性:业务规则相对稳定,不易变化
1. CrossDomain 层
处理跨领域的防腐,定义跨领域接口:
// backend/crossdomain/contract/crossplugin/cross_plugin.go
type PluginService interface {
MGetVersionPlugins(ctx context.Context, versionPlugins []model.VersionPlugin) (plugins []*model.PluginInfo, err error)
MGetPluginLatestVersion(ctx context.Context, pluginIDs []int64) (resp *model.MGetPluginLatestVersionResponse, err error)
BindAgentTools(ctx context.Context, agentID int64, toolIDs []int64) (err error)
ExecuteTool(ctx context.Context, req *model.ExecuteToolRequest, opts ...model.ExecuteToolOpt) (resp *model.ExecuteToolResponse, err error)
// ...
}
在防腐层其实也是接口,不管是否跨领域,最好都有一层防腐层,比如操作数据库,发送短信这种,底层实现可以随意换。在coze studio中,这一层重点作为领域防腐层(CrossDomain)。一般情况下接口定义在domain中,在基础设施层实现,这边单独剥离了一层。
所以CrossDomain 层的作用:
-
• 防腐层:防止领域间直接依赖 -
• 接口定义:定义跨领域的标准接口 -
• 适配转换:处理不同领域间的数据转换
2. Infrastructure 层
这一层提供具体的技术实现,比如数据库可以用多种厂商,文件存储也可以多个厂商,可以通过配置等动态切换。但是对模型来说,只关注与接口层
// backend/infra/contract/storage/storage.go
type Storage interface {
PutObject(ctx context.Context, objectKey string, content []byte, opts ...PutOptFn) error
GetObject(ctx context.Context, objectKey string) ([]byte, error)
DeleteObject(ctx context.Context, objectKey string) error
GetObjectUrl(ctx context.Context, objectKey string, opts ...GetOptFn) (string, error)
}
// backend/infra/impl/storage/minio/minio.go - 具体实现
type minioStorage struct {
client *minio.Client
bucket string
}
func (m *minioStorage) PutObject(ctx context.Context, objectKey string, content []byte, opts ...storage.PutOptFn) error {
// MinIO 具体实现
}
Infrastructure 层特点:
-
• 契约分离:contract 定义接口,impl 提供实现 -
• 技术细节:处理具体的技术实现 -
• 可替换性:可以方便地切换不同实现
3. Application 层
严格来说是组装领域对象,也就是领域服务,在应用落地的时候,一般组合领域对象和基础设施,实现应用逻辑。比如一些非业务性的需求,比如事务,缓存、领域事件。
领域事件有强业务线和非强业务性,这块要注意。在java中一般都通过切面处理(技术实现手段而已,不用过多关注)
// backend/application/knowledge/knowledge.go
type KnowledgeApplicationService struct {
DomainSVC service.Knowledge // 领域服务
eventBus search.ResourceEventBus // 事件总线
storage storage.Storage // 存储服务
}
func (k *KnowledgeApplicationService) CreateKnowledge(ctx context.Context, req *dataset.CreateDatasetRequest) (*dataset.CreateDatasetResponse, error) {
// 1. 参数转换和校验
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
returnnil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required"))
}
// 2. 调用领域服务
domainResp, err := k.DomainSVC.CreateKnowledge(ctx, &createReq)
if err != nil {
return dataset.NewCreateDatasetResponse(), err
}
// 3. 发布领域事件
err = k.eventBus.PublishResources(ctx, &resourceEntity.ResourceDomainEvent{
OpType: resourceEntity.Created,
Resource: &resourceEntity.ResourceDocument{
ResType: resource.ResType_Knowledge,
ResID: domainResp.KnowledgeID,
// ...
},
})
return &dataset.CreateDatasetResponse{
DatasetID: domainResp.KnowledgeID,
}, nil
}
Application 层的特点:
-
• 组装器:组合多个领域服务和基础设施 -
• 流程控制:控制业务流程的执行顺序 -
• 事件发布:处理跨领域的事件通信 -
• 无业务逻辑:不包含核心业务规则
其实在这里有一个应用上下文的概念的,coze studio中直接把api层的实体,传入到了应用层。在多模块的时候,这个是有问题的。但是多了这个应用上下文,会导致实体的转来转去的,也可以通过技术手段解决,我记的go中也有类似于java中的mapstruct的。
4. API 层(也叫adapter层)
主要进行协议转换,在coze studio中,是处理 HTTP 请求,这种转换也可以是其他的协议。
// API 层只做协议转换,以及基础校验,不包含业务逻辑
// CreateDataset .// @router /api/knowledge/create [POST]
func CreateDataset(ctx context.Context, c *app.RequestContext) {
var err error
var req dataset.CreateDatasetRequest
// 参数校验绑定与校验
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
//定义响应结构,属于领域层
resp := new(dataset.CreateDatasetResponse)
// 调用应用层的服务
resp, err = application.KnowledgeSVC.CreateKnowledge(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
实际运行流程
让我们通过一个完整的用例来看看这些层次是如何协作的
创建知识库的完整流程
InfrastructureDomainApplicationAPIClientInfrastructureDomainApplicationAPIClientPOST /knowledge/create协议转换、参数校验CreateKnowledge(req)获取用户上下文CreateKnowledge(domainReq)业务规则验证GenID() 生成业务ID返回IDknowledgeRepo.Create()创建成功返回创建结果eventBus.PublishResources()事件发布成功返回应用结果HTTP Response
这个流程展示了:
-
1. API 层:处理 HTTP 协议,进行参数校验和转换 -
2. Application 层:获取用户上下文,调用领域服务,处理事件发布 -
3. Domain 层:执行核心业务逻辑,验证业务规则 -
4. Infrastructure 层:提供 ID 生成、数据持久化、事件发布等技术能力
每一层都有清晰的职责,层次间通过接口交互,实现了高内聚、低耦合。
总结
通过分析 coze studio 的源码,我们可以看到一个相对严格按照 DDD 原则设计的复杂系统:
-
1. 清晰的领域边界:按照业务能力划分领域,每个领域都有独立的实体、服务、仓储 -
2. 丰富的领域模型:不仅仅是数据载体,还包含业务行为和规则 -
3. 严格的分层架构:每层职责清晰,依赖关系合理 -
4. 完善的基础设施:通过接口抽象,实现技术无关性 -
5. 有效的跨域协作:通过防腐层和事件机制实现领域间协作
这样的架构让 coze studio 能够:
-
• 应对复杂业务:清晰的领域模型能很好地表达复杂业务逻辑 -
• 支持快速迭代:良好的分层让功能开发更加高效 -
• 保证系统质量:每层都可以独立测试,保证代码质量 -
• 支持技术演进:基础设施的抽象让技术选型更加灵活
DDD 不是银弹,但对于像 coze studio 这样的复杂业务系统,它提供了一套行之有效的设计方法论。关键是要理解业务,建立正确的领域模型,然后严格按照分层原则进行实现。
希望这篇文章能帮助大家更好地理解 DDD 的实际应用,在自己的项目中也能借鉴这些最佳实践。


