大家好,我是付威,一名已在编码第一线奋斗了十余年的程序员。在2019年我初次接触到领域驱动设计(Domain-Driven Design,简称DDD)的概念。在我的探索中,我发现许多有关DDD的教程过于偏重于战略设计,充斥着许多晦涩难懂的概念,导致阅读起来相当艰难。有些教程往往只是解释了DDD的概念,而未深入探讨为何要采用这种方式以及这样做能带来哪些好处,这导致很多人在实践应用DDD时遇到了诸多难题。甚至有些人为了引入DDD而在项目中强制采用DDD架构,结果却意外增加了代码的复杂性,带来了一系列潜在的风险。

为了解决这一问题,我计划从代码的基础入手,详细讲解如何将DDD的理念应用于实际开发中,以便解答为何DDD能使我们的代码更加整洁的问题。今天,我们将着重讨论如何运用DDD的思想来组织我们的代码,从而实现"高内聚、低耦合"的开发目标。

在接下来的讨论中,我将与大家分享我在将DDD理念融入实际项目中的一些心得和体会,以及如何在现实项目中充分发挥DDD的优势。无论是战略设计还是战术实施,我都将尽可能以通俗易懂的方式进行解释,希望能够帮助大家更好地理解和应用DDD,从而在编码的道路上越走越远。

首先,让我们看一个电商系统中下单功能的代码示例:

@Autowired
ProductDao productDao;
@Autowired
UserDao userDao;
public void createOrder(String productId,String userId,int count){
Product product = productDao.queryById(productId);
UserInfo user=userDao.queryByUserId(userId); //风控检测
RiskResponse riskRes= riskClient.queryRisk(xxx);
if(riskRes!=null&&riskRes.getCode()==0&&riskRes.getRiskCode().equal("0001")){
//命中风控
throw new BizException("下单失败,请检查网络")
} Order order=new Order();
order.setOrderId(IdUtils.generateId());
order.setPrice(product.getPrice()*count);
order.setAddress(user.getAddress());
order.setStatus(OrderEnum.OrderSucess);
orderDao.insert(order); //预热缓存和增加记录
redisService.set("Order:OrderID_"+order.getOrderId(),order);
orderLogDao.addLog(order); MessageEntity message = new MessageEntity();
message.setOrderId(order.getOrderId());
message.setMessage("下单成功");
kafkaSender.sent(messageEntity); }

代码分析

首先,我们对这段代码的逻辑进行整理,共涉及5个步骤:

  1. 查询商品和用户信息
  2. 下单行为的风控检测
  3. 订单创建和持久化
  4. 写入缓存和记录下单日志
  5. 发送订单下单成功消息,通知其他系统

我们从这几个过程入手,根据业务的重要性,我们可以将它们划分为核心业务和非核心业务。显然,下单及其相关操作属于核心代码(步骤1、2、3)。与此相比,写日志、写入缓存以及发送Kafka消息则属于下单过程的非核心业务

核心代码分析

1. 【查询商品和用户信息】

productDaouserDao 这两个类是用于封装数据库的增删改查(CRUD)操作。然而,这种封装方式的问题在于,它们的方法实现与具体的数据存储介质密切相关,导致我们的业务逻辑对数据存储方式有着强烈的依赖。

举个例子来说明:当前情况下,我们的数据存储介质是MySQL数据库,因此 userDaoproductDao 类中的方法都是基于SQL语句的封装。然而,如果以后需要更换不同的数据访问框架,或者将数据存储从MySQL迁移到Elasticsearch(ES),我们就必须修改 userDaoproductDao 类的实现,以适应新的数据存储方式。这样的操作不仅会对核心业务代码产生影响,还会在项目的各个角落引发不确定性,从而导致每一次的代码优化都需要小心谨慎地进行。

这种紧密耦合的情况,除了增加了代码维护的难度,还可能引发系统的脆弱性。一旦需要改动存储层的实现,就必须在整个项目中寻找并修改所有与之相关的代码。这不仅消耗时间,还增加了出错的可能性。

2.【下单的行为的风控检测】

在订单生成的过程中,我们会调用风控查询的接口,这一步骤可以被视为对第三方应用的一种依赖。这种依赖关系迫使我们在处理返回值时必须非常仔细,涵盖判断返回值是否存在、验证成功的响应状态、以及业务代码的验证等多个环节,以确保我们的代码具备足够的健壮性。

然而,这种依赖关系同时也带来了潜在的问题,即我们的核心业务逻辑可能会随着第三方接口的变更而需要进行修改。以一个实际例子来说明,假设风控接口新增了代表风控生效的业务代码,如00020003等,那么我们的核心代码就必须相应地进行调整,例如:

riskRes!=null&&riskRes.getCode()==0&&(riskRes.getRiskCode().equal("0001")||riskRes.getRiskCode().equal("0002")||riskRes.getRiskCode().equal("0003")),

然而,这种改动会使代码变得难以阅读和维护。每次接口变更,我们都需要在多处代码中进行类似的修改,而且这些修改会在整个代码库中产生涟漪效应,导致代码的耦合度上升,可维护性下降。

3. 【订单的创建和持久化】

关于持久化的问题,上面已经有过详细的讲解,因此不再赘述。

在观察订单的创建过程时,我们发现这属于核心业务的关键部分。然而,仔细思考下,我们发现实际需要的其实是订单创建的结果。因此,将这个过程放在核心的业务代码中,可能会对代码的可读性产生不良影响。

很多朋友看到这里可能会想到,我们可以将订单的创建过程独立出来,以此来减轻核心业务对订单创建过程的依赖。当然,这是一个合理的解决方案,许多DDD实践也是这么做的。 我个人更倾向于采用实体的工厂模式来创建实体,以此进一步解耦实体的创建过程。

如果我们采用这种方式,我们可以更好地组织和管理代码,使其更易于阅读和理解。同时,这也能够避免在核心业务代码中过度混合不同的功能,从而提高代码的可维护性。

在订单创建过程中,有两个属性的赋值操作

order.setOrderId(IdUtils.generateId());
order.setPrice(product.getPrice() * count);

这两个赋值语句背后蕴含着更深层次的业务意义。其中,生成订单号是为了唯一标识每个订单,确保订单信息的准确无误;而计算订单价格则是根据产品数量和单价进行计算,确保订单金额的准确性。

然而,在传统的贫血模型中,这些隐藏在赋值语句背后的业务意义并没有得到明确的定义和体现。当类似的业务代码分散在各个类或服务中时,会导致业务代码呈现出碎片化的状态,无法形成有机的整体。在进行修改和维护时,我们需要在整个代码库中搜索引用,分别进行修改,这无疑增加了维护的难度和成本。

非核心代码分析

在领域驱动设计(DDD)中,我们通常将系统划分为三个主要部分:核心域、通用域和支撑域。

  1. 核心域:这是业务的核心部分,包括业务的核心规则和业务流程。在这个例子中,下单动作及其依赖的数据应该是核心域的一部分。

  2. 通用域:这个部分包含了一些跨领域的业务逻辑,比如缓存、日志记录、通知等。在这个例子中,下单后写入缓存、写入下单日志和通知都属于通用域。

  3. 支撑域:这个部分包含了一些基础设施和公共代码,比如数据库访问、网络通信、错误处理等。

对于如何拆分, 并没有固定的规则,需要根据具体的业务需求来确定。在这个例子中,由于下单动作及其依赖的数据是核心,而下单后写入缓存、写入下单日志和通知属于其他领域,所以应该采用领域间的交互方式进行拆分。也就是说,下单动作应该在核心域中完成,而写入缓存、写入下单日志和通知等操作则通过领域间的方式进行调用。这样可以保证核心域的内聚性,同时也可以降低不同领域之间的耦合度。

代码优化与领域驱动设计

问题分析总结

结合以上讨论,我们归纳出上述代码存在的问题:

  1. 第三方接口的业务无关性影响核心业务可维护性,容易引发对核心代码的频繁修改,降低代码的稳定性和可维护性。
  2. 业务逻辑与数据存储紧耦合,难以实现逻辑的复用和数据存储的切换,扩展性受限。
  3. 核心业务中掺杂了与核心业务无关的代码片段,影响代码的可读性,理解核心逻辑需要分离非关键细节。
  4. 实体内的业务逻辑分散在代码的不同地方,导致业务逻辑零散、难以维护。
  5. 领域间存在强耦合,对其他领域的修改容易对当前核心逻辑造成意外影响,增加系统的脆弱性和改动的风险。

针对这些问题,我们需要考虑采用领域驱动设计(DDD)的原则和方法,以及相应的重构策略,来优化和改善代码结构,提高代码的可维护性、扩展性和稳定性。

代码优化实践

为了解决上述问题,我们引入了DDD的思想,通过优化核心业务代码和拆分通用业务逻辑,使代码更加整洁和可维护。以下是我们对代码的优化方案和具体实现:

1. 适配器模式隔离第三方接口

原始代码中的风控查询接口可能会变化,因此我们引入了适配器模式,将第三方接口的调用从核心业务代码中分离出来。具体地,我们创建了一个 RiskCheckAdapter 类来封装风控查询逻辑,并将返回值转化为业务领域的语言。这样,核心业务只关心风控是否通过,而不关心具体的返回值和变化。

@Service
public class RiskCheckAdapter {
@Autowired
private RiskClient riskClient;
public boolean isRiskDetected(String productId, String userId) {
RiskResponse riskRes = riskClient.queryRisk(xxx); // 根据实际情况传入参数
return riskRes != null && riskRes.getCode() == 0 && riskRes.getRiskCode().equals("0001");
}
} @Service
public class RiskCheckService {
@Autowired
private RiskCheckAdapter riskCheckAdapter; public boolean isRiskCheckPassed(String productId, String userId) {
return riskCheckAdapter.isRiskDetected(productId, userId);
}
}

2. 仓储模式解耦数据访问

为了解决核心业务与数据存储的紧密耦合问题,我们引入了仓储模式。通过创建抽象的仓储接口和具体的实现,我们将核心业务与数据访问解耦。这样,如果数据存储介质发生变化,只需修改对应的仓储实现,而不影响核心业务代码。

// IProductRepository.java
public interface IProductRepository {
Product findById(String productId);
} // IUserRepository.java
public interface IUserRepository {
User findByUserId(String userId);
} // IOrderRepository.java
public interface IOrderRepository {
Order add(OrderDO order);
} // IOrderLogRepository.java
public interface IOrderLogRepository {
void addLog(OrderLogDO log);
} // ProductRepository.java
@Repository
public class ProductRepository implements IProductRepository {
@Autowired
private ProductDao productDao; @Override
public Product findById(String productId) {
return productDao.queryById(productId);
}
} // UserRepository.java
@Repository
public class UserRepository implements IUserRepository {
@Autowired
private UserDao userDao;
@Override
public User findByUserId(String userId) {
return userDao.queryByUserId(userId);
}
} // UserRepository.java
@Repository
public class OrderRepository implements IOrderRepository {
@Autowired
private OrderDao orderDao;
@Autowired
private OrderConvert orderConvert;
@Override
public int save(OrderDO order) {
OrderPO orderPO=orderConvert.convertPO(order);
return orderDao.insert(orderPO);
}
} // OrderLogRepository.java
@Repository
public class OrderLogRepository implements IOrderLogRepository {
@Autowired
private OrderLogDao orderLogDao;
@Override
public void addLog(OrderLog log) {
orderLogDao.addLog(log);
}
}

经过上面的组合后,从下图中我们可以看下前后的依赖对比,从图中可以看出,service层已经对数据不再有数据依赖。

3. 充血模式收敛实体业务逻辑

通过将实体的业务逻辑进行收敛,我们可以提高代码的内聚性和可读性。原始的贫血模式中,订单实体的业务逻辑分散在各处,使得代码难以维护。现在,我们使用充血模式,将订单的创建和属性设置封装在实体内部,提高了代码的聚焦度。

public class Order {
private String orderId;
private int count;
private double totalPrice;
private String address;
private int status; public void createOrder(Product product, User user, int count) {
this.address = user.getAddress();
this.status = OrderEnum.OrderSuccess;
this.generateOrderId();
this.calculateTotalPrice(product.getPrice(), count);
} private void generateOrderId() {
this.orderId = IdUtils.generateId();
} private void calculateTotalPrice(double price, int count) {
this.count = itemCount;
this.totalPrice = price * count;
} // ...其他属性和方法
} public class OrderFactory {
public static Order createOrder(Product product, User user, int itemCount) {
Order order = new Order();
order.createOrder(product, user, itemCount);
return order;
}
} //使用方式
Order order = orderFactory.createOrder(product, user, count);
orderDao.insert(order);
  1. 领域事件解耦领域间通信

    在解耦领域之间的通信方面,我们引入了领域事件。通过定义领域事件、事件监听器以及事件发布机制,不同领域之间的交互变得更加松耦合。这样,当订单创建完成时,我们只需发布订单创建事件,其他领域根据事件进行响应,降低了领域间的依赖性。示意图如下:

事件的代码:

// CommonEventListener.java
@Service
public class CommonEventListener { @Autowired
private IOrderLogRepository orderLogRepository; @Autowired
private RedisService redisService; @Autowired
private KafkaSender kafkaSender; @EventListener
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
String orderId = event.getOrderId();
Order order = getOrderDetailsFromRepository(orderId); // Process the event within the domain object
order.processOrderCreatedEvent(orderLogRepository, redisService);
sendMessage(order);
} private Order getOrderDetailsFromRepository(String orderId) {
// Retrieve order details from repository using orderId
// Return the Order object
} private void sendMessage(Order order) {
MessageEntity message = new MessageEntity();
message.setOrderId(order.getOrderId());
message.setMessage("下单成功");
kafkaSender.send(message);
}
}

优化后的核心业务代码

经过上述优化,核心业务代码变得更加清晰和可维护。以下是优化后的订单创建过程的示例:

// OrderServiceImpl.java
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private IProductRepository productRepository;
@Autowired
private IUserRepository userRepository;
@Autowired
private IOrderRepository orderRepository;
@Autowired
private IOrderFactory orderFactory;
@Autowired
private RiskCheckService riskCheckService; @Override
public Order createOrder(String productId, String userId, int count) {
Product product = productRepository.findById(productId);
User user = userRepository.findByUserId(userId); boolean isRiskPassed = riskCheckService.isRiskCheckPassed(); if (!isRiskPassed) {
throw new BizException("下单失败,请检查网络");
} Order order = orderFactory.createOrder(product, user, count);
orderRepository.save(order); // Publish OrderCreatedEvent
eventPublisher.publishEvent(new OrderCreatedEvent(order.getOrderId()));
return order;
}
}

总结:

通过领域驱动设计的思想,我们成功地对原始的代码进行了优化。引入适配器模式、仓储模式、充血模式和领域事件等概念,使得代码更加整洁、可读和可维护。这些优化不仅使核心业务更加稳定,也为未来的扩展和变化提供了更好的支持。

在下一讲中,我们将探讨如何在项目架构中演进DDD,并提供一个简洁的项目框架作为示例。感谢大家的关注!

领域驱动设计(DDD):从基础代码探讨高内聚低耦合的演进的更多相关文章

  1. 轻量级领域驱动设计DDD Lite在嵌入式系统重构中的应用

    前言 目前,关于领域驱动设计(Domain Driven Design)DDD的培训,材料,视频都比较多,大家对DDD的一些概念都有所了解,但是在实际使用过程中,有很多的问题.例如 为什么DDD的架构 ...

  2. 领域驱动设计(DDD)

    领域驱动设计(DDD)实现之路 2004年,当Eric Evans的那本<领域驱动设计——软件核心复杂性应对之道>(后文简称<领域驱动设计>)出版时,我还在念高中,接触到领域驱 ...

  3. 关于领域驱动设计 DDD(Domain-Driven Design)

    以下旨在 理解DDD. 1.     什么是领域? 妈妈好是做母婴新零售的产品,应该属于电商平台,那么电商平台就是一个领域. 同一个领域的系统都有相同的核心业务. eg: 电商领域都有:商品浏览.购物 ...

  4. 领域驱动设计(DDD:Domain-Driven Design)

    领域驱动设计(DDD:Domain-Driven Design) Eric Evans的"Domain-Driven Design领域驱动设计"简称DDD,Evans DDD是一套 ...

  5. python 全栈开发,Day116(可迭代对象,type创建动态类,偏函数,面向对象的封装,获取外键数据,组合搜索,领域驱动设计(DDD))

    昨日内容回顾 1. 三个类 ChangeList,封装列表页面需要的所有数据. StarkConfig,生成URL和视图对应关系 + 默认配置 AdminSite,用于保存 数据库类 和 处理该类的对 ...

  6. 基于领域驱动设计(DDD)超轻量级快速开发架构(二)动态linq查询的实现方式

    -之动态查询,查询逻辑封装复用 基于领域驱动设计(DDD)超轻量级快速开发架构详细介绍请看 https://www.cnblogs.com/neozhu/p/13174234.html 需求 配合Ea ...

  7. DDD领域驱动设计之运用层代码

    1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 3.DDD领域驱动设计之领域基础设施层 4.DDD领域驱动设计之领域服务 5.整体DEMO代码 什么是运用层,说 ...

  8. 分享我对领域驱动设计(DDD)的学习成果

    本文内容提要: 1. 领域驱动设计之领域模型 2. 为什么建立一个领域模型是重要的 3. 领域通用语言(Ubiquitous Language) 4.将领域模型转换为代码实现的最佳实践 5. 领域建模 ...

  9. 我对领域驱动设计(DDD)的学习成果

    领域驱动设计之领域模型 2004年Eric Evans发表Domain-Driven Design – Tackling Complexity in the Heart of Software (领域 ...

  10. 领域驱动设计(DDD)实现之路

    2004年,当Eric Evans的那本<领域驱动设计——软件核心复杂性应对之道>(后文简称<领域驱动设计>)出版时,我还在念高中,接触到领域驱动设计(DDD)已经是8年后的事 ...

随机推荐

  1. 2022-07-26:以下go语言代码输出什么?A:5;B:hello;C:编译错误;D:运行错误。 package main import ( “fmt“ ) type integer in

    2022-07-26:以下go语言代码输出什么?A:5:B:hello:C:编译错误:D:运行错误. package main import ( "fmt" ) type inte ...

  2. 2021-07-18:最高的广告牌。你正在安装一个广告牌,并希望它高度最大。这块广告牌将有两个钢制支架,两边各一个。每个钢支架的高度必须相等。你有一堆可以焊接在一起的钢筋 rods。举个例子,如果钢筋

    2021-07-18:最高的广告牌.你正在安装一个广告牌,并希望它高度最大.这块广告牌将有两个钢制支架,两边各一个.每个钢支架的高度必须相等.你有一堆可以焊接在一起的钢筋 rods.举个例子,如果钢筋 ...

  3. Django中render()函数和redirect()函数

    render() 作用:render是渲染变量(结合一个给定的模板和一个给定的上下文字典)在模板中,通俗点将context的内容,加载进模板中定义的文件,通过浏览器渲染呈现. render()方法常用 ...

  4. MongoDB + SpringBoot 的基础CRUD、聚合查询

    1.数据准备 1.1.springboot导包 springboot版本:2.7.10 点击查看代码 <!--mongodb的包--> <dependency> <gro ...

  5. tkinter的Entry设置为不可编辑状态

    前 首先我们知道,tkinter中有许许多多的控件,其中使用频繁的控件之一就是Entry(输入框),那么怎么设置它为不可编辑状态呢? state选项 一般我们在写Entry的时候只传入了一个maste ...

  6. vue+iview 动态调整Table的列顺序

    需求:因table列太多,且每个部门关注的信息不一样,拖来拖去不方便观看,客户想让Table列可以拖动,且可以保存顺序. 但是搞动态拖动太难了,我不会,于是改为操作columns数据 思路: < ...

  7. MySQL之视图,索引,存储过程,触发器--实操

    一.视图 什么是视图? 视图是一个虚拟表,其内容由查询定义. 同真实的表一样,视图包含系列带有名称的列和行数据. 行和列数据来自定义视图的查询所引用的表,并且在引用视图时动态生成. 简单的来说视图是由 ...

  8. 为 Windows 系统替换优雅的苹果字体

    使用 Windows 的童鞋,大家估计都用惯了默认的微软雅黑字体,字体本身也很不错,但使用久了也该换个别的字体了,换个字体换个心情嘛. 今天给大家推荐一款非常棒的一键更换 Windows 系统字体的软 ...

  9. vs2022的一些调试技巧——远程调试&线程检查&性能检查

    visual studio一直都是.net/c#开发人员最受欢迎的编译器,除了强大的代码提示和项目模板,还拥有大量的调试工具,这一期我们介绍下code freeze阶段的一些调试技巧.包括测试环境/生 ...

  10. 曲线艺术编程 coding curves 第六章 平托图 (Pintographs)

    第六章 平托图 (Pintographs) 原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/ 译者:池中物王二狗( ...