原文是 Alex Whitcombe 发在他个人博客《Notes on Software Design》上的一篇 How Much DDD Is Enough?,2023 年 9 月,讨论的是「小项目到底要不要上 DDD」。我读的时候正好在给公司那套内部系统做重构,被里面几句话戳到了,就按自己的理解译了一遍。
这不是严格翻译。原文有些篇幅在讲作者自己的咨询经历,我做了删减;术语我尽量保留英文原词再给中文,因为国内这块的译名不统一,「聚合」和「聚合根」经常被混着用。原文里没有代码,下面几段 C# 是我自己加的,用来说明我在公司项目里的做法,跟原文无关。译得生硬的地方,建议对照原文看。
一、先问的不是「要不要 DDD」,而是「打算维护多久」
作者开篇就说,他见过的大部分关于 DDD 的争论,其实争的是两个不同的问题:一个是「这套建模思想有没有用」,另一个是「值不值得为此付出结构上的代价」。前者在他看来基本没有争议——统一语言、限界上下文这些想法,对任何有点规模的软件都成立。真正需要权衡的是后者。
他给的第一个判断维度是时间。「如果这套东西你打算维护三年以上,而且业务逻辑会持续变化,那么提前把边界划清楚是划算的。如果是两周交差的活动页,或者一个用完就扔的迁移脚本,那么任何分层都是纯成本。」
这一点我自己在公司那两套系统上验证过。其中一套是 2021 年上线到现在还在改的工单系统,业务规则一直在加,当年没划清楚边界的地方,现在改起来都要来回翻三四个文件;另一套是个查询类的看板,上线之后除了加字段几乎没动过,当年图省事写的那种「Controller 里直接拼 SQL」的写法,除了不好看,其实没造成过任何问题。
二、分层的代价在哪里
作者花了不少篇幅讲他对「四层架构」的保留意见。他的说法是:分层本身不产生价值,它只是为了让依赖关系有个明确方向。但如果每一层都只是把上一层的参数原样传下去,那这些层就是在制造噪音。
「如果你打开一个项目,看到 Application 层里有三十个方法,每个方法体都是一行 _repository.GetById(id),那么这三十个方法不是在保护你的领域模型,它们只是在证明你有三十个查询。」
这段话我当时读得有点脸红,因为我写的项目里就有不少这种东西。译到这里停了两天,回头把自己那套系统的 Application 层翻了一遍,删掉了七个这样的空壳方法,剩下的确实每个都有点逻辑在里面。
三、哪些值得先拿过来
作者认为,对小团队来说性价比最高的是三样:
- 统一语言(Ubiquitous Language)。不给术语开会,只在代码里坚持:数据库字段、类名、接口返回的字段,都用业务那边的叫法。这一项几乎零成本,回报却最稳定。
- 限界上下文(Bounded Context)。不用画上下文映射图,只要能说清楚「这两块东西不是一回事」就够了。作者特别提醒,同一个名词在不同上下文里含义不同,强行共用一个实体类是后期最难拆的坑。
- 聚合(Aggregate)的边界。不一定要实现完整的仓储与工作单元,但要想清楚「哪些对象必须一起改才是一致的」。他说这是他唯一建议所有人都做的一项。
我在工单系统里只落实了第一项和第三项。工单状态和它的流转记录,我把它俩当成一个聚合来改,不允许绕过状态去改记录:
public class Ticket
{
public TicketStatus Status { get; private set; }
public void TransferTo(int operatorId, DateTime at)
{
if (Status == TicketStatus.Closed)
throw new InvalidOperationException("已关闭的工单不能再转派");
Status = TicketStatus.Processing;
_records.Add(new TicketRecord(operatorId, at));
}
}
就这一段,挡住了至少三次「在别处直接改状态字段」的改动。代价是写起来确实啰嗦一点。
四、哪些作者建议先别碰
- 为每张表都配一个仓储(Repository)。他的原话是「如果你的仓储接口长得跟 DbSet 一模一样,那你只是把 EF Core 包了一层」。
- CQRS。读写分离带来的心智负担,在读写比例没到某个程度之前不划算。
- 事件溯源(Event Sourcing)。他承认自己只在一个项目上真正用对了,其余几次都是过度设计。
- 为「以后可能要换数据库」而做的抽象。「你不会换的。」
这几条我基本同意,但最后那条我想加一句:我们在 2024 年真的换过一次,把一套系统的报表查询从 SQL Server 挪到了另一个实例上,因为提前没有抽象,改了十几个地方。所以这条我保留意见,不确定是作者的判断更对,还是我们属于少数情况。
五、一个可以操作的判断办法
文章最后给的办法很朴素:每次准备引入一个新概念(一个新的层、一个新的抽象、一个新的模式)之前,写下「它替我挡住了哪件具体的事」。如果写不出来,就先不做。等哪天真遇到那件事了,再加。
我把这条抄在了工位便签上。它不太像方法论,更像一句提醒:结构是为问题服务的,不是反过来。
译者后记
译完最大的收获不在 DDD,而在「删掉空壳方法」那件事。我以前总觉得多一层总比少一层安全,现在不那么确定了——多出来的层也是要有人读的,而读代码的时间是我自己出的。
要说明的是,我对 DDD 的理解基本来自这本书和几篇文章,没有在任何正式项目上完整落地过,上面关于聚合那段代码是否算「对的做法」,我心里没底。另外原文的第四节我删掉了两小节,觉得跟国内项目的实际情况差别太大,如果有人需要完整版可以写信找我,我把原文出处发你。