Arda Akgür
软件架构中的大多数想法,并不是凭空完整出现的。
工程师学到的一条原则,在遇到另一个问题时会呈现出不同的形态。有时一个小小的想法会历经数年——被应用、被打破、被重新设计——最终形成一套属于自己的架构方法。
ARC-DDD 的故事就是这样开始的。
我在澳大利亚担任 Associate Engineer 期间,开始开发这一方法的最初版本。
但在说明这个想法从何而来时,我尤其需要把功劳归于一个人:
Victorian Software CEO Adrian Budiman。
Adrian 是第一个向我展示这个想法的人:不要只把领域当作经典架构分层来思考,而是把系统拆成水平分离的领域。
那个想法成了我的起点。
后来,我在自己构建的系统中把这一方法推进得更远。
今天的 ARC-DDD,或者我有时称呼的 Australian DDD,就是这一过程的结果。
经典的 Domain-Driven Design 通常把系统拆成垂直分层:
这一方法极有价值,ARC-DDD 并不试图取消它。
我增加的是第二条轴。
在系统被拆成垂直分层的同时,领域模型本身也被拆成水平的限界上下文。
例如:
Core├── Users├── Events├── Transactions├── Vendors└── Payments这里的基本规则是:
一个领域不应了解另一个领域的整个世界。
ARC-DDD 真正的特点就在这里显现出来。
在大型系统中,我最常见的问题之一是过于庞大的实体模型。
例如,系统里有一个单独的 User 类。
随着时间推移,什么都被加进去:
PasswordPhoneAddressBalanceIBANKYCPermissionsProfileSecurityTransactionsPreferences...然后 Events 领域即便只想展示用户的姓名和照片,也会开始使用整个 User 模型。
Transactions 领域为了余额和 IBAN 也使用同一个模型。
Vendor 系统再次使用同一个模型。
最终,理论上看起来按领域分离的系统,在实践中却通过相同的实体绑成了一个巨大的结构。
ARC-DDD 在这里采取了不同的做法。
例如:
Users.UserEvents.User2Transactions.User3这些是不同的领域模型。
但它们可以表示同一张物理数据库表。
Users.User ──┐Events.User2 ──┼──> Users tableTransactions.User3 ──┘如果 Events 领域只需要:
UserIdFirstNameFamilyNamePhotoUser2 就只包含这些。
如果 Transactions 领域需要:
UserIdBalanceIBANVerified这些字段,那么 User3 就会按自己领域的需要来定义。
每个领域只看到自己需要的那一部分世界。
这正是 ARC-DDD 的核心方法:限界上下文彼此分离;你不是携带另一个领域的完整聚合,而是创建包含所需字段的领域本地投影。必要时,该投影映射到同一张物理表。
ARC-DDD 的一个重要特征是:它并不把独立数据库当作创建限界上下文的必要条件。
例如,在 Entity Framework Core 中:
modelBuilder .Entity<User2>() .ToTable("Users") .HasKey(x => x.UserId);Events 领域中的 User2 仍然可以绑定到物理的 Users 表。
但由于模型只包含 Events 领域需要的属性,该领域不会把不必要的用户字段拖进自己的模型。
在公开参考实现(eski-dev)的 Goldtag 示例中,User、User2 和 User3 从不同领域视角绑定到同一张 Users 表。User2 表示 Events 需要的数据与行为;User3 表示 Transactions 需要的内容。
这给了我们一个重要的平衡:
数据库在物理上可以共享,但领域模型不必共享。
这是 ARC-DDD 的核心原则之一。
这种分离并不止于文件夹层面。
每个限界上下文都有自己的 DbContext 结构。
例如:
UserDbContextEventsDbContextTransactionDbContextVendorsDbContextEvents 领域知道自己的投影模型。
Transactions 领域知道自己的模型。
Users 领域拥有规范的 User 模型。
在公开参考实现(eski-dev)中,例如 UserDbContext 使用规范的 User 模型,EventsDbContext 使用 User2 投影,TransactionDbContext 在自己的上下文中使用 User3 投影。
因此,一个领域不会直接导入另一个领域的 Core 模型。
在 Events 内部:
using Gold.Core.Users;Events 不是把整个 User 聚合拉进系统,而是创建自己的投影。
这是有意的重复。
因为在 ARC-DDD 中,有时:
少量重复比强耦合更便宜。
在 Core 层面的核心规则之一是:领域文件夹彼此不直接导入。例如,Events 不是使用 Gold.Core.Users,而是创建自己的本地表示,如 User2。
在真实应用中,限界上下文无法完全孤立地存在。
一次操作可能同时需要多个领域,例如:
ARC-DDD 在这里并不把领域层彼此连接。相反,它把组合移到系统的外缘。
例如:
API | +--> OrderingRepository | +--> PaymentsRepository | +--> UsersRepository换句话说,跨领域编排发生在 API 或应用层。
Core 领域不会彼此导入并形成依赖链。
在 eski-dev 中,跨领域组合同样在 API 侧完成。控制器或应用服务在需要时可以与多个仓储协作,但 Core 领域彼此并不接线。
随着系统增长,这种分离会变得非常有价值。
如果我今天要创建一个新的 ARC-DDD 系统,我会把基本结构保持得很简单:
Product.CoreProduct.DomainProduct.Api依赖方向:
Product.Api ↓Product.Domain ↓Product.CoreCore 包含纯领域模型。
它没有 Entity Framework 或基础设施依赖。
Domain 包含持久化层、DbContext 以及仓储结构。
Api 作为组合根,并在需要时在同一用例中编排不同的限界上下文。
eski-dev 当前参考结构中的依赖方向也是 Gold.Api → Gold.Domain → Gold.Core。保持 Core 层不依赖基础设施包,是基本规则之一。
如果今天必须用一句话描述这一架构:
ARC-DDD 是一种软件架构方法:它用水平限界上下文,以及在共享表上的领域特定实体投影,扩展了经典分层 DDD。
我给出了更技术性的定义:
经典分层 DDD + Core 中的水平限界上下文文件夹 + 在共享表上的按上下文实体投影。
但对我来说,这一方法背后更重要的想法是:
一个领域不应看到系统中存在的全部知识——只应看到完成自身工作所需的世界。
因为对我来说,这一方法的起源可以追溯到澳大利亚。
我在担任 Associate Engineer 期间从 Adrian Budiman 学到的水平领域分离想法,就是起点。
后来我把这一想法扩展并应用到自己的系统中——用领域本地实体模型、共享表投影、按领域的 DbContext,以及受控的跨领域组合原则。
因此,当我谈论 ARC-DDD 时,我认为不应把它呈现为“一切都是我从零发明的”。
工程通常本来就不会那样发展。
有人给你一个强有力的想法。
你把它应用到其他问题上。
你会看到缺了什么。
你在其上添加新的原则。
多年之后,会出现一个与最初想法相当不同的系统。
对我来说,ARC-DDD 正是这样发展起来的。
Adrian 向我展示的水平领域思维是火花。
我的贡献,是把那一点火花变成今天我称为 ARC-DDD 的实用架构模型。
ARC-DDD 不只是我在理论上描述的一种架构方法。
对于想了解这一方法如何在真实系统中应用的人,我已经公开了一份较早的开发代码库:
GitHub:
https://github.com/Arcnaboo/eski-dev
该仓库是公开的、基于 C#,并以 MIT 许可证发布。
这个仓库是 ARC-DDD 的公开 Goldtag legacy backend reference implementation。
仓库本身明确把参考实现定义为:
Gold.CoreGold.DomainGold.Api在仓库中,我特别建议查看这些模型:
Gold.Core/Users/User.csGold.Core/Events/User2.csGold.Core/Transactions/User3.cs它们直接展示了 ARC-DDD 最重要的想法。
规范的 User 属于 Users 限界上下文。
Events 对同一用户使用按自身需要收窄的 User2 模型。
Transactions 使用适合自身操作的 User3 模型。
你也可以在 Event 与 Event2、Notification 与 Notification2 等模型中看到同样的方法。要学习参考实现,请特别查看仓库中的这些文件。
这里需要做一个重要区分。
这个仓库带着一个旧的、真实生产代码库的痕迹。
因此,我不会把其中的每一个实现细节一比一地用到今天从零编写的系统中。
例如,旧的连接字符串做法、某些控制器结构、不完整的依赖注入用法,以及不再需要的遗留部分,都不是 ARC-DDD 本身。
我特别强调了这一区别:
吸收想法,现代化机制。
持久的想法是:
水平限界上下文 + 按领域投影 + 按领域 DbContext。
而不是旧代码的历史实现细节。
我认为,仅用图表解释一种架构方法是不够的。
人们需要能够:
因此,我希望 ARC-DDD 的架构文档以及真实的公开参考实现都能保持可访问。
仓库:
https://github.com/Arcnaboo/eski-dev
但我并不希望你为了理解 ARC-DDD 而去复制仓库里的每一行。
恰恰相反。
把那些来自代码年代的部分区分开来。
看领域边界。
看同一物理实体如何在不同限界上下文中被不同地建模。
看上下文如何被分离。
看为什么 Core 不导入其他领域的模型。
因为 ARC-DDD 的本质不是某个框架或特定技术。
Entity Framework 可以变。
.NET 可以变。
数据库可以变。
仓储模式可以变。
多年之后,我们今天使用的大多数工具可能也已经改变。
但基本原则不会变:
让领域边界在代码中成为真正的边界。
并且只向每个领域展示它需要的世界。