写在前面
前五篇讲了 DDD 的"道"——通用语言、战略设计、聚合、实体值对象、领域事件、CQRS。最后这篇讲"术":这些概念在 .NET 工程里到底怎么放。
答案是一套分层架构——名字很多(整洁架构、洋葱架构、六边形/端口适配器架构),本质就一句话:依赖方向永远向内,领域模型在最中心、不依赖任何人。这篇用 .NET 把它落地,并收口整个 DDD 系列。
这篇的分层结构和微服务系列第八篇(单个微服务的内部结构)是一致的——那里搭的是"一个服务的骨架",这里补上"DDD 建模在骨架里怎么落地"。
一、为什么需要分层架构
1
2
3
4
5
6
7
8
| 不分层的痛:
Controller 直接调 SqlConnection、直接写 SQL、直接 return
→ 业务逻辑散落在 Controller / SQL / 前端 各处
→ 改数据库要动 Controller,改接口要动 SQL
→ 没法测试(测个业务规则还要连数据库)
根因:职责没分离,依赖方向乱。
分层架构解决的就是"什么依赖什么"的纪律问题。
|
二、整洁架构的内核:依赖向内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| 整洁架构(Clean Architecture)/ 洋葱架构 / 六边形架构,本质相同:
┌──────────────────────┐
│ API / UI(最外层) │ 依赖 ↓
│ ┌──────────────────┐ │
│ │ Infrastructure │ │ 依赖 ↓
│ │ ┌────────────┐ │ │
│ │ │Application │ │ │ 依赖 ↓
│ │ │ ┌───────┐ │ │ │
│ │ │ │Domain │ │ │ │ ← 最内,不依赖任何人
│ │ │ └───────┘ │ │ │
│ │ └────────────┘ │ │
│ └──────────────────┘ │
└──────────────────────┘
铁律:依赖方向只能"向内"(外层依赖内层,内层绝不依赖外层)
Domain(领域)在最中心:
- 只有纯领域逻辑(实体、值对象、聚合、领域事件、接口)
- 不依赖 EF Core、不依赖 ASP.NET Core、不依赖任何基础设施
- 是一个"纯 C# 类库",谁都不认识
外层认识内层,内层不认识外层。
→ 改数据库、换 Web 框架,领域模型一行不用动。
|
1
2
3
4
| 六边形架构(端口与适配器)是同一思想的不同说法:
端口(Port)= 领域定义的接口(IOrderRepository)
适配器(Adapter)= 基础设施对接口的实现(EfOrderRepository)
领域只定义端口,不关心适配器是谁 → 基础设施可替换(六边形的六个边 = 可插拔的适配器)
|
三、四层职责划分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| Domain(领域层)—— 最内,纯逻辑
- 实体、值对象、聚合、聚合根
- 领域事件、领域服务
- 仓储接口(IOrderRepository)、其他端口接口
- 不依赖任何外部框架
→ 前五篇讲的建模,全在这层
Application(应用层)—— 用例编排
- 命令/查询(CQRS)、用例服务(OrderService)
- 调用领域层完成业务,调度事务、发事件
- 定义"应用服务接口",不关心持久化细节
→ 很薄,只编排,不写业务规则(规则归 Domain)
Infrastructure(基础设施层)—— 技术实现
- 仓储实现(EfOrderRepository : IOrderRepository)
- EF Core DbContext、数据库连接
- 消息队列、缓存、第三方 API 客户端
- 领域事件的分发器实现、Outbox 投递
→ 所有"脏活累活"和技术细节,全收在这层
API / Presentation(表现层)—— 最外
- Controller / Minimal API
- HTTP 入参出参、DTO 与领域对象的转换
- 认证、异常处理中间件
→ 只做"协议转换",不含业务逻辑
|
1
2
3
4
5
6
7
8
| 依赖关系(编译期引用):
API → Application → Domain
Infrastructure → Application → Domain
└──(实现 Domain 定义的接口)
注意:Infrastructure 引用 Domain(为了实现仓储接口),
但 Domain 不引用 Infrastructure(依赖倒置)。
这正是"端口在内层定义、适配器在外层实现"的体现。
|
四、.NET 工程结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| OrderService/ (一个限界上下文 = 一个解决方案)
├── src/
│ ├── OrderService.Domain/ ← 领域层(纯,无框架依赖)
│ │ ├── Aggregates/
│ │ │ └── Orders/
│ │ │ ├── Order.cs (聚合根)
│ │ │ ├── OrderItem.cs (内部实体)
│ │ │ └── OrderStatus.cs
│ │ ├── ValueObjects/
│ │ │ ├── Money.cs Address.cs
│ │ ├── Events/
│ │ │ └── OrderPlaced.cs (领域事件)
│ │ └── Ports/ (端口接口)
│ │ └── IOrderRepository.cs
│ │
│ ├── OrderService.Application/ ← 应用层(编排)
│ │ ├── Commands/ (CQRS 写侧)
│ │ │ └── CancelOrderHandler.cs
│ │ ├── Queries/ (CQRS 读侧)
│ │ │ └── OrderQueryService.cs
│ │ └── Abstractions/
│ │ └── IUnitOfWork.cs
│ │
│ ├── OrderService.Infrastructure/ ← 基础设施(实现)
│ │ ├── Persistence/
│ │ │ ├── EfOrderRepository.cs (实现 Domain 的 IOrderRepository)
│ │ │ └── OrderDbContext.cs (EF Core)
│ │ └── Events/
│ │ └── MediatRDispatcher.cs (领域事件分发)
│ │
│ └── OrderService.Api/ ← 表现层(最外)
│ ├── Program.cs
│ ├── Controllers/
│ │ └── OrdersController.cs
│ └── appsettings.json
└── tests/
├── OrderService.Domain.Tests/ ← 领域层测试(纯单元,超快)
└── OrderService.Application.Tests/ ← 应用层测试
|
五、各层代码长什么样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Domain:纯领域,不认识 EF Core / ASP.NET Core
namespace OrderService.Domain.Orders;
public class Order : Entity<OrderId>, IAggregateRoot
{
public OrderStatus Status { get; private set; }
public void Cancel(string reason)
{
if (Status >= OrderStatus.Shipped)
throw new DomainException("已发货不能取消");
Status = OrderStatus.Cancelled;
}
}
// Domain 层定义端口(接口),不关心实现
public interface IOrderRepository { Task<Order?> Find(OrderId id, CancellationToken ct); }
|
1
2
3
4
5
6
7
8
9
10
11
| // Application:编排,薄
namespace OrderService.Application.Commands;
public class CancelOrderHandler(IOrderRepository repo, IUnitOfWork uow)
{
public async Task Handle(CancelOrderCommand cmd, CancellationToken ct)
{
var order = await repo.Find(cmd.OrderId, ct) ?? throw new NotFoundException();
order.Cancel(cmd.Reason); // 业务规则在领域层
await uow.SaveChangesAsync(ct); // 不关心是 EF 还是别的
}
}
|
1
2
3
4
5
6
7
| // Infrastructure:实现端口(依赖倒置)
namespace OrderService.Infrastructure.Persistence;
public class EfOrderRepository(OrderDbContext db) : IOrderRepository // 实现 Domain 接口
{
public Task<Order?> Find(OrderId id, CancellationToken ct)
=> db.Orders.FirstOrDefaultAsync(o => o.Id == id, ct);
}
|
1
2
3
4
5
6
7
| // API:协议转换,无业务逻辑
app.MapPut("/orders/{id}/cancel", async (OrderId id, CancelRequest req,
CancelOrderHandler h, CancellationToken ct) =>
{
await h.Handle(new(id, req.Reason), ct);
return Results.NoContent();
});
|
六、防贫血:业务逻辑回归领域层
1
2
3
4
5
6
7
8
9
10
11
12
| 整洁架构落地最容易跑偏的点:贫血模型
贫血(错):Order 只有属性,规则写在 CancelOrderHandler 里
→ Handler 变成"事务脚本",领域层形同虚设
→ 规则分散、难测、难复用
充血(对):规则在 Order.Cancel() 内部
Handler 只负责"加载→调用→保存",不掺合规则
→ 规则内聚、可单测、可复用
落地纪律:应用层只编排,业务规则一律下沉到领域层。
问自己:"这段逻辑,放实体方法里是不是更合适?"
|
七、可测试性:分层最大的红利
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 领域层是纯 C#、无依赖 → 单元测试极快、极简单
(呼应我的《.NET 单元测试》系列)
[Fact]
public void Cancel_Should_Throw_When_Shipped()
{
var order = Order.Place(NewId(), AnyAddress(), [Line(Money(99), 1)]);
order.MarkShipped();
var act = () => order.Cancel("误操作");
act.Should().Throw<DomainException>("已发货不能取消");
}
// 不连数据库、不起 Web 服务器、毫秒级跑完
// 因为领域层不依赖任何基础设施,测试就是 new 对象 + 断言
|
1
2
3
4
| 这正是分层 + 依赖倒置的核心回报:
- 领域规则可以被快速、确定性地测试
- 基础设施(EF、HTTP)可以被替换/打桩
- 这就是《.NET 单元测试(四):依赖注入与可测试性》的实战价值
|
八、DDD 系列小结
六篇 DDD,一条主线:
- (一)全景:通用语言是灵魂;战略(限界上下文)重于战术
- (二)聚合:一致性边界;四原则——小聚合、ID 引用、一事务一聚合、内部强一致
- (三)实体与值对象:身份 vs 值;优先用值对象;强类型 ID
- (四)领域事件:事实解耦;可靠投递用 Outbox;事件溯源按需
- (五)CQRS:读写分家;事件投影同步;按痛点渐进落地
- (六)落地:整洁架构,依赖向内,领域在中心,可测试是最大红利
DDD 的核心心法,其实就三条:
- 先理解业务(通用语言、限界上下文)——比任何技术细节都重要
- 把规则关进聚合(一致性边界、充血模型)——让代码自我保护
- 用事件解耦(领域事件、最终一致、CQRS)——应对复杂与规模
九、回到整个架构系列
把 DDD 放回更大的架构图景里:
1
2
3
4
5
6
7
8
9
10
11
12
| 微服务系列(单体→云原生)回答"系统怎么拆、怎么通信、怎么一致、怎么观测"
DDD 系列(业务怎么建模)回答"拆开的边界里、领域怎么设计"
两者交汇点:
限界上下文 = 微服务/模块的拆分单位
聚合边界 = 事务/一致性边界
领域事件 = 事件驱动/最终一致的载体
整洁架构 = 每个服务内部的工程骨架
→ 战略设计决定系统怎么拆(架构)
→ 战术设计决定拆开的单元怎么建(DDD)
→ 二者合一,才是"业务复杂度可控、技术可演进"的完整答案。
|
架构没有终点。不管你最终选单体还是微服务、用不用事件溯源、上不上 CQRS,理解业务、忠实建模、控制一致性边界、用事件解耦——这些原则永远适用。这才是这两个系列想留给你的东西。