没有合适的资源?快使用搜索试试~ 我知道了~
领域驱动设计(DDD)实践
需积分: 5 0 下载量 91 浏览量
2023-07-29
19:07:08
上传
评论
收藏 1.3MB PDF 举报
温馨提示
试读
27页
领域驱动设计(DDD)实践
资源推荐
资源详情
资源评论
领域驱动设计(DDD)编码实践
2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域
驱动设计),简称Evans DDD。领域驱动设计分为两个阶段:
以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现
领域概念,然后将这些概念设计成一个领域模型;
由领域模型驱动软件设计,用代码来实现该领域模型;
由此可见,领域驱动设计的核心是建立正确的领域模型。
为什么建立一个领域模型是重要的
领域驱动设计告诉我们,在通过软件实现一个业务系统时,建立一个领域模型是非常重要和必要的,因
为领域模型具有以下特点:
1. 领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有
边界的,只反应了我们在领域内所关注的部分;
2. 领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货
物,书本,应聘记录,地址,等;还能反映领域中的一些过程概念,如资金转账,等;
3. 领域模型确保了我们的软件的业务逻辑都在一个模型中,都在一个地方;这样对提高软件的可维护
性,业务可理解性以及可重用性方面都有很好的帮助;
4. 领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造;
5. 领域模型贯穿软件分析、设计,以及开发的整个过程;领域专家、设计人员、开发人员通过领域模
型进行交流,彼此共享知识与信息;因为大家面向的都是同一个模型,所以可以防止需求走样,可
以让软件设计开发人员做出来的软件真正满足需求;
6. 要建立正确的领域模型并不简单,需要领域专家、设计、开发人员积极沟通共同努力,然后才能使
大家对领域的认识不断深入,从而不断细化和完善领域模型;
7. 为了让领域模型看的见,我们需要用一些方法来表示它;图是表达领域模型最常用的方式,但不是
唯一的表达方式,代码或文字描述也能表达领域模型;
8. 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分;设计足够精良且符合业务需
求的领域模型能够更快速的响应需求变化;
领域通用语言(UBIQUITOUS LANGUAGE)
我们认识到由软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的,但是,那种方法通常
会由于一些基础交流的障碍而存在难点。开发人员满脑子都是类、方法、算法、模式、架构,等等,总
是想将实际生活中的概念和程序工件进行对应。他们希望看到要建立哪些对象类,要如何对对象类之间
的关系建模。他们会习惯按照封装、继承、多态等面向对象编程中的概念去思考,会随时随地这样交
谈,这对他们来说这太正常不过了,开发人员就是开发人员。但是领域专家通常对这一无所知,他们对
软件类库、框架、持久化甚至数据库没有什么概念。他们只了解他们特有的领域专业技能。比如,在空
中交通监控样例中,领域专家知道飞机、路线、海拔、经度、纬度,知道飞机偏离了正常路线,知道飞
机的发射。他们用他们自己的术语讨论这些事情,有时这对于外行来说很难直接理解。如果一个人说了
什么事情,其他的人不能理解,或者更糟的是错误理解成其他事情,又有什么机会来保证项目成功呢?
在交流的过程中,需要做翻译才能让其他的人理解这些概念。开发人员可能会努力使用外行人的语言来
解析一些设计模式,但这并一定都能成功奏效。领域专家也可能会创建一种新的行话以努力表达他们的
这些想法。在这个痛苦的交流过程中,这种类型的翻译并不能对知识的构建过程产生帮助。
领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很
适合作为这种通用语言的构造基础。使用模型作为语言的核心骨架,要求团队在进行所有的交流是都使
用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。这儿需
要确保团队使用的语言在所有的交流形式中看上去都是一致的,这种语言被称为“通用语言(Ubiquitous
Language)”。通用语言应该在建模过程中广泛尝试以推动软件专家和领域专家之间的沟通,从而发现
要在模型中使用的主要的领域概念。
将领域模型转换为代码实现的最佳实践
拥有一个看上去正确的模型不代表模型能被直接转换成代码,也或者它的实现可能会违背某些我们所不
建议的软件设计原则。我们该如何实现从模型到代码的转换,并让代码具有可扩展性、可维护性,高性
能等指标呢?另外,如实反映领域的模型可能会导致对象持久化的一系列问题,或者导致不可接受的性
能问题。那么我们应该怎么做呢?
我们应该紧密关联领域建模和设计,紧密将领域模型和软件编码实现捆绑在一起,模型在构建时就考虑
到软件和设计。开发人员会被加入到建模的过程中来。主要的想法是选择一个能够恰当在软件中表现的
模型,这样设计过程会很顺畅并且基于模型。代码和其下的模型紧密关联会让代码更有意义并与模型更
相关。有了开发人员的参与就会有反馈。它能保证模型被实现成软件。如果其中某处有错误,会在早期
就被标识出来,问题也会容易修正。写代码的人会很好地了解模型,会感觉自己有责任保持它的完整
性。他们会意识到对代码的一个变更其实就隐含着对模型的变更,另外,如果哪里的代码不能表现原始
模型的话,他们会重构代码。如果分析人员从实现过程中分离出去,他会不再关心开发过程中引入的局
限性。最终结果是模型不再实用。任何技术人员想对模型做出贡献必须花费一些时间来接触代码,无论
他在项目中担负的是什么主要角色。任何一个负责修改代码的人都必须学会用代码表现模型。每位开发
人员都必须参与到一定级别的领域讨论中并和领域专家联络
领域建模时思考问题的角度
“用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心设计领域模型”。 《老
子》书中有个观点:有之以为利,无之以为用。在这里,有之利,即建立领域模型;无之用,即包容用
户需求。举些例子,一个杯子要装满一杯水,我们在制作杯子时,制作的是空杯子,即要把水倒出来,
之后才能装下水;再比如,一座房子要住人,我们在建造房子时,建造的房子是空的,唯有空的才能容
纳人的居住。因此,建立领域模型时也要将用户置于模型之外,这样才能包容用户的需求。
所以,我的理解是:
1. 我们设计领域模型时不能以用户为中心作为出发点去思考问题,不能老是想着用户会对系统做什
么;而应该从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联
及其变化规律作为出发点去思考问题。
2. 领域模型是排除了人之外的客观世界模型,但是领域模型包含人所扮演的参与者角色,但是一般情
况下不要让参与者角色在领域模型中占据主要位置,如果以人所扮演的参与者角色在领域模型中占
据主要位置,那么各个系统的领域模型将变得没有差别,因为软件系统就是一个人机交互的系统,
都是以人为主的活动记录或跟踪;比如:论坛中如果以人为主导,那么领域模型就是:人发帖,人
回帖,人结贴,等等;DDD的例子中,如果是以人为中心的话,就变成了:托运人托运货物,收货
人收货物,付款人付款,等等;因此,当我们谈及领域模型时,已经默认把人的因素排除开了,因
为领域只有对人来说才有意义,人是在领域范围之外的,如果人也划入领域,领域模型将很难保持
客观性。领域模型是与谁用和怎样用是无关的客观模型。归纳起来说就是,领域建模是建立虚拟模
型让我们现实的人使用,而不是建立虚拟空间,去模仿现实。
以Eric Evans(DDD之父)在他的书中的一个货物运输系统为例子简单说明一下。在经过一些用户需求
讨论之后,在用户需求相对明朗之后,Eric这样描述领域模型:
1. 一个Cargo(货物)涉及多个Customer(客户,如托运人、收货人、付款人),每个Customer承
担不同的角色;
2. Cargo的运送目标已指定,即Cargo有一个运送目标;
3. 由一系列满足Specification(规格)的Carrier Movement(运输动作)来完成运输目标;
从上面的描述我们可以看出,他完全没有从用户的角度去描述领域模型,而是以领域内的相关事物为出
发点,考虑这些事物的本质关联及其变化规律的。上述这段描述完全以货物为中心,把客户看成是货物
在某个场景中可能会涉及到的关联角色,如货物会涉及到托运人、收货人、付款人;货物有一个确定的
目标,货物会经过一系列列的运输动作到达目的地;其实,我觉得以用户为中心来思考领域模型的思维
只是停留在需求的表面,而没有挖掘出真正的需求的本质;我们在做领域建模时需要努力挖掘用户需求
的本质,这样才能真正实现用户需求;
关于用户、参与者这两个概念的区分,可以看一下下面的例子:
试想两个人共同玩足球游戏,操作者(用户)是驱动者,它驱使足球比赛领域中,各个“人”(参与者)
的活动。这里立下一个假设,假设操作者A操作某一队员a,而队员a拥有着某人B的信息,那么有以下说
法,a是B的镜像,a是领域参与者,A是驱动者。
Martin Fowler在《企业应用架构模式》一书中写道:
I found this(business logic) a curious term because there are few things that are less logical
than business logic.
初略翻译过来可以理解为:业务逻辑是很没有逻辑的逻辑。
的确,很多时候软件的业务逻辑是无法通过推理而得到的,有时甚至是被臆想出来的。这样的结果使得
原本已经很复杂的业务变得更加复杂而难以理解。而在具体编码实现时,除了应付业务上的复杂性,技
术上的复杂性也不能忽略,比如我们要讲究技术上的分层,要遵循软件开发的基本原则,又比如要考虑
到性能和安全等等。
在很多项目中,技术复杂度与业务复杂度相互交错纠缠不清,这种火上浇油的做法成为不少软件项目无
法继续往下演进的原因。然而,在合理的设计下,技术和业务是可以分离开来或者至少它们之间的耦合
度是可以降低的。在不同的软件建模方法中,领域驱动设计(Domain Driven Design,DDD)尝试通过其自
有的原则与套路来解决软件的复杂性问题,它将研发者的目光首先聚焦在业务本身上,使技术架构和代
码实现成为软件建模过程中的“副产品”。
DDD总览
DDD分为战略设计和战术设计。在战略设计中,我们讲求的是子域和限界上下文(Bounded Context,BC)
的划分,以及各个限界上下文之间的上下游关系。当前如此火热的“在微服务中使用DDD”这个命题,究
其最初的逻辑无外乎是“DDD中的限界上下文可以用于指导微服务中的服务划分”。事实上,限界上下文
依然是软件模块化的一种体现,与我们一直以来追求的模块化原则的驱动力是相同的,即通过一定的手
段使软件系统在人的大脑中更加有条理地呈现,让作为“目的”的人能够更简单地了解进而掌控软件系
统。
如果说战略设计更偏向于软件架构,那么战术设计便更偏向于编码实现。DDD战术设计的目的是使得业
务能够从技术中分离并突显出来,让代码直接表达业务的本身,其中包含了聚合根、应用服务、资源
库、工厂等概念。虽然DDD不一定通过面向对象(OO)来实现,但是通常情况下在实践DDD时我们采用的
是OO编程范式,行业中甚至有种说法是“DDD是OO进阶”,意思是面向对象中的基本原则(比如SOLID)在
DDD中依然成立。本文主要讲解DDD的战术设计。
本文以一个简单的电商订单系统为例,通过以下方式可以获取源代码:
实现业务的3种常见方式
在讲解DDD之前,让我们先来看一下实现业务代码的几种常见方式,在示例项目中有个“修改Order中
Product的数量”的业务需求如下:
可以修改Order中Product的数量,但前提是Order处于未支付状态,Product数量变更后Order的
总价(totalPrice)应该随之更新。
1. 基于“Service + 贫血模型”的实现
这种方式当前被很多软件项目所采用,主要的特点是:存在一个贫血的“领域对象”,业务逻辑通过一个
Service类实现,然后通过setter方法更新领域对象,最后通过DAO(多数情况下可能使用诸如Hibernate
之类的ORM框架)保存到数据库中。实现一个OrderService类如下:
这种方式依然是一种面向过程的编程范式,违背了最基本的OO原则。另外的问题在于职责划分模糊不
清,使本应该内聚在 Order 中的业务逻辑泄露到了其他地方( OrderService ),导致 Order 成为一个只
是充当数据容器的贫血模型(Anemic Model),而非真正意义上的领域模型。在项目持续演进的过程中,
这些业务逻辑会分散在不同的Service类中,最终的结果是代码变得越来越难以理解进而逐渐丧失扩展能
力。
2. 基于事务脚本的实现
在上一种实现方式中,我们会发现领域对象( Order )存在的唯一目的其实是为了让ORM这样的工具能够
一次性地持久化,在不使用ORM的情况下,领域对象甚至都没有必要存在。于是,此时的代码实现便退
化成了事务脚本(Transaction Script),也就是直接将Service类中计算出的结果直接保存到数据库(或者有
时都没有Service类,直接通过SQL实现业务逻辑):
git clone https://github.com/e-commerce-sample/order-backend`
`git checkout a443dace
@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);
}
可以看到,DAO中多出了很多方法,此时的DAO不再只是对持久化的封装,而是也会包含业务逻辑。另
外, DAO.updateTotalPrice(id) 方法的实现中将直接调用SQL来实现Order总价的更新。与
“Service+贫血模型”方式相似,事务脚本也存在业务逻辑分散的问题。
事实上,事务脚本并不是一种全然的反模式,在系统足够简单的情况下完全可以采用。但是:一方面“简
单”这个度其实并不容易把握;另一方面软件系统通常会在不断的演进中加入更多的功能,使得原本简单
的代码逐渐变得复杂。因此,事务脚本在实际的应用中使用得并不多。
3. 基于领域对象的实现
在这种方式中,核心的业务逻辑被内聚在行为饱满的领域对象( Order )中,实现 Order 类如下:
然后在Controller或者Service中,调用 Order.changeProductCount() :
可以看到,所有业务(“检查Order状态”、“修改Product数量”以及“更新Order总价”)都被包含在了
Order 对象中,这些正是 Order 应该具有的职责。(不过示例代码中有个地方明显违背了内聚性原则,
下文会讲到,作为悬念读者可以先行尝试着找一找)
事实上,这种方式与本文要讲的DDD战术模式已经很相近了,只是DDD抽象出了更多的概念与原则。
基于业务的分包
@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);
}
public void changeProductCount(ProductId productId, int count) {
if (this.status == PAID) {
throw new OrderCannotBeModifiedException(this.id);
}
OrderItem orderItem = retrieveItem(productId);
orderItem.updateCount(count);
}
@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);
}
剩余26页未读,继续阅读
资源评论
潇凝子潇
- 粉丝: 106
- 资源: 57
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功