解惑领域驱动设计

最近重读Eric Evans的经典《领域驱动设计》,正如Eric提倡我们要去发现隐式概念一般,这次重读也让我发现了许多隐藏的DDD知识。恰好今日有朋友咨询我一些DDD问题,好似激活了触发器,随着问题的解答,我倒是在回答过程中又把这些知识梳理了一遍,才有了这篇杂记。

问题一:Repository的问题

怎么看待DDD中的Repository?我们必须把握一个根本的底线,就是采用DDD方式设计Repository时,一定要忘记所有与数据访问有关的技术实现细节。Repository接口属于领域层,一旦我们将Repository视为DAO对象,就会不期然地重回数据驱动设计的老路。

Eric在书中写道:“Repository将某种类型的所有对象表示为一个概念集合(通常是模拟的)”。这句话一语道破天机,也是DDD得名的由来,必须是通过领域去驱动设计,也就是说在这个设计过程中,应尽量去掉技术的色彩。

借用Martin Fowler对重构的隐喻,在领域驱动设计过程中,也有两顶帽子:领域设计与技术实现。在进行领域设计时,考虑的应该是领域逻辑、业务规则,以及随之需要设计演进的领域模型;一旦开始关注技术实现,就应该切换到与领域完全无关的技术关注点上。这也就是我认为非常关键的点:分离技术复杂度和业务复杂度

Repository是一个概念集合,我们在领域设计时,又需要保证领域概念的完整性,并考虑领域逻辑的不变性约束,因此,DDD才会引入Aggregate。同时,DDD明确约定:一个Aggregate只能有一个Repository,即聚合根的Repository。所有对聚合的访问都应该通过Repository来完成。

问题二:针对没有采用DDD的项目,如何演化为DDD

在《领域驱动设计》的第四章“分离领域”,Eric给出了几点DDD的适用范围:

  • 领域驱动设计只有应用在大型项目上才能产生最大的收益,而这也确实需要高超的技巧。不是所有的项目都是大型项目;也不是所有的项目团队都能掌握这些技巧
  • 如果一个架构能够把那些与领域相关的代码隔离出来,得到一个内聚的领域设计,同时又使领域与系统其它部分保持松散耦合,那么这种架构也许可以支持领域驱动设计
  • 将领域实现独立出来是领域驱动设计的前提

因此,领域驱动设计绝对不是银弹,我们也不要将领域驱动设计视为拯救项目的灵丹妙药。从上述几点描述,我们似乎可以得出DDD的基础要素:

  • 项目的规模与领域复杂度
  • 项目成员的设计能力

当我们开始做一个新项目时,有可能从一开始业务并没有多复杂,系统规模也不够大,没有运用DDD是可以接受的选择。但随着需求的增加与变化,项目规模与领域复杂度都达到了DDD的要求。这时该如何应对?

针对这种已有的系统,若要从Non-DDD形式演化为DDD形式,无非是两种策略:

  • 策略一:对已有系统进行重构。注意这种重构并非Martin Fowler提出的代码级别重构,而是对领域模型的重构。如果没有领域模型,那么我们就需要去重新发掘领域知识,建立统一语言,进而提炼出领域模型,然后使用领域模型指导我们的程序设计。这时,需要重构已有代码来满足领域模型表达的知识。
  • 策略二:如果已有系统的功能与新需求存在一个清晰的边界,更简单的办法是将已有功能视为一个Bounded Context,然后对新需求采用DDD设计方法,并通过引入防腐层和已有系统进行通信。

倘若开启的新项目在领域复杂度上达不到DDD的要求,我仍然建议运用DDD,只不过需要将DDD的设计重点放在战略设计阶段,即对项目划分合理的Bounded Context。一旦确定了这些Context的边界,在边界之内进入战术设计阶段时,就可以不采纳DDD的设计方式,例如选择使用Transaction Script。

问题三:微服务与领域驱动设计的关系

领域驱动设计的战略设计可以帮助我们识别微服务的边界。针对微服务内部,可以采用DDD的方式,也可以采用其他方式,这个并没有特别约束。

大体可以这样认为:

  • 战略层面,领域驱动设计指导了微服务设计,微服务架构影响了领域驱动设计
  • 战术层面,二者没有任何关系,但DDD可以是微服务的其中一种实现

实践中,我们通常会使用DDD的Bounded Context、Context Map以及六边形架构来指导微服务设计。反过来,由于微服务强调服务的独立部署,因此微服务的引入重新定义了Bounded Context的边界,服务之间的通信也突破了Context Map的集成模式。

至于微服务对数据存储的设计约束——“每个微服务的数据单独存储”,属于基础设施层面,严格来讲,与领域驱动设计是没有任何关系的。