DDD领域驱动设计实践


参考来源:
使用DDD指导业务设计的一点思考 https://insights.thoughtworks.cn/ddd-business-design/
后端开发实践系列——领域驱动设计(DDD)编码实践 https://insights.thoughtworks.cn/backend-development-ddd

领域驱动设计指导业务设计

领域驱动设计是什么

领域驱动设计是 Eric Evans 提出的一种软件设计方法和思想,主要解决业务系统的设计和建模。

理论发展过程

  1. DDD(Domain-Driven Design,领域驱动设计),对应中文版为《领域驱动设计》。
  2. Implement Domain-Driven Design,简称IDDD,中文版《实现领域驱动设计》。
    3.《领域驱动设计模式、原理与实践》问世,简称PPPDDD。
  3. IDDD的精华版DDDD(Domain-Driven Design Distilled),《领域驱动设计精粹》。

模型和领域模型

模型是能够表达系统业务逻辑和状态的对象。
模型,用来反映事物某部分特征的物件,无论是实物还是虚拟的。
领域,指的特定行业或者场景下的业务逻辑。
DDD 中的模型是指反应 IT 系统的业务逻辑和状态的对象,是从具体业务(领域)中提取出来的,因此又叫做领域模型。

我们可以吧系统复杂的问题分为两类:

  • 业务复杂度
  • 技术复杂度
    技术复杂度,软件设计中和技术实现相关的问题,例如处理用户输入,持久化模型,处理网络通信等。
    业务复杂度,软件设计中和业务逻辑相关的问题,例如为订单添加商品,需要计算订单总价,应用折扣规则等。

识别上下文的边界是 DDD 中最难得一部分,同时上下文边界是由业务变化动态变化的,我们把识别出边界的上下文叫做限界上下文(Bounded Context)
使用上下文之后,带来另外一个收获。模型之间本质上没有多对多关系,如果有,说明存在一个隐含的成员关系,这个关系没有被充分的分析出来,对后期的开发会造成非常大的困扰。

我们将那相关性极强的领域模型放到一起考虑,数据的一致性必须解决,同时生命周期也需要保持同步,我们把这个集合叫做聚合

聚合中需要选择一个代表负责和全局通信,类似于一个部门的接口人,这样就能确保数据保持一致。我们把这个模型叫做聚合根

相对于非聚合根的模型,我们叫做实体

我们把没有自己生命周期的模型,仅用来呈现多个字段的值的模型和对象,称作为值对象

使用领域模型指导程序设计

指导数据库设计

使用领域模型建立数据库的要点:

  • 留意多对多关系,并拆解成一对多关系
  • 值对象和实体往往为一对一关系
  • 使用 ORM 时,聚合根和实体可以配置为级联删除和更新
  • 禁止聚合根之间进行关联

指导 API 设计

RESTful API 已经变成了主流 API 设计方式,当设计好领域对象后,设计 API 的难度大大降低。

使用聚合根作为 URI 的根路径,使用实体作为子路径。通过 ID 作为 Path 参数。

指导对象设计

领域模型解决业务复杂度的问题,领域模型只应该被用作处理业务逻辑,存储、业务表现都应该和领域模型无关。
可以把这些 Plain Object 分为三类:

  • DTO,和交互相关或者和后端、第三方服务对接
  • Entity,数据库表映射
  • Model,领域模型

指导代码组织

代码组织,通俗来说就是如何分包。
DDD 特别的抽离出一层叫做 application。这一层是 DDD 的精华,领域模型关心业务逻辑,但是不关心业务场景。

application 用来隔离业务场景,显得非常重要。

领域驱动设计(DDD)编码实践

实现业务的3种常见方式

  1. 基于“Service + 贫血模型”的实现
    这种方式当前被很多软件项目所采用,主要的特点是:存在一个贫血的“领域对象”,业务逻辑通过一个Service类实现,然后通过setter方法更新领域对象,最后通过DAO(多数情况下可能使用诸如Hibernate之类的ORM框架)保存到数据库中。
    这种方式依然是一种面向过程的编程范式,违背了最基本的OO原则。另外的问题在于职责划分模糊不清,使本应该内聚在Order中的业务逻辑泄露到了其他地方(OrderService),导致Order成为一个只是充当数据容器的贫血模型(Anemic Model),而非真正意义上的领域模型。在项目持续演进的过程中,这些业务逻辑会分散在不同的Service类中,最终的结果是代码变得越来越难以理解进而逐渐丧失扩展能力。

  2. 基于事务脚本的实现
    我们会发现领域对象(Order)存在的唯一目的其实是为了让ORM这样的工具能够一次性地持久化,在不使用ORM的情况下,领域对象甚至都没有必要存在。于是,此时的代码实现便退化成了事务脚本(Transaction Script),也就是直接将Service类中计算出的结果直接保存到数据库(或者有时都没有Service类,直接通过SQL实现业务逻辑)。
    在系统足够简单的情况下完全可以采用。但是:一方面“简单”这个度其实并不容易把握;另一方面软件系统通常会在不断的演进中加入更多的功能,使得原本简单的代码逐渐变得复杂。

  3. 基于领域对象的实现
    在这种方式中,核心的业务逻辑被内聚在行为饱满的领域对象(Order)中,实现Order类如下:

    1
    2
    3
    4
    5
    6
    7
    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中调用:

1
2
3
4
5
6
7
@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应该具有的职责。


文章作者: KavenRan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 KavenRan !
 上一篇
研发团队状态评估模型 研发团队状态评估模型
评估目的:参考来源为Spotify团队管理实践,组织层面对研发小队的支持调查。我们进行了适应性改造,并探索出一套评估流程。 目的是为了帮助我们寻找团队需要聚焦于改善的点,以及了解团队需要哪些组织层面上的支持,便于组织更有针对性的协调资源并加
2020-09-29
下一篇 
ELK从入门到精通(三)——Logstash传输到Elasticsearch ELK从入门到精通(三)——Logstash传输到Elasticsearch
Logstash作为Elastic stack的重要组成部分,其最常用的功能是将数据导入到Elasticssearch中。将Logstash中的数据导入到Elasticsearch中操作也非常的方便,只需要在pipeline配置文件中增加E
2020-08-07
  目录