什么是领域驱动设计(DDD): 领域驱动设计和实践如何做

引言

软件系统面向对象的设计思想可谓历史悠久,20 世纪 70 年代的 Smalltalk 可以说是面向对象语言的经典,直到今天我们依然将这门语言视为面向对象语言的基础。随着编程语言和技术的发展,各种语言特性层出不穷,面向对象是大部分语言的一个基本特性,像 C++、Java、C#这样的静态语言,Ruby、Python 这样的动态语言都是面向对象的语言。

但是面向对象语言并不是银弹,如果开发人员认为使用面向对象语言写出来的程度本身就是面向对象的,那就大错特错了,实际开发中,大量的业务逻辑堆积在一个巨型类中的例子屡见不鲜,代码的复用性和扩展性无法得到保证。为了解决这样的问题,领域驱动设计提出了清晰的分层架构和领域对象的概念,让面向对象的分析和设计进入了一个新的阶段,对企业级软件开发起到了巨大的推动作用。

本文主要介绍了领域驱动设计的基本概念、要素、特点,对比了事务脚本和领域模型的特点,最后介绍了我们在软件开发过程中的领域驱动设计实践。

什么是领域驱动设计(DDD)

2004 年著名建模专家 Eric Evans 发表了他最具影响力的书籍:《Domain-Driven Design –Tackling Complexity in the Heart of Software》(中文译名:领域驱动设计—软件核心复杂性应对之道),书中提出了“领域驱动设计 (简称 DDD)”的概念。

领域驱动设计事实上是针对 OOAD 的一个扩展和延伸,DDD 基于面向对象分析与设计技术,对技术架构进行了分层规划,同时对每个类进行了策略和类型的划分。

领域模型是领域驱动的核心。采用 DDD 的设计思想,业务逻辑不再集中在几个大型的类上,而是由大量相对小的领域对象 (类) 组成,这些类具备自己的状态和行为,每个类是相对完整的独立体,并与现实领域的业务对象映射。领域模型就是由这样许多的细粒度的类组成。基于领域驱动的设计,保证了系统的可维护性、扩展性和复用性,在处理复杂业务逻辑方面有着先天的优势。

领域驱动设计的特点

领域驱动的核心应用场景就是解决复杂业务的设计问题,其特点与这一核心主题息息相关:

  1. 分层架构与职责划分:领域驱动设计很好的遵循了关注点分离的原则,提出了成熟、清晰的分层架构。同时对领域对象进行了明确的策略和职责划分,让领域对象和现实世界中的业务形成良好的映射关系,为领域专家与开发人员搭建了沟通的桥梁。
  2. 复用:在领域驱动设计中,领域对象是核心,每个领域对象都是一个相对完整的内聚的业务对象描述,所以可以形成直接的复用。同时设计过程是基于领域对象而不是基于数据库的 Schema,所以整个设计也是可以复用的。
  3. 使用场景:适合具备复杂业务逻辑的软件系统,对软件的可维护性和扩展性要求比较高。不适用简单的增删改查业务。

如果不使用 DDD?

面对复杂的业务场景和需求,如果没有建立和实现领域模型,会导致应用架构出现“胖服务层”和“贫血的领域模型”,在这样的架构中,Service 层开始积聚越来越多的业务逻辑,领域对象则成为只有 getter 和 setter 方法的数据载体。这种做法还会导致领域特定业务逻辑和规则散布于多个的 Service 类中,有些情况下还会出现重复的逻辑。我们曾经见过 5000 多行的 Service 类,上百个方法,代码基本上是不可读的。

在大多数情况下,贫血的领域模型没有成本效益。它们不会给公司带来超越其它公司的竞争优势,因为在这种架构里要实现业务需求变更,开发并部署到生产环境中去要花费太长的时间。

领域驱动设计的分层架构和构成要素

下面我们简单介绍一下领域驱动设计的分层架构和构成要素,这部分内容在 Eric Evans 的书中有非常详尽的描述,想要详细了解的,最好去读原版书籍。

下面这张图是该书中著名的分层架构图,如下:

整个架构分为四层,其核心就是领域层(Domain),所有的业务逻辑应该在领域层实现,具体描述如下:

用户界面 / 展现层

负责向用户展现信息以及解释用户命令。

应用层

很薄的一层, 用来协调应用的活动。它不

包含业务逻辑。它不保留业务对象的状态,

但它保有应用任务的进度状态。

领域层

本层包含关于领域的信息。这是业务软件

的核心所在。在这里保留业务对象的状态,

对业务对象和它们状态的持久化被委托给

了基础设施层。

基础设施层

本层作为其他层的支撑库存在。它提供了

层间的通信, 实现对业务对象的持久化,

包含对用户界面层的支撑库等作用。

领域驱动设计除了对系统架构进行了分层描述,还对对象(Object)做了明确的职责和策略划分:

  1. 实体(Entities):具备唯一 ID,能够被持久化,具备业务逻辑,对应现实世界业务对象。
  2. 值对象(Value objects):不具有唯一 ID,由对象的属性描述,一般为内存中的临时对象,可以用来传递参数或对实体进行补充描述。
  3. 工厂(Factories):主要用来创建实体,目前架构实践中一般采用 IOC 容器来实现工厂的功能。
  4. 仓库(Repositories):用来管理实体的集合,封装持久化框架。
  5. 服务(Services):为上层建筑提供可操作的接口,负责对领域对象进行调度和封装,同时可以对外提供各种形式的服务。

当然,DDD 中还提出了聚合和聚合根(Aggregate Root)的概念,不过我们在实践过程发现聚合根有问题复杂化的倾向,用传统的聚合、组合等概念去描述领域对象之间的关系更容易理解,所以这里对这个概念就不做介绍了。

事务脚本和领域模型

Martin Fowler 2004 年所著的企业应用架构模式(Patterns of Enterprise Application Architecture)中的第九章领域逻辑模式(Domain Logic Patterns)专门介绍了事务脚本(Transaction Script)和领域模型(Domain Model),理解这两种模式对设计和构建企业应用软件非常有帮助,所以有必要介绍一下。

事务脚本:

事务脚本的核心是过程,通过过程的调用来组织业务逻辑,每个过程处理来自表现层的单个请求。大部分业务应用都可以被看成一系列事务,从某种程度上来说,通过事务脚本处理业务,就像执行一条条 Sql 语句来实现数据库信息的处理。事务脚本把业务逻辑组织成单个过程,在过程中直接调用数据库,业务逻辑在服务(Service)层处理。

事务脚本模式可以简单的通过 UML 图表示成这样:

由 Action 层处理 UI 层的动作请求,将 Request 中的数据组装后传递给 BusinessService,BS 层做简单的逻辑处理后,调用数据访问对象进行数据持久化,其中 VO 充当了数据传输对象的作用,一般是贫血的 POJO,只具备 getter 和 setter 方法,没有状态和行为。

事务脚本模式的特点是简单容易理解,面向过程设计。对于少量逻辑的业务应用来说,事务脚本模式简单自然,性能良好,容易理解,而且一个事务的处理不会影响其他事务。不过缺点也很明显,对于复杂的业务逻辑处理力不从心,难以保持良好的设计,事务之间的冗余代码不断增多,通过复制粘贴方式进行复用。可维护性和扩展性变差。

领域模型:

领域模型的特点也比较明显, 属于面向对象设计,领域模型具备自己的属性行为状态,并与现实世界的业务对象相映射。各类具备明确的职责划分,领域对象元素之间通过聚合和引用等关系配合解决实际业务应用和规则。可复用,可维护,易扩展,可以采用合适的设计模型进行详细设计。缺点是相对复杂,要求设计人员有良好的抽象能力。

领域模型对应的就是领域驱动设计中划分的领域层,这里就不详细讨论了。

在实际的设计中,我们需要根据具体的需求选择相应的设计模式。具备复杂业务逻辑的核心业务系统适合使用领域模型,简单的信息管理系统可以考虑采用事务脚本模式。

领域驱动设计实践

下面主要讲一下我们在构建企业级应用开发平台中对 DDD 的实践和扩展。

本人近年来一直在从事企业级应用开发平台的相关工作,GAP 平台是我们的一个软件产品,用来解决企业级软件开发过程中复用、快速开发和过程规范等问题。设计这样一个平台,从底层的框架上就应该能够支撑复杂业务逻辑的系统构建,所以我们在大的架构设计思路上采用了领域驱动设计的思路,并根据实际采用的技术和要实现的功能对 DDD 的四层架构进行了细化和实现:

整个平台采用了 JavaEE 的技术及其相关的开源框架。系统的核心业务逻辑由 Domain 层处理,其中的业务服务(BusinessService)负责处理某个相对内聚的业务逻辑单元,同时对内对外提供本地或远程的服务。

下面是对各层的简要描述:

  1. View:展示层,由于 GAP 平台主要面向 B/S 架构,展示层主要由 web 资源文件组成,包括 JSP,JS 和大量的界面控件,同时还采用了 AJAX 和 Flex 等 RIA 技术,负责向用户展现丰富的界面信息,并执行用户的命令。
  2. Control:控制层,负责展示层请求的转发、调度和基础验证,同时自动拦截后台返回的 Runtime 异常信息,如果控制层需要与第三方系统交互,可以通过 Action 做远程的请求。
  3. Domain:领域层,是系统最为丰富的一层,主要负责处理整个系统的业务逻辑。这一层包括业务服务和领域对象,同时负责系统的事务管理。其中业务服务可以提供本地调用和共享远程服务的功能。
  4. Persistence:持久化层,主要负责数据持久化,支持 O/R Mapping 和 JDBC。对数据源的访问提供多种方式。

另外, 我们引入了 Spring 的 IOC 容器,系统的控制层、领域层和持久化层元素都有 IOC 容器统一管理,实现完全的接口分离和解耦。同时在控制、领域和持久化层都可以引用日志服务。

我们对领域驱动要素的定义上和原有的命名和含义上稍有区别。

原来的服务(Service),我们定义为业务服务(BusinessService),面向业务服务的架构是 GAP 平台的核心设计思想,一个业务服务可以由一个或多个领域模型和数据访问对象(DAO)组成,去实现一个完整的业务逻辑单元。业务服务主要负责事务处理和维护各个领域对象之间的关系,同时为上层访问提供本地和远程服务,服务类型包括 Web Service,RMI 等。

领域对象由实体(Entity)和值对象(VO)构成,实体类具备自己的属性和行为、状态,可以聚合 VO,实体类之间可以有聚合关联等关系,可以由数据访问对象(DAO)进行持久化。

持久化由数据访问对象(DAO)实现,不处理业务逻辑,主要负责实体类的持久化。提供多种持久化方式(O/R Mapping 和 JDBC)。

那么如何在去实现领域驱动设计呢?我们总结了以下四个步骤:

  1. 确定业务服务(Business Service):根据业务需求和功能模块划分,确定业务单元,每个 Business Service 是一个内聚的业务单元,覆盖相关的领域对象。
  2. 定义领域对象(Entity, VO):根据业务单元的业务逻辑定义领域对象,通过 UML 方法和设计模式描述领域对象。
  3. 定义领域对象的属性和关联关系:确定领域对象的各种属性和各个领域对象之间的关联关系。
  4. 为领域对象增加行为:根据业务需求(系统用例和界面原型等)为领域对象增加行为,并定义哪些方法要被业务服务引用。

案例——网上书店

为了更好的理解领域驱动设计,我们基于以上设计方法,实现了一套简单的网上书店系统。

网上书店系统是采用 DDD 设计思想构建的一个应用系统示例。通过网上书店系统,可以快速理解领域驱动设计。该系统实现网上书店的常用功能:包括浏览书籍、挑选书籍、提交订单、查看订单、自动折扣、处理订单、取消订单等。未登录用户可以浏览和挑选书籍;已登录用户可以提交和查看自己相关的订单;管理员可以处理订单。

经过业务抽象,即使是这样一个简单的业务场景也包含了很多领域对象,例如订单、账户、书籍、购物车、购物项、折扣等,通过分析和设计,我们可以得到这样的设计图(为了查看方便,图中的类隐藏了属性信息):

BookStoreAction 负责处理展现层的请求,并把请求转发给业务服务 IBookStoreBS,业务服务负责调度上图中显示的领域对象,处理该场景的所有业务。

其中领域对象和现实业务的对应关系为:

  • Account——账户
  • Order——订单
  • Book——书籍
  • Cart——购物车
  • Item——订单项
  • Discount——折扣

与事务脚本的编程模式不同,领域驱动设计不是把业务逻辑放在 BS(BusinessService)中,而是由具备属性、行为和状态的领域对象处理。例如 Order 类,如果是贫血的 POJO,那它内部只有与数据表字段对应的属性以及 getter 和 setter 方法,而在领域驱动设计中,则是一个相对独立的、能够处理自身关联业务的领域对象。在本系统中,我们对 Order 的描述如下:

订单的实现类是 gap.template.bookstore.model.Order,类中除了联系方式、邮寄地址等基本属性外,还有以下领域相关的行为:

  1. init(…),结算时调用方法,根据当前用户与购物车中的 Items 初始化订单,供用户修改。
  2. submit(…),提交订单时调用的方法,保存订单。
  3. cancel(…),取消订单,把订单和相关 item 的状态设置为“已取消”,然后委托 Dao 进行持久化。
  4. dispose(…),处理订单,首先更新订单项的状态,然后委托 Dao 持久化订单数据。
  5. reSubmit、setItemsStatus…

通过以上的描述,我们可以看到,Order 类基本上覆盖了现实世界中订单这个业务的所有行为和状态,是相对内聚的,这样的特性使其复用性大大增加,即使未来开发新的模块,涉及到订单业务的,可以直接复用 Order 类。同时在后期维护中,如果我想了解订单的业务,直接读 Order 的代码就可以了。

从上图中我们还可以清晰的看到各个领域对象之间的关系。Order 和 Cart 都聚合了 Item,对应都是 1…n,Item 聚合了 Book,对应关系 1…1。Order 分别与折扣、账户发生关联和调用等等,整个网上书店的场景就这样描述出来了。

另外,不要忘了 BS,除了起到基础设施的作用外(事务管理和服务共享),它还要负责调度和维护领域对象之间的关系。因为总会有些业务逻辑,既不属于这个领域对象,也不属于那个,那这部分业务由谁来处理呢?由 BS 来处理。例如在管理员处理订单这个场景中,首先需要根据订单信息获取账户,根据账户信息确定折扣率,同时进行余额校验,如果校验通过,就会调用订单对象的 dispose 方法处理订单,这个场景会涉及到 Order、Account、Discount 等对象,这样的业务逻辑,应该由 BS 实现。

IBookStoreDao 是数据访问对象,可以被 BS 调用,用来持久化对象,也可以被领域对象引用,用来持久化自身。

通过以上的描述,我们可以看到,整个设计和实现是优雅、清晰的。业务逻辑没有堆积在 BS 中,而是分散在 BS 和各个领域对象中,服务和对象都与现实世界的业务息息相关,无论是对领域专家、开发人员和后期维护人员,都能这种方式中获得自己需要的内容。

总结

我们采用领域驱动设计相对比较早,就我个人的检验和实践而言,DDD 对构建企业级应用开发平台和大型核心业务系统的作用是非常明显的,无论是在产品的稳定性、扩展性、可维护性、生命周期等方面都有显著的提升。

但是,由于这样那样的原因(复杂度、工期、开发人员能力限制等等),很多人会不自觉的抵制采用 DDD,有时候一个软件项目重写了两次,第二次依然不去做良好的设计。事实上采用了 DDD 的设计方法,我们的设计阶段已经变得非常轻量级和敏捷了,开发人员只要能够把领域模型之间的关系画出来并描述说明,并与需求人员达成一致,那么做出来的东西基本上是靠谱的。

在技术领域,只有主动的尝试和提升,效果才是最明显的。很多人问过我,如何开始学习和实践 XXX,其实很简单,现在就开始吧!

参考资料

《 领域驱动设计—软件核心复杂性应对之道》,Evans Eric 著,Addison-Wesley 出版社

《企业应用架构模式》, Martin Fowler 著, Addison-Wesley 出版社

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/139768.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

大数据 Hive 数据仓库介绍

目录 一、​​数据仓库概念 二、场景案例:数据仓库为何而来? 2.1 操作型记录的保存 2.2 分析型决策的制定 2.3 OLTP 环境开展分析可行吗? 2.4 数据仓库的构建 三、数据仓库主要特征 3.1 面向主题性(Subject-Orient…

【Web开发 | Django】数据库分流之道:探索Django多数据库路由最佳实践

🤵‍♂️ 个人主页: AI_magician 📡主页地址: 作者简介:CSDN内容合伙人,全栈领域优质创作者。 👨‍💻景愿:旨在于能和更多的热爱计算机的伙伴一起成长!!&…

iOS线上闪退问题解决方案

iOS线上闪退问题的收集工具是关键,它们可以帮助你及时发现和解决应用程序中的崩溃问题。以下是一些常用的iOS线上闪退问题收集工具及其使用方法,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎交流合…

绘图系统六:动态三维展示

文章目录 时间轴单帧跳转动图绘制函数接口优化 📈一 三维绘图系统 📈二 多图绘制系统📈三 坐 标 轴 定 制📈四 定制绘图风格 📈五 数据生成导入源码地址 Python打造动态绘图系统 时间轴 三维并不是人类理解的极限&am…

精品Python数字藏品购物商城爬虫-可视化大屏

《[含文档PPT源码等]精品基于Python实现的数字藏品爬虫》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程等 软件开发环境及开发工具: 开发语言:python 使用框架:Django 前端技术:JavaScript、VUE.js&a…

PHP-composer安装扩展安装,批量操作合并pdf

清除Composer缓存: 运行以下命令来清除Composer的缓存,并再次尝试安装包。 bash composer clear-cache 使用不同的镜像源: Composer使用的默认包源可能会受到限制或访问问题。你可以切换到使用其他镜像源,如阿里云、Composer中国…

Nginx之gzip模块解读

目录 gzip基本介绍 gzip工作原理 Nginx中的gzip 不建议开启Nginx中的gzip场景 gzip基本介绍 gzip是GNUzip的缩写,最早用于UNIX系统的文件压缩。HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术,web服务器和客户端(浏览器&…

科目三基础四项(一)

​ 第一天,基础操作,仪表,方向,挡位 按照模块来 1、方向盘两手在两侧 ​ 编辑 转向时的角度,只用:向左540,向右180 向左打和向右打的角度要抵消,回正 掉头向左打满再回 注意…

【STM32】IAP升级 预备知识

IAP(In Application Programming)简介 Flash够大的情况下,上电后的程序通过修改 MSP 的方式,可以在一块Flash上存在多个功能差异的程序。 IAP是为了在执行正常功能前,为了升级功能,提前运行的一段程序。这…

微软在Windows 11推出Copilot,将DALL-E 3集成在Bing!

美东时间9月21日,微软在美国纽约曼哈顿举办产品发布会,生成式AI成为重要主题之一。 微软表示,Copilot将于9月26日在Windows 11中推出;Microsoft 365 Copilot 将于11 月1日向企业客户全面推出;将OpenAI最新的文本生成图…

C# OpenCvSharp 图片模糊检测(拉普拉斯算子)

效果 项目 代码 using OpenCvSharp; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Windows.Forms.VisualStyl…

二叉树层序遍历及判断完全二叉树

个人主页:Lei宝啊 愿所有美好如期而遇 目录 二叉树层序遍历: 判断完全二叉树: 二叉树层序遍历: 层序遍历就是一层一层,从上到下遍历,上图遍历结果为:4 2 7 1 3 6 9 思路: 通过队列来实现层序…

vue下载Excel文件

前端vue实现导出Excel文件 用到的是 上代码 var wb XLSX.utils.table_to_book(document.querySelector(#my-table));//关联dom节点 这个是表格绑定的id名称var wbout XLSX.write(wb, {bookType: xlsx,bookSST: true,type: array})try {FileSaver.saveAs(new Blob([wbout], {…

Win10专业版开启远程桌面

Win10专业版开启远程桌面 方法一: 一、按“Win R”键,然后输入“sysdm.cpl”并按下回车键打开系统属性。 二、选择“远程”选项卡,在远程桌面中勾选“允许远程连接到此计算机”就可以开启远程桌面; 方法二: 一、打…

Mybatis 映射器与XML配置职责分离

之前我们介绍了使用XML配置方式完成对数据的增删改查操作,使用此方式在实际调用时需要使用【命名空间.标签编号】的方式执行,此方式在编写SQL语句时很方便,而在执行SQL语句环节就显得不太优雅;另外我们也介绍了使用映射器完成对数…

杂记 | 使用gitlab-runner将python项目以docker镜像方式流水线部署到服务器(解决部署缓慢和时区不对的问题)

文章目录 01 需求背景1.1 需求1.2 步骤 02 编写BaseDockerfile2.1 编写2.2 说明2.3 执行 03 编写Dockerfile04 编写.gitlab-ci.yml05 项目结构 01 需求背景 1.1 需求 我有一个python项目,该项目可能是一个服务器监控程序,也可能是一个后端程序&#xf…

ChatGLM GPT原理介绍

图解GPT 除了BERT以外,另一个预训练模型GPT也给NLP领域带来了不少轰动,本节也对GPT做一个详细的讲解。 OpenAI提出的GPT-2模型(https://openai.com/blog/better-language-models/) 能够写出连贯并且高质量的文章,比之前语言模型效果好很多。GPT-2是基于Transformer搭建的,相…

使用 PyTorch 的计算机视觉简介 (1/6)

一、说明 Computer Vision(CV)是一个研究计算机如何从数字图像和/或视频中获得一定程度的理解的领域。理解这个定义具有相当广泛的含义 - 它可以从能够区分图片上的猫和狗,到更复杂的任务,例如用自然语言描述图像。 二、CV常见的问…

Python类练习

文章目录 题目要求步骤 题目要求 1)创建一个 Kid 类,包含姓名,性别,年龄属性和 play 方法 2) 创建一个 Stu 类,继承 Kid 类,同时包含成绩属性,获取成绩方法,努力学习方法,play方法&…

Java笔记:看清类加载过程

1 类加载的过程 1.1 加载 “加载”是“类加载”(Class Loading)过程的第一步。这个加载过程主要就是靠类器实现的,包括用户自定义类加载器。 加载的过程 在加载的过程中,JVM主要做3件事情 1)通过一个类的全限定名来获取定义此类的二进制字节…