关于ddd:领域驱动设计入门与实践上

28次阅读

共计 3357 个字符,预计需要花费 9 分钟才能阅读完成。

编者按:

软件工程师所做的事件就是把事实中的事件搬到计算机上,通过信息化进步生产力。在这个过程中有一个点是不能被忽视的,那就是 [零碎的内建品质]

设计良好的零碎: 概念清晰,结构合理,即便代码库宏大,仍然可了解、可保护;

设计蹩脚的零碎:“屎上雕花”。

其中,畛域概念和畛域模型的缺失是造成这种差别的罪魁祸首。

概念解读

畛域驱动设计 – DDD(Domain-Driven Design)是一种基于畛域常识来解决简单业务问题的软件开发方法论,其本质是将业务上要做的一件小事,通过推演和形象,拆分成多个内聚的畛域。

它有以下三个重点:

  • 跟领域专家(Domain Expert)密切合作来定义出 Domain 的范畴及解决方案
  • 切分畛域出数个子畛域,并专一在外围子畛域
  • 透过一系列设计模式,将畛域常识转换成对应的程序模型(Model)

畛域可大可小,对应着大小业务问题的边界,对边界的划分与管制是畛域驱动设计强调的核心思想。

DDD 的扭转

向 Anemic Model 说“No!”

跟大家介绍一个有名的反模式:贫血模型(Anemic Model)。此模式泛指那些只有 getter 与 setter 的 model。这些 model 不足行为表述,导致使用者每次都要本人组合出想要的性能。

“贫血模型应用起来像在教小孩子一样,一个指令一个动作还很容易忘掉;具备行为表述能力的模型则像跟小孩儿沟通一样,一次口头就能实现许多指令。”

举个栗子:以数据为核心的形式是要求客户代码必须晓得如何正确地将一个待定项提交到冲刺中。此时,谬误地批改 sprintId 或有另外一个属性须要设值,都要求开发人员认真剖析客户代码来实现从客户数据到 BacklogItem 属性的映射。这样的模型不是畛域模型。


public class BacklogItem extends Entity {

    private SprintId sprintId;

    private BacklogItemStatusType status;

    ...

    public void setSprintId(SprintId sprintId) {this.sprintId = sprintId;}

    public void setStatus(BacklogItemStatusType status) {this.status = status;}

    ...

}

// 客户端通过设置 sprintId 和 status 将一个 BacklogItem 提交到 Sprint 中

backlogItem.setSprintId(sprintId);

backlogItem.setStatus(BacklogItemStatusType.COMMITTED);

通过业务语言封装程序行为

DDD 重视将业务语言引入程序模型之中,对重点业务行为进行封装。与其随便封装代码,将程序模型与业务逻辑绑定在一起的行为能够保障代码紧随业务变动做出调整。在建模时,领域专家探讨了以下几个需要:

  • 容许将每一个待定项提交到冲刺中且只有在一个待定项位于公布打算(Release)中时能力进行提交
  • 如果一个待定项已提交到了另外一个冲刺中,先将其回收
  • 提交实现时,告诉相干客户方

客户代码并不需要晓得提交 BacklogItem 的实现细节,因为实现代码的逻辑恰好可能形容业务行为。


public class BacklogItem extends Entity {
   private SprintId sprintId;
   private BacklogItemStatusType status;
   ...
   public void commitTo(Sprint sprint) {if (!this.isScheduledForRelease()) {throw new IllegalStateException("Must be scheduled for release to commit to sprint.");
       }
       if (this.isComittedToSprint()) {if (!sprint.sprintId().equals(this.sprintId())) {this.uncommitFromSprint();
           }
       }
       this.elevateStatusWith(BacklogItemStatus.COMMITTED);
       this.setSprintId(sprint.sprintId());
       DomainEventPublisher.instance()
           .publish(new BacklogItemCommitted(this.tenantId(),
               this.backlogItemId(),
               this.sprintId()));
   }
}
// 客户端通过设置特定于畛域的行为将 BacklogItem 提交到 Sprint 中
backlogItem.commitTo(sprint);

详解 DDD

举例说明:

假如咱们当初在做一个简略的数据统计零碎,其运算逻辑是这样的:地推员输出客户的姓名和手机号,零碎依据客户手机号的归属地和所属运营商,将客户群体分组,调配给相应的销售组,由销售组跟进后续的业务。

代码如上,大部分人都是这么写的,看起来也没什么问题,对一个小工程或短期下线的零碎来说,这样写能够称得上是又快又好;但把其放在一起迭代频繁的大工程内,还留有一些隐患:

隐患 1:接口语义不明确

Register 办法的 bug 在于它反对一种类型、两组参数(用户名、手机号)。当用户注册零碎的参数变更时,比方改用身份证注册,Register 办法就要被革新为 RegisterByPhone 和 RegisterByIdCard。

因为外部校验只会保留参数类型不会保留参数名,因而变更参数意味着新的接口和再来一遍的校验,这不是咱们预期的指标。咱们冀望的是:语义接口足够明确无歧义、可扩展性强且带有肯定的自检性,这才是最优解。

接口语义批改指标:语义明确无歧义、扩展性强、带有肯定的自检性

隐患 2:参数校验逻辑简单

如果存在多个相似的办法,每个办法都要在结尾校验,肯定会存在大量反复代码。一旦某个类型的参数校验逻辑须要批改,那么每个中央都要一一批改,这显然不合乎“开闭准则 ”。即便将其封装进某个工具进行复用,还存留两个 bug:1、在业务办法中把参数异样和业务逻辑异样混合起来,不太正当:业务办法内还须要被动调用工具类来进行校验,如果校验失败,须要抛出异样;
2、随着参数类型越来越多,工具类中的校验逻辑会随之一直收缩,后续保护起来是不小的工作量。

参数校验批改指标:进步校验逻辑复用性参数校验异样与业务逻辑异样解耦

隐患 3:外围业务逻辑清晰度不够

通过革新后的代码,尽管多了些优雅但不“纯正 ”。RegistrationService 是用于对用户进行注册的服务,它的职责应仅限定为「注册」。而注册最实质的行为就是「拿到用户的信息并存储起来」。在这段代码中存在的两个行为「获取手机号的归属地编码」、「获取运营商编码」显然并不适用于「注册」这个业务逻辑。

问题来了:那咱们为什么要在 Register 办法里边写这些逻辑?为了适配 findRep 这个接口来对原始的参数进行解决拼接,就像拿胶水来进行缝缝补补的“胶水逻辑 ”?

如何革新这些“胶水逻辑” 才正当?

两个思路:

1、革新 findRep 这个接口的入参

这在形象上就是正当的,不用在 register 办法内进行胶水操作了

2、把「获取手机号的归属地编码」&「获取运营商编码」内聚到手机号这个类型中

这两个行为都是获取手机号相干的属性,内聚在手机号这个类型中在形象上也是正当的。

由此看来,采纳内聚、搭建外围畛域边界的办法能使注册办法逻辑最为清晰。

什么逻辑应该归属于哪个业务域,这是对“畛域 ” 的了解,就像如何对微服务进行边界限定一样,不同的了解角度会产生不同的畛域模型划分。

须要阐明一点:很多同学对写单元测试感到头疼:写的话,要做到高笼罩很麻烦;不写的话,不仅跑不过 CI,心里还有点慌…不怕!通过对 PhoneNumber 逻辑的内聚、业务逻辑的简化,童鞋们写单元测试的效率可能失去极大的晋升。PhoneNumber 这类型的改变频率比拟小,一旦写了欠缺的测试用例,复用水平会很高~ 这样,后边的业务逻辑只管会变简单,但单元测试逻辑的保护老本也不会进步~


后续咱们将就一则新的案例探讨其背地的语言逻辑和行为办法—— LigaAI 新一代智能研发合作平台

正文完
 0