实习成长之路:后端开发实践系列——领域驱动设计(DDD)编码实践一
原文链接https://zhuanlan.zhihu.com/p/75931257
DDD总览
DDD分为战略设计和战术设计。在战略设计中,我们讲求的是子域和限界上下文(Bounded Context,BC)的划分,以及各个限界上下文之间的上下游关系。当前如此火热的“在微服务中使用DDD”这个命题,究其最初的逻辑无外乎是“DDD中的限界上下文可以用于指导微服务中的服务划分”。事实上,限界上下文依然是软件模块化的一种体现,与我们一直以来追求的模块化原则的驱动力是相同的,即通过一定的手段使软件系统在人的大脑中更加有条理地呈现,让作为“目的”的人能够更简单地了解进而掌控软件系统。
如果说战略设计更偏向于软件架构,那么战术设计便更偏向于编码实现。DDD战术设计的目的是使得业务能够从技术中分离并突显出来,让代码直接表达业务的本身,其中包含了聚合根、应用服务、资源库、工厂等概念。虽然DDD不一定通过面向对象(OO)来实现,但是通常情况下在实践DDD时我们采用的是OO编程范式,行业中甚至有种说法是“DDD是OO进阶”,意思是面向对象中的基本原则(比如SOLID)在DDD中依然成立。本文主要讲解DDD的战术设计。
本文以一个简单的电商订单系统为例,通过以下方式可以获取源代码:
git clone https://github.com/e-commerce-sample/order-backend
git checkout a443dace
实现业务的3种常见方式
在讲解DDD之前,让我们先来看一下实现业务代码的几种常见方式,在示例项目中有个“修改Order中Product的数量”的业务需求如下:
可以修改Order中Product的数量,但前提是Order处于未支付状态,Product数量变更后Order的总价(totalPrice)应该随之更新。
1.基于“Service + 贫血模型”的实现
这种方式当前被很多软件项目所采用,主要的特点是:存在一个贫血的“领域对象”,业务逻辑通过一个Service类实现,然后通过setter方法更新领域对象,最后通过DAO(多数情况下可能使用诸如Hibernate之类的ORM框架)保存到数据库中。实现一个OrderService类如下:
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {Order order = DAO.findById(id);if (order.getStatus() == PAID) {throw new OrderCannotBeModifiedException(id);}OrderItem orderItem = order.getOrderItem(command.getProductId());orderItem.setCount(command.getCount());order.setTotalPrice(calculateTotalPrice(order));DAO.saveOrUpdate(order);
}
这种方式依然是一种面向过程的编程范式,违背了最基本的OO原则。另外的问题在于职责划分模糊不清,使本应该内聚在Order中的业务逻辑泄露到了其他地方(OrderService),导致Order成为一个只是充当数据容器的贫血模型(Anemic Model),而非真正意义上的领域模型。在项目持续演进的过程中,这些业务逻辑会分散在不同的Service类中,最终的结果是代码变得越来越难以理解进而逐渐丧失扩展能力。
2.基于事务脚本的实现
在上一种实现方式中,我们会发现领域对象(Order)存在的唯一目的其实是为了让ORM这样的工具能够一次性地持久化,在不使用ORM的情况下,领域对象甚至都没有必要存在。于是,此时的代码实现便退化成了事务脚本(Transaction Script),也就是直接将Service类中计算出的结果直接保存到数据库(或者有时都没有Service类,直接通过SQL实现业务逻辑):
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {OrderStatus orderStatus = DAO.getOrderStatus(id);if (orderStatus == PAID) {throw new OrderCannotBeModifiedException(id);}DAO.updateProductCount(id, command.getProductId(), command.getCount());DAO.updateTotalPrice(id);
}
可以看到,DAO中多出了很多方法,此时的DAO不再只是对持久化的封装,而是也会包含业务逻辑。另外,DAO.updateTotalPrice(id)方法的实现中将直接调用SQL来实现Order总价的更新。与“Service+贫血模型”方式相似,事务脚本也存在业务逻辑分散的问题。
事实上,事务脚本并不是一种全然的反模式,在系统足够简单的情况下完全可以采用。但是:一方面“简单”这个度其实并不容易把握;另一方面软件系统通常会在不断的演进中加入更多的功能,使得原本简单的代码逐渐变得复杂。因此,事务脚本在实际的应用中使用得并不多。
3.基于领域对象的实现
在这种方式中,核心的业务逻辑被内聚在行为饱满的领域对象(Order)中,实现Order类如下:
public void changeProductCount(ProductId productId, int count) {if (this.status == PAID) {throw new OrderCannotBeModifiedException(this.id);}OrderItem orderItem = retrieveItem(productId);orderItem.updateCount(count);
}
然后在Controller或者Service中,调用Order.changeProductCount():
@PostMapping("/order/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {Order order = DAO.byId(orderId(id));order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());order.updateTotalPrice();DAO.saveOrUpdate(order);
}
可以看到,所有业务(“检查Order状态”、“修改Product数量”以及“更新Order总价”)都被包含在了Order对象中,这些正是Order应该具有的职责。(不过示例代码中有个地方明显违背了内聚性原则,下文会讲到,作为悬念读者可以先行尝试着找一找)
事实上,这种方式与本文要讲的DDD战术模式已经很相近了,只是DDD抽象出了更多的概念与原则。
基于业务的分包
所谓基于业务分包即通过软件所实现的业务功能进行模块化划分,而不是从技术的角度划分(比如首先划分出service和infrastruture等包)。在DDD的战略设计中,我们关注于从一个宏观的视角俯视整个软件系统,然后通过一定的原则对系统进行子域和限界上下文的划分。在战术实践中,我们也通过类似的提纲挈领的方法进行整体的代码结构的规划,所采用的原则依然逃离不了“内聚性”和“职责分离”等基本原则。此时,首先映入眼帘的便是软件的分包。
在DDD中,聚合根(下文会讲到)是主要业务逻辑的承载体,也是“内聚性”原则的典型代表,因此通常的做法便是基于聚合根进行顶层包的划分。在示例电商项目中,有两个聚合根对象Order和Product,分别创建order包和product包,然后在各自的顶层包下再根据代码结构的复杂程度划分子包,比如对于product包:
└── product├── CreateProductCommand.java├── Product.java├── ProductApplicationService.java├── ProductController.java├── ProductId.java├── ProductNotFoundException.java├── ProductRepository.java└── representation├── ProductRepresentationService.java└── ProductSummaryRepresentation.java
可以看到,ProductRepository和ProductController等多数类都直接放在了product包下,而没有单独分包;但是展现类ProductSummaryRepresentation却做了单独分包。这里的原则是:在所有类已经被内聚在了product包下的情况下,如果代码结构足够的简单,那么没有必要再次进行子包的划分,ProductRepository和ProductController便是这种情况;而如果多个类需要做再次的内聚,那么需要另行分包,比如通过REST API接口返回Product数据时,代码中涉及到了两个对象ProductRepresentationService和ProductSummaryRepresentation,这两个对象是紧密关联的,因此将他们放在representation子包下。而对于更加复杂的Order,分包如下:
├── order
│ ├── OrderApplicationService.java
│ ├── OrderController.java
│ ├── OrderPaymentProxy.java
│ ├── OrderPaymentService.java
│ ├── OrderRepository.java
│ ├── command
│ │ ├── ChangeAddressDetailCommand.java
│ │ ├── CreateOrderCommand.java
│ │ ├── OrderItemCommand.java
│ │ ├── PayOrderCommand.java
│ │ └── UpdateProductCountCommand.java
│ ├── exception
│ │ ├── OrderCannotBeModifiedException.java
│ │ ├── OrderNotFoundException.java
│ │ ├── PaidPriceNotSameWithOrderPriceException.java
│ │ └── ProductNotInOrderException.java
│ ├── model
│ │ ├── Order.java
│ │ ├── OrderFactory.java
│ │ ├── OrderId.java
│ │ ├── OrderIdGenerator.java
│ │ ├── OrderItem.java
│ │ └── OrderStatus.java
│ └── representation
│ ├── OrderItemRepresentation.java
│ ├── OrderRepresentation.java
│ └── OrderRepresentationService.java
可以看到,我们专门创建了一个model包用于放置所有与Order聚合根相关的领域对象;另外,基于同类型相聚原则,创建command包和exception包分别用于放置请求类和异常类。
实习成长之路:后端开发实践系列——领域驱动设计(DDD)编码实践一相关推荐
- 领域驱动设计(DDD)实践之路(四):领域驱动在微服务设计中的应用
这是"领域驱动设计实践之路"系列的第四篇文章,从单体架构的弊端引入微服务,结合领域驱动的概念介绍了如何做微服务划分.设计领域模型并展示了整体的微服务化的系统架构设计.结合分层架构. ...
- 领域驱动设计(DDD)实践之路(三):如何设计聚合
本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/oAD25H0UKH4zujxFDRXu9Q 作者:wenbo zhang [领域驱动设计实践 ...
- 领域驱动设计(DDD)在爱奇艺打赏业务的实践
领域驱动设计(Domain-Driven Design,以下简称DDD)思潮的形成要追述到30几年前,17年前,Eirc Evans定义了领域驱动设计的概念.DDD一直为传统行业的软件工程师提供软件设 ...
- 业务单据进行领域驱动设计的最佳实践
作者:少岚 阿里同城履约物流技术团队 本文以电商购物场景为例,介绍了领域驱动设计的应用实践.你会发现,DDD的核心目标就是通过各种实用性的方法和技巧提炼出具有体现问题实质的领域模型,协作解决领域问题, ...
- python 全栈开发,Day116(可迭代对象,type创建动态类,偏函数,面向对象的封装,获取外键数据,组合搜索,领域驱动设计(DDD))...
昨日内容回顾 1. 三个类 ChangeList,封装列表页面需要的所有数据.StarkConfig,生成URL和视图对应关系 + 默认配置 AdminSite,用于保存 数据库类 和 处理该类的对象 ...
- 读美团的营销业务实践以及领域驱动设计有后感
文章目录 1 背景 2 前言 3 实体类.值对象.聚合根 4 领域.领域对象.限界上下文 5 资源库 6 防腐层 7 领域服务 8 数据流转 9 DDD系统架构图 10 营销业务的实践 11 总结 1 ...
- 领域驱动设计(DDD)实践之路(一)
本文首发于 vivo互联网技术 微信公众号 链接: https://mp.weixin.qq.com/s/gk-Hb84Dt7JqBRVkMqM7Eg 作者:张文博 领域驱动设计(Domain ...
- 微服务与领域驱动设计,架构实践总结
一.软件复杂性 1.复杂原因 如果软件系统存在持续的迭代周期,那么其中业务.技术.架构的复杂性都会直线拉升,其相应的开发难度也会提高,可以用一句话总结其根本原因:唯一不变的就是变化: 业务变化:导致复 ...
- ABP学习实践(十六)--领域驱动设计(DDD)回顾
ABP框架并没有实现领域驱动设计(DDD)的所有思想,但是并不妨碍用领域驱动的思想去理解ABP库框架. 1.领域驱动设计(DDD)与微服务(MicroService)的关系? 领域驱动设计(DDD)是 ...
- bdd行为驱动开发 java_行为驱动开发(BDD)如何与领域驱动设计(DDD)结合?
BDD是从TDD发展过来的,也属于DDD中一种描述业务的无处不在的统一语言,它的描述格式是: As a [Role] I want [Feature] so that [benefit] 用中文的意思 ...
最新文章
- Datawhale实验室
- 设计模式六大原则之白话讲解
- linux gcc strip 去文件头工具
- leetcode-458-Poor Pigs
- OpenCV gPhoto2 VideoCapture的用法(附完整代码)
- 文件创建时间、访问时间、修改时间
- flex 左右布局_移动端开发常用布局:前端弹性布局总结
- 支持向量机SVM推导
- 效率工具Focus for Mac,阻止应用弹窗,集中注意力
- 无人机在土地测绘中的应用
- 我的个人博客网站是怎么制作的?
- U盘安装苹果系统教程,菜鸟一步一步也能成大牛
- 项目管理甘特图-动态时间轴
- 2022中级Android开发面试解答,当上项目经理才知道
- python os.path.abspath()与os.path.realpath()区别
- 物联网之STM32开发一(基础知识)
- 2014 年放弃阿里巴巴offer 的人是否格外多?如果是,为什么?
- 环境变量:系统变量和用户变量
- js ascii码使用攻略
- 这个技术发展到现在就为了让你能愉快地多打几局游戏
热门文章
- qt定时器暂停与重新开始_Qt编写自定义控件22-蚂蚁线
- 改键走a显示攻击距离_英雄联盟谁才是真正的走A怪?大神钟爱VN,新手都去玩复仇之矛!...
- Pannellum:实例之自动加载全景图
- 行业动态_天才、忽悠与炮灰
- 眼底图像血管增强与分割--(1)匹配滤波算法原理及实现
- c++类详解:访问权限,构造函数,拷贝构造函数,析构函数
- VS2017编写C++多文件时,出现LNK2005、LNK1169报错的解决方法
- 非常详细的机器学习知识点汇总(二)之SVM23问
- 面试之手撕BP反向传播
- c++ string