根据简单示例来辅助理解DDD

编程入门 行业动态 更新时间:2024-10-11 17:19:13

根据简单<a href=https://www.elefans.com/category/jswz/34/1770116.html style=示例来辅助理解DDD"/>

根据简单示例来辅助理解DDD

提要及相关概念

DDD(Domain-Driven-Design)领域驱动设计是近年来比较火的一个概念,一种通过将软件实现与核心业务概念的演进紧密相连从而实现复杂需求的软件开发方法。分为战术和战略两个层面,本文旨在通过结合简单示例来帮助大家理解战术层面的DDD。

在开始前,先普及下本文中用到的概念,提前声明相关 概念仅为笔者的理解

  • 领域(domain):在某些或某个业务场景中的一切,包括但不仅包括问题、属性和行为等
  • 子域:领域更细粒度的划分,根据功能和重要性大致可分为以下三类,在不同的角色或视角中,一段业务逻辑可能被划分至不同域或子域
    • 核心子域:决定产品和公司核心竞争力的子域
    • 支撑子域:必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域
    • 通用子域:被多个子域使用的子域
  • 聚合(aggregate):由于某个或某些业务而聚在一起的实体的集合,作为一个整体被外界访问,对外暴露操作的基本单元。是领域的子集
  • 聚合根(aggregate root):聚合的根节点,外部仅能对聚合根进行操作,对其聚合内其他实体的操作由聚合根来完成,一个聚合有且只有一个聚合根
  • 限界上下文(bounded context):领域边界,根据单一职责和高内聚的原则来划分聚合内应该包括哪些实体等,如果没有边界,我们将无法区分在不同领域中相同实体的具体含义。例如你同时拥有自行车和摩托车,当单指车的时候无法区分是在指自行车还是摩托车,如果增加上下文,那么在不同上下文中含义就足够明确了,在机动车上下文中车代指摩托车,在非机动车上下文中就代指自行车
  • 贫血模型(anemic domain model):不包含任何业务逻辑,只包含属性和get/set方法的类
  • 充血模型(rich domain model):包含业务逻辑且包含属性和get/set方法的类
  • 实体(entity):带有唯一标识,且由唯一标识区分的对象
  • 值对象(value object):不带唯一标识,通过属性值区分的对象
  • PO(Persistent Object):持久化对象,它和数据库表结构一致。

代码示例

现在假设要设计一个转账系统,业务主要为A用户向B用户转账并在转账成功后向B用户发送短信通知

mvc设计

简单梳理下需求

  1. controller层接受参数,并传入service
  2. service校验参数
    1. 参数不合法return
    2. 参数合法继续业务逻辑
  3. 实现转账逻辑
    1. 转账记录表增加记录
    2. 获取询源、目标账户信息
    3. 源账户金额减少
    4. 目标账户金额增加
  4. 调用dao,持久化数据
  5. 向B用户发送短信通知

接下来用伪代码来实现下部分逻辑

//账户实体
public class Account{id;money;phone;email;...
}//转账记录实体
public class TransferRecord{id;targetAccount;sourceAccount;transferTime;transferSum;...
}//转账service
public class TransferService{public void transferAccount(Req req){//校验参数if(req.xxx == null || ...){return}//转账记录表增加记录TransferRecord record = new TransferRecord();record.setXxx(xxx);//获取询源、目标账户信息Account source = getAccouByXXX();Account target = getAccouByXXX();//源账户金额减少AccountService.updateMoney(source,req);//目标账户金额增加AccountService.updateMoney(target,req);//持久化数据accountDao.update(source);accountDao.update(target);transferRecordDao.save(record);//向目标账户绑定的手机发送短信通知sendTextMessage(target.getPhone());}}

在常规的mvc三层架构中,controller负责接受参数和控制请求类型等,service层负责业务逻辑,dao层负责和数据库交互。这种设计在简单业务场景下行之有效的,但在复杂业务场景的情况下就会导致耦合度变高service层臃肿,代码都堆积在service中,一个service类几千行,修改起来牵一发而动全身。

同时还有一个直击灵魂的问题:“这样的设计真的面向对象了吗”,相信所有的java开发者在敲下“hello world”的时候就被告知java是面向对象的,是一种面向对象的程序设计语言。我们现在重新回顾下什么是面向对象,在java中万物都是对象,对象有自己的属性行为,对外界暴露应该是行为而不是属性,对属性的修改都应通过行为来触发。类比现实:一个人感到饥饿,应该通过进食这个行为来将饥饿这个属性改为否,而不是直接操作胃来修改状态。

现在我们来重新审视下刚才的代码,存在AccountService.updateMoney修改Account的money属性。这在刚才的类比中就像是:一个人感到饥饿,另一个人把他的肚子剖开,并往胃里塞满食物。这并不合理,也很明显和面向对象的理念是相悖。原因是在mvc三层架构中绝大多数的实体都是经典的贫血模型,除了属性之外只包含了get/set方法,也就无法提供额外的行为去操作属性。而且在设计模型的时候是更多的是面向页面/接口,而不是面向对象。相信不少人在拿到一个需求并需要建模的时候,是照搬页面上的属性加上id和公共字段了事,将页面映射到系统架构,这样很简单粗暴还很高效,只是没那么面向对象。

而DDD就是用来让我们的设计避免上述的问题的。

DDD设计

DDD的核心诉求就是将业务架构映射到系统架构,首当其冲的就是领域,领域模型设计应该有以下几个步骤:

  1. 根据需求确定领域和限界上下文,以及上下文的关系
  2. 分析限界上下文和领域,判断哪些类为实体或值对象
  3. 将实体、值对象聚合并确定聚合根
  4. 设计仓储

接下来我们通过这些步骤来将需求重新整理下:
从自然语义的角度出发,补充需求:A用户(账户)向B用户(账户)转账(金额转入、转出)并在转账成功后(消息中心)向B用户发送短信通知(发送消息)。账户有转入、转出行为,消息中心有发送短信行为。

1、确定限界上下文

提炼需求中的名字和动词,名词在DDD中表现为领域、限界上下文、动词则为行为。根据需求中的名词来形成相应的限界上下文。需求暂时为发送短信通知,但有可能在其他的模块需要发送邮件或者微信公众号等类型通知,需要被公用,所以提炼为消息中心的通用子域。

确认好限界上下文后,还需要确定上下文之间的关系,上下文的关系分为以下几种:

  • 合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。
  • 共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
  • 客户方-供应方开发(Customer-Supplier Development):上下文之间有组织的上下游依赖。
  • 遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
  • 防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上下文交互。
  • 开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上下文进行访问。
  • 发布语言(Published Language):通常与OHS一起使用,用于定义开放主机的协议。
  • 大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
  • 另谋他路(Separate Way):两个完全没有任何联系的上下文。

在业务逻辑种转账上下文应通过协议来访问发送消息上下文,所以转账上下文和发送通知上下文的关系应该为开放主机服务

2、分析限界上下文和领域,判断哪些类为实体或值对象

梳理下需求,在需求中我们应该用到的类应该有:

  • TransferReq:转账请求类
  • Account:账户类
  • TransferRecord:转账记录类

结合需求及上文的定义,我们能确认Account和TransferRecord是需要持久化的数据,需要持久化的数据必定会带有唯一标识(id),所以Account和TransferRecord在领域中应该为实体

TransferReq在领域中只关注其属性值,需要通过属性值来区分,所以TransferReq在领域中应该为值对象

需要注意的是在上文中,我们确定了两个子域,且两个子域的关系为开放主机服务,所以在两个子域间应该还存在一个类:

  • SendMessageReq:发送信息请求类

SendMessageReq仅用于两个子域通信,且上下文间只关注其属性值,所以SendMessageReq应该为值对象

3、将实体、值对象聚合并确定聚合根

在上一步中,我们确认了实体有:

  • Account:账户类
  • TransferRecord:转账记录类

值对象有:

  • TransferReq:转账请求类
  • SendMessageReq:发送信息请求类

结合上文,我们能确认在转账子域中包含实体Account、TransferRecord,值对象TransferReq,发送通知子域中包含值对象SendMessageReq。

A用户向B用户转账并在转账成功后向B用户发送短信通知

根据需求和基于OOP的理念,我们可以提炼出各实体和值对象应该具备哪些行为

  • Account:
    • 资金转入
    • 资金转出
  • TransferRecord:build
    • 根据转入转出账户构建实体
  • TransferReq:get
    • 上层传递过来的值对象,只要提供get能力即可
  • SendMessageReq:build
    • 根据转账信息构建值对象

聚合(aggregate):由于某个或某些业务而聚在一起的实体的集合,作为一个整体被外界访问,对外暴露操作的基本单元。是领域的子集

聚合根(aggregate root):聚合的根节点,外部仅能对聚合根进行操作,对其聚合内其他实体的操作由聚合根来完成,一个聚合有且只有一个聚合根

上述的行为都和实体深度绑定,结合聚合定义中“对外暴露操作的基本单元”,基本确定每个类自己就是一个聚合,同时也时聚合根。

4、设计仓储

在上文中,我们用自行车和摩托车例举过,在不同的上下文(是否机动车道)中相同的类(车)可能代表了不同的含义。

同样的话反过来说就是,在不同的上下文中不同的聚合,可能会引用同样的PO,也就意味着为这个类建表时,应该为引用该类的聚合的业务的并集。

在我们模拟的业务逻辑中,只存在两个子域,且引用的PO并不重合,所以PO可直接设计为对应的聚合。

部分示例代码

不同于MVC的三层架构,DDD为四层架构

  • 接口层(Interfaces):封装应用和对外暴露接口,负责与前端交互
  • 应用层(Application):对领域层能力进行编排组合并提供其能力给接口层
  • 领域层(Domain):领域层核心业务逻辑相关,领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑
  • 基础层(Infrastructure):基础资源服务相关,为其它各层提供通用技术能力、三方软件包、数据库服务、配置和基础资源服务

上文中,我们已经确认了领域模型,接下来使用伪代码形式展示DDD四层架构。

包结构如下:

展示部分代码,application中的service,及entity、PO

public class TransferService {AccountRepository accountRepository;TransferRecordRepository transferRecordRepository;SendMesssageService sendMesssageService;public void transfer(TransferReq req){//校验参数req.check();//获取账户信息AccountPO sourceAccountPo = accountRepository.getById(req.getSourceAccountId());AccountPO targetAccountPo = accountRepository.getById(req.getTargetAccountId());//PO转换为实体AccountEntity sourceAccount = sourceAccountPo.po2Entity();AccountEntity targetAccount = targetAccountPo.po2Entity();//源账户转出sourceAccount.rolloff(req.getMoney());//目标账户转入targetAccount.into(req.getMoney());//根据req构建转账记录TransferRecordEntity transferRecordEntity = new TransferRecordEntity();transferRecordEntity = transferRecordEntity.buildByTransferReq(req);//持久化数据AccountPO sourceAccountPO = sourceAccount.entity2PO();AccountPO targetAccountPO = targetAccount.entity2PO();accountRepository.update(sourceAccountPO);accountRepository.update(targetAccountPO);TransferRecordPO transferRecordPO = transferRecordEntity.entity2PO(transferRecordEntity);transferRecordRepository.save(transferRecordPO);//发送消息SendMessageReq sendMessageReq = new SendMessageReq();sendMessageReq = sendMessageReq.buildByTransferReq(req);sendMesssageService.send(sendMessageReq);}}public class TransferRecordEntity {private Long id;private Double money;private Long targetAccountId;private Long sourceAccountId;private LocalDateTime transferTime;public TransferRecordEntity buildByTransferReq(TransferReq req) {TransferRecordEntity entity = new TransferRecordEntity();xxxxreturn entity;}public TransferRecordPO entity2PO(TransferRecordEntity entity){TransferRecordPO po = new TransferRecordPO()xxxxreturn po;}}public class AccountEntity {private Long id;private Double money;private String phone;private String email;public AccountPO entity2PO(){AccountPO po = new AccountPO();xxxxreturn po;}public void rolloff(Double transferMoney){xxx}public void into(Double transferMoney){xxx}
}public class TransferRecordPO {private Long id;private Double money;private Long targetAccountId;private Long sourceAccountId;private LocalDateTime transferTime;
}public class AccountPO {private Long id;private Double money;private String phone;private String email;public AccountEntity po2Entity(){AccountEntity eneity = new AccountEntity();xxxreturn eneity;}
}

观察代码同时和MVC部分的示例代码做对比可以发现

  1. MVC架构中实体都为贫血模型,所有的业务逻辑都在service层中
    DDD架构中实体都为充血模型,使用充血模型来描述其核心业务能力
  2. MVC架构中代码大多都堆积在service层中,类文件比较少
    DDD架构中service层只保留了核心逻辑,具体实现都分散在实体中,类文件比较多
  3. MVC由数据驱动
    DDD由业务驱动
  4. MVC设计简单,开发迅速,易于理解,无法良好的应对复杂逻辑
    DDD设计复杂,在确认好领域模型后,可适应业务的快速变化

结语

DDD的核心诉求是将业务架构映射到系统架构,在领域中开发者只需要关注领域内的业务逻辑,不需要关注对象和数据表的映射,换言之不关注面向对象去增删改查,适用于频繁迭代和复杂业务逻辑场景。

DDD并非银弹,设计复杂,且对开发人员要求较高,上限很高,但下限也很低,应该是手段而非目的。MVC在业务复杂度还未上升至难以处理之前还是卓有成效的。

更多推荐

根据简单示例来辅助理解DDD

本文发布于:2024-03-14 19:42:18,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1737190.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:示例   简单   DDD

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!