编辑推荐: |
本文介绍面向数据库与面向对象、数据库查询、面向对象查询、预约服务传统实现及预约服务领域驱动实现。希望对您的学习有所帮助。
本文来自于知乎 ,由火龙果软件Linda编辑、推荐。 |
|
领域驱动设计(Domain Driven Design,简称DDD)一经提出,就引起很多人的关注,至今已有10余载(2004年,Eric
Evans《领域驱动设计——软件核心复杂性应对之道》)。
主流的领域驱动设计里,概念众多,如实体、值对象、领域服务、资源库和聚合,六边形架构,CQRS,Event
Source等。
众多的概念和理论,也让目前的领域驱动设计,实现复杂,难以落地。
一. 面向数据库与面向对象
领域驱动设计,实际上是一种面向对象的系统分析和设计方法。
传统开发,以数据库为中心。
数据集中存储在不同的table中,且为了满足关系模型理论,这些table的设计还需要符合各种范式和标准。
其业务逻辑通过一系列针对这些table的增删改查来实现。
领域驱动,则是面向对象。
数据分布在各个独立的对象里,各对象封装自己的内部数据和逻辑,对外只公开消息接口。
业务逻辑的实现表现为创建对象和向各个对象发送消息。
二. 数据库查询
不过要真正实现领域驱动设计,却非易事。
最困难的,在于不能摆脱对数据库的依赖,严格地说,是对数据库查询功能的依赖。
想象一下某ERP系统中存了几百万笔数据的采购单主档table,需要统计订单001中A材料目前的订购量。除了用SQL来sum(qty),不会有什么更好的方法了。
面向对象可以带来另一种思路,来看看下面这个例子:
一个会议室预约系统,预约会议室时,需要检查目前的预约记录,以防止时段冲突。
一般通过查询预约table :booking就可以实现,如下:
![](images/2022061041.png)
-- booking 中已有2笔当天A会议室的预约记录,以下SQL会查出第1笔数据,表示预约9/12
9:00~10:30的A会议室有冲突
--(某时段的结束时间如果大于要预约的开始时间,且其开始时间小于要预约的结束时间,那就表示时间区间有重叠)
SELECT *
FROM booking
WHERE booking_day = '20180912'
AND room_id = 'A'
AND end_time > '0900'
AND start_time < '1030' |
这是传统的面向数据库的做法,有一个地方(table)存储了所有的预约数据,有一个可以按条件快速找到数据的查询功能(SELECT)。
三. 面向对象查询
面向对象的世界里,不会有这样的集中存储。
不过我们可以把数据存储到一个对象里,把查询数据库,改为找到那个对象。
比如这个例子,设计一个名(ID)为20180912-A的【日会议室预约对象】,里面存储了2018/9/12这一天A会议室的所有预约时段,这样就可以在预约9/12的A会议室时,通过预约日期和会议室ID先找到这个对象,再检查时段冲突。
![](images/2022061042.png)
//【20180912-A】对象里有2笔预约数据, 下列代码会检查出9:00~10:30会和第一笔8:00~10:00的预约有冲突
foreach
(TimeRange bookinged in timeRangeList){
if(bookinged.EndTime.CompareTo(startTime)
> 0 && bookinged.StartTime.CompareTo(endTime)
< 0)
throw new BusException("ROOM_OCCUPIED");
} |
把查询数据库,改为设计一个合适的查询对象,这是没有关系数据库和SQL的面向对象做法。
四. 预约服务传统实现
我们再通过实际代码来比较一下两者的差异,
首先是传统数据库的实现:
![](images/2022061043.png)
先是一些基本验证(日期,时间等),
然后再查数据库检查时段冲突,
最后Insert 数据到Booking table,完成预约会议室的功能。
写法可能有很多种,例如分DAL层,或者使用LINQ,不过本质上都是面向数据库,都是增删改查的过程。
五. 预约服务领域驱动实现
再来看一下面向对象的领域驱动设计实现:
![](images/2022061044.png)
服务层的接口和上面一样。选择用传统方式还是领域模型实现服务层,对于外部是透明的。
领域驱动的服务层,只是一个通往领域模型的入口。
预约会议室就是new一个Booking对象。
再来看看领域模型Booking对象:
![](images/2022061045.png)
一个Booking对象就是一次预约,记录下自己的数据后,再把预约消息发送给RoomDayBooking对象。
接下来是RoomDayBooking对象:
![](images/2022061046.jpg)
这个对象负责检查某会议室某天的时段冲突,因其内部存有那一天那个会议室所有的预约时段数据timeRangeList。
至于timeRangeList,和Booking对象里的subject,startTime没什么区别,只是数据类型是复杂类型,如果愿意,也可以用一个string来实现:
![](images/2022061047.png)
六. 两种思维方式的差异
再来比较一下面向对象思维和传统方式的差异:
如果是传统思维,可能是找到RoomDayBooking对象后,取得其预约数据,一一比较,检查冲突,最后再把新的时段存回RoomDayBooking对象。
即:取得数据 -> 加工处理 -> 写回数据
而面向对象思维,则是发送消息给RoomDayBooking对象,由对象自行负责其内部逻辑和数据存储。
RoomDayBooking开放预约接口,而不直接把数据timeRangeList给外部使用。
下面的活动图表达了面向对象发送消息的过程:
发消息给Booking,其负责存储预约数据;
发消息给RoomDayBooking,其负责检查时段冲突。
![](images/2022061048.png)
对象相对于数据库,最大的优势,是既可以存数据,还可以有逻辑,这样就可以把数据封装在对象内部,实现高内聚和低耦合。
七. 读写分离
到这里,还有一个重要问题,那就是任意查询怎么办?
例如要查询9/12~9/15全部会议室的预约状况,或者查询某个用户的所有预约数据。
答案是读写分离。
业务功能分为读服务和写服务。读服务是单纯的查询,其过程不会修改数据。而写服务则是系统的核心,是那些含有增删改过程的服务。
领域驱动只适用,也只需要专注写服务。
写服务如果需要查询,则设计专门的查询对象。
有一个DomainContext上下文对象,负责按ID取出领域对象,以及持久化领域对象。
领域对象透过ORM最后存储到数据库,UI和其它读服务使用SQL进行查询。
八. 结论
查询对象的建立,让整个领域驱动设计真正摆脱了对数据库的依赖,让我们可以更专注在领域模型的建立上。
只需要通过ID取得领域对象,大大降低了领域驱动设计基础设施实现的难度。
数据库可以延后设计,可以更快测试和验证核心逻辑。
查询功能仍然使用SQL和数据库,让领域驱动设计更接地气,可以真正落地实现。
分析建模阶段不仅涉及数据,还会涉及逻辑,让后续的开发更确定,更顺利。
领域驱动模型,实践了面向对象分析理论,利用它的封装特性,完成了优雅和简洁的设计。
其不仅适用于复杂系统,而是能像传统方式一样,可以普遍应用在信息系统的分析,设计和开发过程中。
最后附上其它一些代碼:
附一:取消预约的传统实现
![](images/2022061049.png)
附二:取消预约的领域驱动设计
服务层
![](images/20220610410.png)
Booking对象
![](images/20220610411.png)
RoomDayBooking对象
![](images/20220610412.png)
共用的BookingCommand对象(DTO)
![](images/20220610413.png)
附三:测试画面和結果
booking 7:00~8:00
![](images/20220610414.png)
save to database
![](images/20220610415.png)
booking 7:30~9:00 (error occurred)
![](images/20220610416.png)
|