参考来源: 使用DDD指导业务设计的一点思考 https://insights.thoughtworks.cn/ddd-business-design/ 后端开发实践系列——领域驱动设计(DDD)编码实践 https://insights.thoughtworks.cn/backend-development-ddd
领域驱动设计指导业务设计
领域驱动设计是什么
领域驱动设计是 Eric Evans 提出的一种软件设计方法和思想,主要解决业务系统的设计和建模。
理论发展过程
-
DDD(Domain-Driven Design,领域驱动设计),对应中文版为《领域驱动设计》。
-
Implement Domain-Driven Design,简称IDDD,中文版《实现领域驱动设计》。 3.《领域驱动设计模式、原理与实践》问世,简称PPPDDD。
-
IDDD的精华版DDDD(Domain-Driven Design Distilled),《领域驱动设计精粹》。
模型和领域模型
模型是能够表达系统业务逻辑和状态的对象。 模型,用来反映事物某部分特征的物件,无论是实物还是虚拟的。 领域,指的特定行业或者场景下的业务逻辑。 DDD 中的模型是指反应 IT 系统的业务逻辑和状态的对象,是从具体业务(领域)中提取出来的,因此又叫做领域模型。
我们可以吧系统复杂的问题分为两类:
-
业务复杂度
-
技术复杂度 技术复杂度,软件设计中和技术实现相关的问题,例如处理用户输入,持久化模型,处理网络通信等。 业务复杂度,软件设计中和业务逻辑相关的问题,例如为订单添加商品,需要计算订单总价,应用折扣规则等。
识别上下文的边界是 DDD 中最难得一部分,同时上下文边界是由业务变化动态变化的,我们把识别出边界的上下文叫做限界上下文(Bounded Context)。 使用上下文之后,带来另外一个收获。模型之间本质上没有多对多关系,如果有,说明存在一个隐含的成员关系,这个关系没有被充分的分析出来,对后期的开发会造成非常大的困扰。
我们将那相关性极强的领域模型放到一起考虑,数据的一致性必须解决,同时生命周期也需要保持同步,我们把这个集合叫做聚合。
聚合中需要选择一个代表负责和全局通信,类似于一个部门的接口人,这样就能确保数据保持一致。我们把这个模型叫做聚合根。
相对于非聚合根的模型,我们叫做实体。
image我们把没有自己生命周期的模型,仅用来呈现多个字段的值的模型和对象,称作为值对象。
image使用领域模型指导程序设计
指导数据库设计
使用领域模型建立数据库的要点:
-
留意多对多关系,并拆解成一对多关系
-
值对象和实体往往为一对一关系
-
使用 ORM 时,聚合根和实体可以配置为级联删除和更新
-
禁止聚合根之间进行关联
指导 API 设计
RESTful API 已经变成了主流 API 设计方式,当设计好领域对象后,设计 API 的难度大大降低。
使用聚合根作为 URI 的根路径,使用实体作为子路径。通过 ID 作为 Path 参数。 image指导对象设计
领域模型解决业务复杂度的问题,领域模型只应该被用作处理业务逻辑,存储、业务表现都应该和领域模型无关。 可以把这些 Plain Object 分为三类:
-
DTO,和交互相关或者和后端、第三方服务对接
-
Entity,数据库表映射
-
Model,领域模型
指导代码组织
代码组织,通俗来说就是如何分包。 DDD 特别的抽离出一层叫做 application。这一层是 DDD 的精华,领域模型关心业务逻辑,但是不关心业务场景。
application 用来隔离业务场景,显得非常重要。 image领域驱动设计(DDD)编码实践
实现业务的3种常见方式
-
基于“Service + 贫血模型”的实现 这种方式当前被很多软件项目所采用,主要的特点是:存在一个贫血的“领域对象”,业务逻辑通过一个Service类实现,然后通过setter方法更新领域对象,最后通过DAO(多数情况下可能使用诸如Hibernate之类的ORM框架)保存到数据库中。 这种方式依然是一种面向过程的编程范式,违背了最基本的OO原则。另外的问题在于职责划分模糊不清,使本应该内聚在Order中的业务逻辑泄露到了其他地方(OrderService),导致Order成为一个只是充当数据容器的贫血模型(Anemic Model),而非真正意义上的领域模型。在项目持续演进的过程中,这些业务逻辑会分散在不同的Service类中,最终的结果是代码变得越来越难以理解进而逐渐丧失扩展能力。
-
基于事务脚本的实现 我们会发现领域对象(Order)存在的唯一目的其实是为了让ORM这样的工具能够一次性地持久化,在不使用ORM的情况下,领域对象甚至都没有必要存在。于是,此时的代码实现便退化成了事务脚本(Transaction Script),也就是直接将Service类中计算出的结果直接保存到数据库(或者有时都没有Service类,直接通过SQL实现业务逻辑)。 在系统足够简单的情况下完全可以采用。但是:一方面“简单”这个度其实并不容易把握;另一方面软件系统通常会在不断的演进中加入更多的功能,使得原本简单的代码逐渐变得复杂。
-
基于领域对象的实现 在这种方式中,核心的业务逻辑被内聚在行为饱满的领域对象(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中调用:
@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应该具有的职责。
网友评论