DDD笔记汇总
前言:之前在改一个去年做的项目,想把原有项目使用的kafka改成redis,发现改起来有点麻烦,有很多层级之间的调用和跨层调用,很不符合面向对象设计的思路。后在网上了解到DDD的思想,遂发现好用爱用,现记一下方便以后补充。
传统mvc的问题:

api层:我的理解是controller层或者其他可以直接调用biz层或者service层的接口。
biz层: 包含service层,service层注重基础业务的处理,biz层是复杂应用层的业务层。简单讲就是service层一般构成单个不可拆分的业务逻辑操作,业务再复杂点用biz 比如一个service要引入十来个别的service,这时候不用biz 代码很乱且臃肿。
service层: 更加关注业务逻辑,是业务处理层,负责业务逻辑实现,适用于稍微复杂的业务,比如你一个service引入了两三个别的service 。
DAO 层: 数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
附一张阿里开发手册中的应用分层的介绍:

开放接口层: 可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。 终端显示层: 各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。 Web 层: 主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。controller层,一般不能写业务逻辑在这一层,因为第一造成了不可复用,第二以后的维护困难,第三这一层没有上层,如果给用户返回了奇怪的错误信息将会非常丑陋。 Service 层: 相对具体的业务逻辑服务层。 Manager 层: 通用业务处理层,它有如下特征:
对第三方平台封装的层,预处理返回结果及转化异常信息;
对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
与 DAO 层交互,对多个 DAO 的组合复用。
DAO 层: 数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。一般是mapper写接口,xml文件写sql语句的形式。 外部接口或第三方平台: 包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口
mvc代码示例:
public class PaymentController{
private PayService payService;
public Result pay(String merchantAccount,BigDecimal amount){
Long userId = (Long) session.getAttribute("userId");
return payService.pay(userId, merchantAccount, amount);
}
}
public class PayServiceImpl extends PayService{
private AccountDao accountDao;//操作数据库
private KafkaTemplate<String, String> kafkaTemplate;//操作kafka
private RiskCheckService riskCheckService;//风控微服务接口
public Result pay(Long userId,String merchantAccount,BigDecimal amount)
{
// 1. 从数据库读取数据
AccountDO clientDO = accountDAO.selectByUserId(userId);
AccountDO merchantDO =
accountDAO.selectByAccountNumber(merchantAccount);
// 2. 业务参数校验
if (amount>(clientDO.getAvailable()) {
throw new NoMoneyException();
}
// 3. 调用风控微服务
RiskCode riskCode = riskCheckService.checkPayment(...);
// 4. 检查交易合法性
if("0000"!= riskCode){
throw new InvalideOperException();
}
// 5. 计算新值,并且更新字段
BigDecimal newSource = clientDO.getAvailable().subtract(amount);
BigDecimal newTarget = merchantDO.getAvailable().add(amount);
clientDO.setAvailable(newSource);
merchantDO.setAvailable(newTarget);
// 6. 更新到数据库
accountDAO.update(clientDO);
accountDAO.update(merchantDO);
// 7. 发送审计消息
String message = sourceUserId + "," + targetAccountNumber + "," +
targetAmount;
kafkaTemplate.send(TOPIC_AUDIT_LOG, message);
return Result.SUCCESS;
}
}mvc代码中的问题:
传统mvc框架随着后期修改会存在服务同层之间互相调用或者跨层调用的问题,称之为代码腐化。
问题1:可维护性差:大量的第三方模块影响核心代码稳定性。 问题2:可拓展性差:业务逻辑与数据存储相互依赖,无法复用。 问题3:可测试性差:庞大事务脚本与基础设施强耦合,无法单元测试。 最后的结果:业务多发生几次迭代后,这段代码就将成为一个可怕的黑洞。
好的软件设计应该遵循的三大设计原则:
单一职责原则:一个类只负责单一职责,另一种理解也就是一个类应该只有一个引起他变化的原因。
开放封闭原则:对扩展开放,对修改封闭。
依赖反转原则:程序之间应该只依赖于抽象接口,而不要依赖于具体实现。
DDD以领域划分为设计基础:
DDD的核心就是边界,如何区分边界就很重要。
DDD主要强调的是业务有限
DDD有助于解决系统老化问题,即方便后期扩展维护

用户接口层(展现层):这一层负责向用户显示信息和解释用户命令,完成前端界面逻辑。并将用户请求传递给应用层。
应用层:这一层是很薄的一层,负责协调领域层中的领域对象,简言之就是对领域层中的实现进行编排,组成具体应用场景。应用层要尽量简单,不包含业务规则或者知识,不保留业务对象的状态,只保留有应用任务的进度状态,更注事流程性的东西。应用层直接依赖于领域层,由领域层提供具体的业务能力。
领域层:这是业务软件的核心所在,包含了业务所涉及的领域对象(实体、值对象),领域服务以及它们之间的关系,负责表达业务概念、业务状态信息以及业务规则,且体表现形式就是领域模型。DDD 强调领域层不需要任何外部依赖,只是反应软件核心的业务能力。为领域层提供持久化机制(如数据库资源)等
基础设施层:这一层向其他层提供通用的技术能力,为应用层传递消息(API网关等)
DDD 充血模型
是面向对象编程的一种表现形式,使用DDD的充血模型,实体对象可以直接描述核心业务能力,系统做什么事情,一目了然。
这里withdraw和deposit修改的都是该类中的private available变量,不涉及到数据库操作。
public class Account{
private Long id;
private Long accountNumber;
private BigDecimal available;
public void withdraw(BigDecimal money){
//转入操作
available = available + money;
}
public void deposit(BigDecimal money){
//转出操作
if(available < money){
throws new InsufficientMoneyException();
}
available = available - money;
}
}
DDD贫血模型
是面向过程编程的一种表现形式, 传统的不带更改实体状态方法的实体就是贫血模型
public class Account{
private Long id;
private Long accountNumber;
private BigDecimal available;
}
DDD统一基础概念
实体
领域服务
防腐层
仓库,工厂
值对象,聚合
DDD使用工厂模式构建接口
具体与数据库交互的实现类使用仓库和工厂,封装实体持久化操作,摆脱数据库限制。
public interface AccountRepository {
.......
}
public class AccountRepositoryImpl implements AccountRepository {
@Autowired
private AccountDao accountDAO;
@Autowired
private AccountBuilder accountBuilder;
@Override
public Account find(Long accountNumber) {
AccountDO accountDO =
accountDAO.selectByAccountNumber(accountNumber);
return accountBuilder.toAccount(accountDO);
}
@Override
public Account save(Account account) {
AccountDO accountDO = accountBuilder.fromAccount(account);
if (accountDO.getId() == null) {
accountDAO.insert(accountDO);
} else {
accountDAO.update(accountDO);
}
return accountBuilder.toAccount(accountDO);
}防腐层,隔离外部服务
public interface BusiSafeService{}
public class BusiSafeServiceImpl implements BusiSafeService{
@Autowired
private RiskChkService riskChkService;
public Result checkBusi(Long userId,Long mechantAccount,BigDecimal
money){
//参数封装 将调用风控微服务的接口通过接口重新封装,将其从原有业务代码中剥离出来
RiskCode riskCode = riskCheckService.checkPayment(...);
if("0000".equals(reskCode.getCode()){
return Result.SUCCESS;
}
return Result.REJECT;
}
}防腐层 ACL 隔离第三方组件
public class AuditMessage{
private Long UserId;
private Long clientAccount;
private Long merchantAccount;
private BigDecimal money;
private Date data;
...
}
public interface AuditMessageProducer{
...
}
public class AuditMessageProducerImpl implements AuditMessageProducer{
private KafkaTemplate<String,String> kafkaTemplate;
public SendResult send(AuditMessage message){
String messageBody = message.getBody();
// 7. 将发送审计消息服务通过接口进行封装隔离
kafkaTemplate.send("some topic",messageBody);
return SendResult.SUCCESS;
}
}领域服务:封装跨实体的业务操作。
public interface AccountTransferService{
void transfer(Account sourceAccount,Account targetAccount,Money money);
}
public class AccountTransferServiceImpl implements AccountTransferService{
public void transfer(Account sourceAccount,Account targetAccount,Money
money){
sourceAccount.deposit(money);
targetAccount.withdraw(money);
}
}经过DDD修改后的mvc代码(应用层):
public class PayServiceImpl extends PayService {
private AccountRepository accountRepository;
private AuditMessageProducer auditMessageProducer;
private BusiSafeService busiSafeService;
private AccountTransferService accountTransferService;
public Result pay(Long userId, String merchantAccount, BigDecimal amount) {
// 参数校验
Money money = new Money(amount);
UserId clientId = new UserId(userId);
AccountNumber merchantNumber = new AccountNumber(merchantAccount);
// 加载数据
Account clientAccount = accountRepository.find(clientId);
Account merAccount = accountRepository.find(merchantNumber);
// 交易检查
Result preCheck = busiSafeService.checkBusi(clientAccount, merAccount, money);
if (preCheck !=Result.SUCCESS){
return Result.REJECT;
}
// 转账业务
accountTransferService.transfer(clientAccount, merAccount, money);
// 保存数据
accountRepository.save(clientAccount);
accountRepository.save(merAccount);
// 发送审计消息
AuditMessage message = new AuditMessage(clientAccount,
merAccount, money);
auditMessageProducer.send(message);
return Result.SUCCESS;
}
}DDD存在的问题:类爆炸问题
DDD很好的使用了面向对象的思想,但是DDD中的接口层和高度解耦合会导致类爆炸问题,当然解决办法也有,就是使用聚合的方法构建根聚合减少类数量,但是这种方法也有问题,就是需要设计需要的领域。
MVC可以理解为技术有限或者数据有限,主要就是面向数据库进行开发的,而DDD则是面向领域的,针对不同的领域构建多个实体进行构建,对后续修改开放,而且对数据库表的设计没有这么高的要求。而且DDD架构在拆分微服务上有很好的应用。

DDD 中的实体和值对象有什么区别?
在DDD中,实体 Entity 和值对象 Value Object 是两个基本的概念,它们之间有一些重要的区别。 1、唯一性:实体是唯一的,每个实体都有一个唯一的标识符,即使它的属性在一段时间内发生了变化,它仍然是这个实体。与之不同,值对象可以有一组属性,这些属性可以描述一个事物的状态或特征,但它们没有唯一的标识符,即使两个值对象的属性完全相同,它们也被视为两个不同的对象。 2、状态变化:实体可以有状态,并且可以在不同的时间或场景下有不同的状态。例如,一个订单实体可能在创建时是一个待支付状态,支付后变为已支付状态,而值对象通常只有固定的属性,不会有状态变化。 3、生命周期:实体有一个明确的生命周期,它可能随着时间的推移而创建、更新或删除,而值对象没有自己的生命周期,它们通常是在需要时创建,并在不再需要时被垃圾回收。 4、相等性:对于实体来说,两个具有相同标识符的实体是相同的,无论它们的状态如何。对于值对象,两个具有相同属性的值对象被认为是相等的,但这需要通过比较它们的属性来确定。 综上所述,实体和值对象在DDD中是两种不同的概念。在 DDD 中,实体通常用于表示有唯一表示以及状态变化的领域概念,而值对象通常用于表示没有唯一标识以及不可变的属性集合。值对象形式上是一个对象,但是其本质则和一个属性值是等价的。
在 DDD 中,如何处理模型的聚合和聚合根?
在DDD中,聚合是指一组紧密关联的实体和值对象,它们共同完成一个特定的业务逻辑,并由一个聚合根进行管理。聚合根是聚合的根节点,它作为聚合内堆外暴露的唯一访问入口,负责管理聚合内部的对象状态,并协调它们之间的交互。 处理模型的聚合和聚合根主要涉及以下步骤: 1、识别聚合:在领域模型中,识别出具有紧密关联的实体和值对象,它们共同构成了一个聚合。这些实体和值对象应该符合高内聚、低耦合的设计原则,具有一致的业务语义和行为。 2、定义聚合根:在确定了聚合之后,需要选择一个合适的对象作为聚合根。聚合根应该是一个具有全局唯一性的对象,例如一个订单聚合,包含订单,商品、地址、用户等多个实体,而这其中订单就是一个很好的聚合根。 3、保证聚合内部的一致性:确保聚合内部的对象之间保持一致性,即要么一起成功创建、修改、删除,要么一起失败。聚合根负责协调聚合内部的操作。 4、限制聚合外部访问:聚合形成之后,外部对象只能通过聚合根来访问聚合内的对象。聚合跟负责限制对聚合内部对象的直接访问,以维护聚合的完整性。例如形成订单聚合后,对订单聚合内部的商品、用户进行访问,都必须通过订单实体进行。 5、合理规划业务行为:DDD的设计过程中,更强调对实体状态的变化梳理。因此,一些不会引起实体状态变化的操作,例如查询,就不必要严格按照聚合进行划分。例如,想要一天内卖出了哪些商品,这个操作并不会引起实体状态发生变化,因此就不需要严格按照聚合的要求,先访问订单这个聚合根,再统计订单中的商品。而可以跨过订单,直接统计卖出的商品。 6、暴露聚合的服务:在聚合根中暴露一些服务,以便其他应用程序可以访问聚合内部的数据和业务逻辑。这些服务可以是领域服务或者应用服务,它们应该遵循统一的接口规范,并且应该保证安全性。
总之,在DDD中,处理模型的聚合和聚合根需要仔细考虑聚合的设计和实现,包括聚合的组成、聚合根的选择、聚合内部的关系、聚合的行为以及聚合服 务的暴露等方面。通过合理的设计和实现,可以提高系统的可维护性、可扩展性和可重用性。
在 DDD 中,如何处理领域对象的持久化?
在 DDD 中,领域对象的持久化工作通常是通过仓库 Repository 和工厂 Factory实现的。仓库是一种用于访问领域对象的机制。他负责将领域对象从内存中保存到持久存储,如数据库中,以及从持久存储中检索领域对象。而工厂则负责从持久存储中组装领域对象。 在处理领域对象的持久化时,通常需要注意以下几个问题:
定义仓库接口:为每个需要持久化的领域对象创建一个对应的仓库接口。这个接口通常包含了一组方法,用于对领域对象进行存储和检索操作。而在实现仓库接口时,通常可以使用泛型,扩大接口的适用场景。
通过工厂组装实体:DDD中的实体包含很多面向对象的业务特色,而数据库中的数据往往带有很多技术特色。这时,通过工厂的设计,可以让实体设计摆脱具体数据库的限制,从而让实体能够真正面向业务进行构建而不用考虑具体数据库技术的影响。
合理进行业务隔离:在DDD中,数据的访问和修改应该通过仓库和工厂来完成,而不是直接访问数据库,仓库和工厂应该提供统一的接口来访问和修改数据,这样可以保证数据的完整性和一致性。
事务管理:在处理领域对象的持久化时,通常需要考虑事务管理,确保在保存或检索领域对象时,事务能够正确地提交或回滚,以保持数据致性。
隔离异常:与数据库的交互过程中产生的异常,应该在仓库和工厂中进行封装。这些业务异常尽量不要蔓延到领域层。
总之,在DDD中,仓库和工厂是两个核心的概念,它们的设计应该考虑到应用的需求、领域模型的结构、数据的访问和修改等方面。通过合理的设计,可以提高系统的可维护性、可扩展性和可重用性。
DDD 中的限界上下文是什么?有什么用?
在DDD中,"限界上下文“是一个非常重要的概念,它指的是一个边界内的领域模型和与之相关的语义环境。限界上下文(Bounded Context)是一种用于定义和隔离领域模型的概念。每个限界上下文都代表了一个明确定义的、有边界的领域模型,用于描述业务领域的一部分。限界上下文有助于在大型系统中管理和组织复杂的领域模型,并确保不同部分之间的一致性和清晰性。
限界上下文可以看作是一种语义上的边界,它可以将领域模型与外部环境隔离开来,保证领域模型的独立性和纯净性。在这个边界内,领域模型的概念和操作都有着明确的含义,不会受到外部因素的干扰和影响。
通过限界上下文,团队成员可以更加清晰地了解业务领域,并在这个边界内进行交流和协作。限界上下文可以帮助团队成员避免使用不准确或歧义性的术语,使交流更加准确、高效。
另外,限界上下文还可以作为微服务设计的重要参考。在微服务设计中,不同服务之间的边界是很重要的,而限界上下文可以帮助我们更好地理解和规划这些服务的边界。在很多情况下,限界上下文的边界往往就是微服务的边界,这可以帮助我们更好地拆分和设计微服务。
总之,限界上下文是DDD中的关键概念之一,它可以帮助我们更好地描述和理解业务领域,提高团队成员的协作效率,同时也可以作为微服务设计的重要参考。
评论区