首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >领域驱动设计(DDD):从基础代码探讨高内聚低耦合的演进【技术创作特训营第一期】

领域驱动设计(DDD):从基础代码探讨高内聚低耦合的演进【技术创作特训营第一期】

原创
作者头像
付威
修改于 2023-08-27 11:57:16
修改于 2023-08-27 11:57:16
5530
举报

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

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

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

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

代码语言:java
AI代码解释
复制
@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等,那么我们的核心代码就必须相应地进行调整,例如:

代码语言:java
AI代码解释
复制
riskRes!=null&&riskRes.getCode()==0&&(riskRes.getRiskCode().equal("0001")||riskRes.getRiskCode().equal("0002")||riskRes.getRiskCode().equal("0003"))

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

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

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

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

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

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

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

代码语言:java
AI代码解释
复制
order.setOrderId(IdUtils.generateId());
order.setPrice(product.getPrice() * count);

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

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

非核心代码分析

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

  1. 核心域:这是业务的核心部分,包括业务的核心规则和业务流程。在这个例子中,下单动作及其依赖的数据应该是核心域的一部分。
  2. 通用域:这个部分包含了一些跨领域的业务逻辑,比如缓存、日志记录、通知等。在这个例子中,下单后写入缓存、写入下单日志和通知都属于通用域。
  3. 支撑域:这个部分包含了一些基础设施和公共代码,比如数据库访问、网络通信、错误处理等。

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

代码优化与领域驱动设计

问题分析总结

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

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

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

代码优化实践

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

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

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

代码语言:java
AI代码解释
复制
@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. 仓储模式解耦数据访问

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

代码语言:java
AI代码解释
复制
// 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. 充血模式收敛实体业务逻辑

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

代码语言:java
AI代码解释
复制
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. 领域事件解耦领域间通信

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

事件的代码:

代码语言:java
AI代码解释
复制
// 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);
    }
}
优化后的核心业务代码

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

代码语言:java
AI代码解释
复制
// 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,并强调了领域的划分和隔离的重要性。通过这种方式,我们旨在更直观地展示DDD的优点,并为读者提供在实际开发中的代码思路和组织方法。

【创作提纲】

1. 传统方式代码会带来哪些问题

2. 如何使用DDD的思想解决问题

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
native2ascii用法
背景:在做Java开发的时候,常常会出现一些乱码,或者无法正确识别或读取的文件,比如常见的validator验证用的消息资源(properties)文件就需要进行Unicode重新编码。原因是java默认的编码方式为Unicode,而我们的计算机系统编码常常是GBK等编码。需要将系统的编码转换为java正确识别的编码问题就解决了。
全栈程序员站长
2022/10/01
6020
java 命令 native2ascii_java native2ascii.exe命令
native2ascii.exe的语法格式:native2ascii [-reverse] [-encoding 编码] [输入文件 [输出文件]]
全栈程序员站长
2022/10/01
3730
mysql中的字符集和校验规则
在MySQL中,最常见的字符集有ASCII字符集、latin字符集、GB2312字符集、GBK字符集、UTF8字符集等,下面我们简单介绍下这些字符集:
AsiaYe
2019/11/06
2.5K0
mysql中的字符集和校验规则
native2ascii命令详解
1、native2ascii简介: native2ascii是sun java sdk提供的一个工具。用来将别的文本类文件(比如.txt,.ini,.properties,.java等等)编码转为Unicode编码。为什么要进行转码,原因在于程序的国际化。Unicode编码的定义:Unicode(统一码、万国码、单一码)是一种在计算机上使用的字符编码。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。 2、获取native2ascii: 安装了jdk后,假如你是在windows上安装,那么在jdk的安装目录下,会有一个bin目录,其中native2ascii.exe正是。
程序员云帆哥
2022/05/12
4110
设置程序运行时的字符编码
  关于可设置的编码可参考(windows平台): https://docs.microsoft.com/zh-cn/windows/win32/Intl/code-page-identifiers
Qt君
2020/02/27
1.5K0
native2ascii命令_native method
native2ascii 是一个关于转码的不错的命令.使用条件简单,只要安装了jdk之后,在cmd窗口就可以使用该命令对文件进行转码,而且转码过程是可逆的.安装路径下bin目录下,有一个native2ascii 批处理文件也可以完成转码.
全栈程序员站长
2022/10/01
3930
linux之iconv命令
iconv -f encoding [-t encoding] [inputfile]...
入门笔记
2021/09/14
1.6K0
java properties native2ascii_使用native2ascii针对中文乱码,进行转码操作,用于native2ascii处理properties文件…
native2ascii是sun java sdk提供的一个转码工具, 用来将别的文本类文件(比如 *.txt, *.ini, *.properties, *.java 等等)编码转为Unicode编码。
全栈程序员站长
2022/10/01
6150
java properties native2ascii_使用native2ascii针对中文乱码,进行转码操作,用于native2ascii处理properties文件…
java文件转码工具-native2ascii.exe命令简介
java的转码工具,需要java.dll动态库和相关程序才能执行。native2ascii.exe 是 Java 的一个文件转码工具,是将特殊各异的内容 转为 用指定的编码标准文体形式统一的表现出来,它通常位于 JDK_home\bin 目录下,安装好 Java SE 后,可在命令行直接使用 native2ascii 命令进行转码。JDK自带的工具native2ascii可以将uncode编码的文件转换为本地编码的文件,但是不能批量转换文件。
全栈程序员站长
2022/10/02
6550
MySQL中的字符集和校对学习--MySql语法
MySQL服务器能够支持多种字符集。可以使用SHOW CHARACTER SET语句列出可用的字符集:
用户1289394
2021/07/30
8980
native2ascii.exe使用方式
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
奋飛
2019/08/15
9160
native/ascii在线转换工具_中文转ascii
Property文件中,使用的编码根据机器的设置可能是GBK或者UTF-8。而在Java中读取Property文件时使用的是Unicode编码,编码方式不同会导致中文乱码,因此需要将Property文件中的中文字符转化成Unicode编码才能正常显示中文。
全栈程序员站长
2022/10/01
2.5K0
native ascii_编码转换在线
大家好,又见面了,我是你们的朋友全栈君。 1、获取native2ascii:安装了jdk后,假如你是在windows上安装,那么在jdk的安装目录下,会有一个bin目录,其中native2ascii.exe正是。
全栈程序员站长
2022/10/01
1.7K0
真棒!彻底解决了一直困扰的编码问题
在批量处理文件(后缀包括 ".csv"、".xlsx"、".xls"的文件)时,可能会出现同一个文件夹内同时存在不同编码的文件;亦或非"utf-8"格式的其他格式,即便相同格式也会出现有些文件能打开,而有些文件不能打开。
数据STUDIO
2021/06/24
1.3K0
>> 技术应用:MySQL - 字符编码表
在MySQL中,字符集的概念和编码方案被看做是同义词,一个字符集是一个转换表和一个编码方案的组合。
艾特
2023/10/10
3280
关于GreatSQL字符集的总结
最近的SQL优化工作中经常遇到因字符集或校验规则不一致导致索引使用不了的问题,修改表的字符集或校验规则相当于把表重构,表中数据量大时,处理起来费时费力,希望应用开发者在设计之初时注意到此问题,让后期接手运维的小伙伴少一些负担。GreatSQL的字符集和校验规则种类繁多,提供灵活性的同时,也带来使用混乱的烦恼。本文对字符集做一个总结,让读者对GreatSQL的字符集有一个全面的了解。
GreatSQL社区
2023/12/20
2320
关于GreatSQL字符集的总结
MySQL的字符集和乱码问题
#字符编码:就是人类使用的英文字母、汉字、特殊符号等信息,通过转换规则,将其转换为计算机可以识别的二进制数字的一种编码方式
老油条IT记
2020/03/20
2.3K0
Linux下Mysql数据库的基础操作
Mysql数据库是一种关系型数据库管理系统,具有的优点有体积小、速度快、总体成本低,开源,可移植性(跨平台,在不同系统中使用),可以和开发语结合,属于轻量级数据库。
江湖有缘
2023/11/11
4280
MySQL常用show命令使用总结
MySQL中有很多的基本命令,show命令也是其中之一,在很多使用者中对show命令的使用还容易产生混淆,本文汇集了show命令的众多用法。show命令可以提供关于数据库、表、列,或关于服务器的状态信息。
星哥玩云
2022/08/18
1.3K0
java编码native2ascii下载_native2ascii.exe
native2ascii.exe是一款好用的转码工具,主要用于字符转码和反转码,在Java开发过程中总会出现一些乱码问题或者无法正确识别读取的问题,这时候就需要进行转码,可对属性文件和其他字符编码进行转换,从而解决上述问题。需要的Java开发人员可下载!
全栈程序员站长
2022/10/01
7290
java编码native2ascii下载_native2ascii.exe
相关推荐
native2ascii用法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档