领域驱动设计(六):落地——整洁架构与 .NET 工程结构

写在前面

前五篇讲了 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 的核心心法,其实就三条:

  1. 先理解业务(通用语言、限界上下文)——比任何技术细节都重要
  2. 把规则关进聚合(一致性边界、充血模型)——让代码自我保护
  3. 用事件解耦(领域事件、最终一致、CQRS)——应对复杂与规模

九、回到整个架构系列

把 DDD 放回更大的架构图景里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
微服务系列(单体→云原生)回答"系统怎么拆、怎么通信、怎么一致、怎么观测"
DDD 系列(业务怎么建模)回答"拆开的边界里、领域怎么设计"

两者交汇点:
  限界上下文 = 微服务/模块的拆分单位
  聚合边界   = 事务/一致性边界
  领域事件   = 事件驱动/最终一致的载体
  整洁架构   = 每个服务内部的工程骨架

  → 战略设计决定系统怎么拆(架构)
  → 战术设计决定拆开的单元怎么建(DDD)
  → 二者合一,才是"业务复杂度可控、技术可演进"的完整答案。

架构没有终点。不管你最终选单体还是微服务、用不用事件溯源、上不上 CQRS,理解业务、忠实建模、控制一致性边界、用事件解耦——这些原则永远适用。这才是这两个系列想留给你的东西。