拿Eric Evans所著的《领域驱动设计》来说,它并非属于一种特定架构模式之类的范畴,而是归为一套思维方式这一类别,它会告知你该如何去划分边界,以及怎样去进行建模。这种《领域驱动设计》能够与上面提及的任何架构模式一同被用于结合使用背景之中。
于近期的项目开发当下,我的项目规模抵达了一个更为庞大的层级,并且业务需求变动的频率也出现了提升的状况。我还首次被自己所撰写的那种宛如“屎山”般的代码给阻碍住了脚步,当去修改一项需求,增添一个新事物的时候,发觉仅仅能够凭借全局变量强行塞进去。
所以借这个机会深度了解了一下软件架构,以此作记录。
介绍
原文
译文
DDD,它属于一套软件设计方法论,它亦是一套思维方式,内心得先存有思路,之后再去学习,我觉得这个样子会好很多,故而要先学习 DDD。
核心思想
需要被设计的软件,应当是由业务领域也就是Domain去进行驱动的,要运用业务概念来对代码展开组织工作,并非是运用技术概念来做这件事。
方法论战略设计(Strategic Design)
战略设计解决架构问题,决定系统怎么拆,更重要。
通用语言(Ubiquitous Language)
定义为,追求的是开发以及业务运用呈现为同一套术语,代码当中的命名能够直接对业务语言予以反映。
Note
你可曾碰到过这般状况,业务讲“把这个款做下架处理”,开发却理解为要将数据库里的记录给删除,然而业务所说的意思实则是把商品的状态转变为不可见,问题就出在了“下架”这个词汇上,双方对此的理解并不相同。
通用语言要求团队先对齐术语。比如大家约定:
比如说,在电商项目里,如果涉及sku,那么关于sku ID必须要约定好,其中,由平台生成的用于称平台SKU的sku,以及我们所设定的sku名称,在代码里的体现分别是前者为platform_sku,后者叫sku。
方法的名称,类的名称,模块的名称,甚至团队在开会期间嘴里所讲的词汇,皆应当与代码里边保持一致。它属于一整套语言体系,并非仅仅是命名规范而已。
限界上下文(Bounded Context)
释义:于这般的边界范围之内,全部的术语以及模型均具备独一无二的含义,在不同情形下出现的相同名称,能够是全然不一样的模型。
!!!这是 DDD 战略设计里最核心的概念。
Note
延续运用电商业务,关于“商品”此词,于各异的业务场景之中,那含义是全然不一样的。
或许商品依旧是商品,原本便存在数目众多的字段,在各异的业务场景之中的 “商品”,从本质上来说就是不一样的业务概念,只是恰好都被称作这个名字罢了。
Bounded Context的做法是,使得每个业务场景具备自身的Product,存在多个Product类,且这些Product类仅仅涵盖自身所需的类。
故而,在限界上下文思维予以指导的情形下,自然而然地,整个项目会首先依据业务场景来划分文件夹,每一个业务场景都存在自身所需要的数据,就算这些数据全部能够归类为Product类,然而,每一个业务场景都会自行定义一个仅仅涵盖自身所需的类。
于实践进程当中,借由 Application 的调用,去分配传进每个类其自身所需要的事物。
上下文映射(Context Map)
Context Map其自身并非代码,它属于一张架构图,绘制在了白板或者文档之中,用于协助你梳理清楚上下文之间的关系。真正落实到代码里面的是各种各样的集成方式,当中最为常用的便是防腐层。处于理想状态下每个项目能够先拿出心思来做设计,设计图便是这一事物。
描述不同限界上下文之间的联系和通讯的。
上游/下游(Upstream/Downstream)
数据提供方是上游
消费方是下游
例如,Excel 模板分析、Excel 填写是一对上下游
具有防止腐败功能的那一层,它对应的英文名字是Anti-Corruption Layer,简称为ACL。
和外部系统进行对接那儿的中间层,把外部呈现的信息格式,依照对应关系转换到内部系统里,达成让信息格式相互解耦的目的。
可以单独放个 acl 目录,里面都是做防腐的。
ACL 的职责在于,将外部的数据结构,转变为自身上下文的领域模型,举例来说,像是把 Temu API 返回的 JSON,转化为自己所定义的 Order 对象。
共享内核(Shared Kernel)
两个上下文共用一小部分模型,尽量少用
子域(Subdomain)
把整个业务范畴进行拆分,拆分成核心的领域,拆分成支撑的领域,拆分成通用的领域,以此来决定资源投入方面的优先级。
避免过度设计带来的开销,先做优先级,判断资源倾斜。
核心域(Core Domain)
比方说,我们的印花生成,值得精心去设计,做出更好的架构,并且,它需要经常维护更新,还有,Excel生成,也值得精心设计,做出更好的架构,且需要经常维护更新。
支撑域(Supporting Subdomain)
能用就行。
通用域(Generic Subdomain)
用谁的都行,目的是有这个。
第三方,比如说什么支付功能。
运用策略的谋划(Tactical Design),具备各种特性的存在(Entity)。
存在着具备唯一ID的业务对象,该业务对象拥有生命周期,如同衣服工厂里的一个订单,从下单那个时刻开始创建,历经业务流程,一直持续到发货,在获得款项之后,订单才宣告结束。
值对象(Value Object)
有这样一种对象,它没有ID,其关注点仅在于属性值 ,这种对象或许是用于封装一系列数据的 ,比如说描述订单里的商品 ,像商品对象可能就涵盖了数量、颜色等 ,商品需要独立进行追踪 ,所以应当算是Entity ,举例来说像地址(Adress) ,该信息自身较为复杂 ,可能会需要进行校验等等 ,这类适合被封装成值对象。
# 这不是 Value Object,这只是一个普通数据容器
data = {"province": "广东", "city": "深圳", "detail": "南山区xxx"}
# 这是 Value Object
class Address:
def __init__(self, province: str, city: str, detail: str):
self.province = province
self.city = city
self.detail = detail
def __eq__(self, other):
"""相等性通过属性值判断,不是通过 ID"""
return (
self.province == other.province
and self.city == other.city
and self.detail == other.detail)
def full_address(self) -> str:
"""可以有业务方法"""
return f"{self.province}{self.city}{self.detail}"
仅当并非只是一个普遍的变量,具备了含义或者内容变得丰富起来,无需借助 ID 去追踪身份,并且具有值得进行封装的业务含义(存在校验规则、拥有组合属性、或者含有业务方法)的时候,才将其封装成为 Value Object。
聚合(Aggregate)
有一组紧密关联之对象(实体加上值对象)所构成的集合,被称作聚合,其整体维护业务规则的一致性,外部代码无法直接对聚合内部对象予以修改,而必须借助聚合根来实施操作。
聚合是一系列对象集合,像订单对象呀,还有订单项对象,在业务方面二者联系紧密呢,并且存在唯一入口,那就是订单。
聚合根(Aggregate Root)
聚合根乃聚合之唯一入口,像订单这般,所有订单里的订单项,计算订单信息等,皆只能以订单作为唯一入口。
因为聚合根负责在每次操作时保证内部业务规则不被破坏。
仓储(Repository)
聚合的持久化接口,隔离数据库细节
定义,Repository 是聚合根的持久化接口,它使得领域层产生这样的感觉,即“我仅仅是在从一个集合之中进行对象的存取操作”,并且完全不需要去知晓背后究竟是数据库、文件、是 API 亦或是 Excel。
Repo意味着一个聚合根,Repo给聚合根予以存取服务,此聚合根存在不同来源,不同来源借助子类加以区分,关键在于业务需求,子类是不同来源或者别的。
关键约束:
领域服务(Domain Service)
不属于任何单个 Entity 的业务逻辑
定义是,当存在一段业务逻辑,这段业务逻辑并未自然合规或适度融洽地归属于任何一个实体或者价值对象,这样情况下,就把它放置到领域服务当中。
服务所指的是处理逻辑,确切来讲该逻辑乃真正去撰写判断以及进行计算的所在之处了,要是能够放置于实体当中就放置于实体当中,要是放置到实体里且还需要引入其他实体,那么就要单独开辟出一个领域服务。
领域事件(Domain Event)
当这个业务场景出现之后,会存在一整套需要去做的事情,这一整套事情还有着随时变化的可能性,属于动态的范畴,而这就是领域事件,要是按照传统方式写成 pipeline 的话,那此处的代码就得依据需求频繁地进行修改。
领域事件所起到的作用在于,使得这个地方仅仅是去发布一种状态,要么就是去修改某一种状态,进而让其他那些依赖此状态的业务逻辑自行去监听,然后进行更新。
假若业务逻辑并非时常进行修改,那就无需领域事件呀,如同写死那般运用便是。关键得瞧业务逻辑是不是有那种需要更改的需求呢。
工厂(Factory)
把复杂对象创建的逻辑进行封装,这逻辑专门用来构建对象,该对象要么是“订单”,要么是“商品”。
为了做出这个聚合,得进行许多校验,或者做其他事情,因为数量多了,所以专门搞个Factory。
甄别判定的准则是:要是构造函数所具备的参数数量超出了五至六个,在得以创建开始的时候存在校验的相关逻辑环节,又或者是需依据相应境况去装配组合不一样的结构,那么这种情形下就要去思考考虑Factory。反之倘若并非如此,那就直接进行构造。
总结
业务文件夹属于战略设计范畴,而战术设计呢,是针对每个业务文件夹当中业务能够得以实现的思路。
完整项目的开发思路步骤做什么目的 / 产出
1. 确定业务场景
梳理业务,识别不同的业务场景和职责边界
划出 Bounded Context,创建顶层业务文件夹
2. 画 Context Map
明确各上下文之间谁是上游谁是下游、怎么通信
产出架构图,指导上下文之间的集成方式
3. 划分子域
判断每个上下文是核心域、支撑域还是通用域
剖析并判定每一个上下文所对应的设计精力投放量,至于核心域需运用全套的DDD,支撑域则采取简化的方式,通用域借助第三方来达成。
4. 统一通用语言
和业务对齐术语,约定每个上下文内的命名
确保代码里的类名、方法名直接反映业务语言
5. 识别 Entity 和 Value Object
在每个上下文内分析:哪些对象需要 ID 追踪,哪些只关心值
生成 models 这个目录,清晰地分辨出其中哪些属于 Entity,哪些属于 Value Object。
6. 划定聚合边界
找出哪些对象必须一起保持一致性,确定聚合根
明确每个聚合的入口和内部结构
7. 定义 Repository 接口
为每个聚合根定义持久化接口(ABC)
生成 repositories 目录,每一个聚合根对应一个 Repository。
8. 确定数据源和防腐层
剖析每一个 Repository 背后的数据根源,外部系统增添 ACL。
有关于基础设施的具体达成方式的产出,以及访问控制列表目录的相关要素。
9. 识别 Domain Service
找出不属于任何单个 Entity 的跨聚合业务逻辑
产出 services 目录
10. 识别 Domain Event
找出"发生后需要触发多个可变后续动作"的业务节点
产出 events 目录,解耦后续动作
11. 按需添加 Factory
聚合创建逻辑复杂时才抽 Factory
产出 factories 目录(不复杂就不建)
12. 编写 Application 层
编排各上下文的协作,不含业务逻辑
产出 application 目录,每个用例一个文件
根据开发思路的一个理想规范项目的架构
project/
│
├── shared_kernel/ # 共享内核:跨上下文共用的极少量基础定义
│ ├── sku_id.py # Value Object:SKU ID 的格式定义和校验
│ └── money.py # Value Object:金额(数值+币种)
│
│
│ ============ 以下每个目录 = 一个 Bounded Context(限界上下文)============
│ ============ 顶层按业务场景分,不按技术层分(战略设计)============
│
│
├── catalog/ # 限界上下文:商品管理(核心域 → 完整 DDD 战术设计)
│ ├── models/ # 领域模型
│ │ ├── product.py # Entity:有唯一 ID、有生命周期的商品
│ │ ├── product_category.py # Value Object:分类信息,无 ID,按值比较
│ │ └── price_rule.py # Value Object:定价规则
│ ├── aggregates/ # 聚合:product 是聚合根,外部只能通过它操作内部对象
│ │ └── product_aggregate.py # Product(聚合根)+ 关联的 Value Object 们
│ ├── services/ # 领域服务:不属于任何单个 Entity 的业务逻辑
│ │ └── pricing_strategy_service.py
│ ├── events/ # 领域事件:业务上有意义的事发生了,解耦后续动作
│ │ └── product_listed_event.py # "商品上架了" → 后续可能要同步到其他渠道
│ ├── factories/ # 工厂:创建聚合的逻辑复杂时才用
│ │ └── product_factory.py
│ └── repositories/ # 仓储:聚合根的持久化接口,隔离存储细节
│ └── product_repository.py # 接口(ABC),不是具体实现
│
│
├── fulfillment/ # 限界上下文:订单履约(核心域 → 完整 DDD 战术设计)
│ ├── models/
│ │ ├── order.py # Entity + 聚合根:订单,唯一入口
│ │ └── order_item.py # Entity:订单项,只能通过 Order 访问和修改
│ ├── services/
│ │ └── shipping_cost_service.py # 领域服务:计算运费需要多个聚合的数据
│ ├── events/
│ │ └── order_paid_event.py # 领域事件:"订单付款了" → 通知仓库、发邮件等
│ ├── acl/ # 防腐层:把外部系统数据转成自己的领域模型
│ │ └── temu_order_translator.py # Temu API JSON → 自己的 Order 模型
│ └── repositories/
│ └── order_repository.py # 接口(ABC)
│
│
├── asset/ # 限界上下文:素材管理(支撑域 → 简单设计够用就行)
│ ├── models/
│ │ └── product_asset.py # 这里的"商品"只有 sku_id + 图片 URL
│ ├── services/ # 和 catalog 里的 Product 是完全不同的类
│ │ └── image_download_service.py # 同名不同义 = Bounded Context 的体现
│ └── repositories/
│ └── asset_repository.py
│
│
├── reporting/ # 限界上下文:报表(支撑域 → 不需要完整 DDD)
│ ├── excel_generator.py # 直接写,不需要 Entity/Aggregate
│ └── report_config.py
│
│
├── infrastructure/ # 基础设施层:所有接口的具体实现
│ ├── persistence/
│ │ ├── sqlite_product_repository.py # 实现 catalog 的 ProductRepository
│ │ └── sqlite_order_repository.py # 实现 fulfillment 的 OrderRepository
│ ├── external_api/
│ │ └── temu_api_client.py # Temu HTTP 调用,纯技术
│ └── storage/
│ └── r2_asset_repository.py # 实现 asset 的 AssetRepository(Cloudflare R2)
│
│
└── application/ # 应用层:编排各上下文,不含业务逻辑
├── create_order_use_case.py # 用例:协调 fulfillment + catalog
├── export_excel_use_case.py # 用例:协调 catalog + reporting
└── sync_images_use_case.py # 用例:协调 asset + external API
