人人都可以领域驱动设计(一)

774 阅读8分钟

最近几年,领域驱动设计(Domain-Driven Design,DDD)这个术语越来越多地出现在软件工程师的视野里。对DDD不熟悉的人可能会觉得它是软件领域里的一个新的概念,但是实际上,Eric Evans在十几年前就已经提出了这个概念。这个“古老”的概念在之所以能够重焕新生,很大程度上是因为遇上了“微服务”这个浪潮。如果把DDD里面的理念拿去和微服务架构做对比,你会发现它们有着高度的相似性——DDD里的限界上下文不正是微服务架构中的微服务吗?于是,各大公司纷纷运用DDD的方法论来构建自己的产品架构。有些团队成功地将DDD结合到了产品架构中,产生了许多优秀的实践;也有些团队反映DDD太过复杂,很难落地。那么DDD究竟是个什么样的理念?为什么大家都争先恐后地使用它,彷佛不加点DDD都不好意思说自己是微服务架构?然而又为什么那么多团队说DDD难以落地?本文将会结合简单的代码实现来谈谈笔者对DDD的理解。

什么是领域驱动设计?

软件的核心是其为用户解决领域相关的问题的能力。

软件就是为了解决某一领域相关问题而存在的,比如一个普通的计算器软件,就是为了满足我们进行简单的加减乘除运算而存在。对于计算器这种小而简单的软件,一个普通的软件工程师可能花上几天就能过设计开发出来,而且基本不会出现Bug。但是对于一些大型而且复杂的系统,一个团队都得花上很长的时间去设计整个架构,然后经过n轮迭代才能开发出可用的版本,而且后面还有各种Bug要去处理。比如证券交易系统,里面就包括了用户系统、账户系统、订单系统、撮合系统等一系列的子系统,而且其中的调用关系和业务都非常复杂。像这样一个庞大的系统,怎样才能把它设计好呢?这正是DDD要回答的问题。

领域驱动设计(DDD)是一种软件开发的方法论,旨在帮助我们设计出高质量的软件模型

在软件领域,解决复杂问题的方法不外乎是“分治”和“抽象”,DDD也是基于这两个理念建立起一套方法论。其中将一个系统划分成多个限界上下文,限界上下文中划分出多个子域,这是分治;然后在分别对各个子域进行领域建模,这是抽象。当你在设计一个业务复杂的系统却无从下手时,尝试一下DDD,说不定困难就会迎刃而解了。DDD中最核心的理念就是领域建模,可以说它提供的各种方法都是为了帮助我们设计出更能准确传达业务规则的领域模型。一个好的领域模型可以让一个系统更加健壮,可以让一个框架易用性更加好,可以让一段代码更加好维护。那么,什么样的模型才是好的领域模型?下面,我们通过一个例子来简单说明下。

什么是领域模型?

领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同事包含了数据和行为,并且表达了准确的业务含义。

日期和时间领域模型

如何设计一个日期和时间API?

首先需要对日期和时间的概念进行建模,从直觉上,我们可以将日期和时间抽象成一个对象Date。另外,时间和日期经常都需要进行格式化输出,因此我们还需要一个用于表示时间格式的对象DateFormat。为了更好地表示年月周日等概念,再抽象出一个表示日历的Calendar对象,以及表示时区的TimeZone对象。

JDK 1.1的日期时间模型
JDK 1.1的日期时间模型

相信到这里大家都已经知道,这正是JDK 1.1版本的日期时间API,下面我们先回顾一下它的用法:

public class TestOldDate {
public static void main(String[] args) {
// 获取表示当前时刻的Date对象
Date date1 = new Date();
// 通过Calendar等到指定日期时间的Date对象,采用当前的系统时区
Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
calendar.set(2020, 2, 10, 0, 0, 0);
Date date2 = calendar.getTime();
// 进行时间比较
System.out.println("date1 is after date2: " + date1.after(date2));
// 进行时间的加减法,如获得昨天的这个时刻:
calendar.setTime(date1);
calendar.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH) - 1);
Date date3 = calendar.getTime();
// 对日期格式化输出
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("date3: " + df.format(date3));
}
}
/* Output:
date1 is after date2: false
date3: 2020-02-07 23:55:39
*/

如果你用习惯了JDK 1.1版本的日期时间API,可能会觉得上述例子中的用法也没有多大的问题。但是,仔细思考一下就会发现这其中的逻辑跟我们人类对时间的处理逻辑不太像,比如要对时间做加法,首先要将需要操作的Date对象设置到Calendar,然后对Calendar做加法,最后调用Calendar的接口得到结果。时间的加法难道不应该直接对Date对象加上一个时间段就行了吗?

相信很多小伙伴都会遇到这种情况,自己写出来的代码可读性不够好。这其中原因可能是对领域的理解不够深,设计出来的领域模型没能准确的表达业务逻辑(如JDK 1.1的日期时间模型),或者开发前根本就没有进行领域建模。这样容易导致采用了一种“机器思维”去进行开发,而不是按照我们平常思考问题的思维去开发。

JDK 1.8引入了全新的日期和时间API来解决老版本的API所存在的种种问题,下面,我们来看一下比之前更准确地表达日期和时间的领域模型:

JDK 1.8的日期时间模型
JDK 1.8的日期时间模型

JDK 1.8的日期和时间领域模型中的领域知识明显比老版本的领域模型要丰富很多,而且模型更加符合人类思考日期和时间的思维。下面,我们看下新的日期和时间API的用法:

public class TestNewDate {
public static void main(String[] args) {
// 获取表示当前时刻的LocalDateTime对象
LocalDateTime date1 = LocalDateTime.now(ZoneId.systemDefault());
// 获取指定时间的LocalDateTime对象
LocalDateTime date2 = LocalDateTime.of(
LocalDate.of(2020, 2, 10),
LocalTime.of(0, 0, 0));
// 进行是时间比较
System.out.println("date1 is after date2: " + date1.isAfter(date2));
// 进行时间的加减法,如获得昨天的这个时刻:
LocalDateTime date3 = date1.minus(Period.ofDays(1));
// 对时间进行格式化输出
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println("date3: " + df.format(date3));
}
}
/* Output:
date1 is after date2: false
date3: 2020-02-07 23:56:51
*/

实现同样的功能,JDK 1.8版本的日期和时间API明显更加简洁,而且代码的逻辑更加符合人的思维,可读性更好(比如使用.now()函数创建当前时刻的LocalDateTime对象,代码阅读起来就跟人类的自然语言一样)。由此可见,设计出一个高质量的领域模型对于软件系统是多么的重要。

在这个例子中,JDK并没有显式地使用DDD提供的战术建模手段对日期和时间API进行设计,但是从JDK 1.1到JDK 1.8版本中的变化,其中就蕴含着DDD最核心的内容:设计出更符合业务规则和人类思维的领域模型。从这个例子中我们也能看出,DDD并没有传说中的那么神秘,也未必一定要运用在复杂的系统,即使是一个简单的日期和时间API,其中也可以看到它的身影。

如果让你对JDK 1.1的日期和时间API进行优化,相信很多人都设计不出像JDK 1.8版本的这样优秀的API,不管在经验,还是在方法上我们都欠点火候。简单的API如此,更别说设计复杂的大型系统了。这时,我们需要一些具体的建模方法来指导设计。

DDD的建模方法

DDD主要提出了两种建模方法来帮助我们设计出高质量的领域模型:战略建模和战术建模。

战略建模根据领域知识对系统进行限界上下文和子域的划分,战术建模具体为每个限界上下文设计出领域模型。而这两者中又内涵很多知识点,光看下面的这张DDD的概念图,你可能会觉得DDD太过复杂了。

DDD中的概念
DDD中的概念

确实,DDD的学习曲线比较陡,特别是第一次看Eric Evans所著的《领域驱动设计——软件核心复杂性应对之道》时,会有种不知所云的感觉。再看Vaughn Vernon所著的《实现领域驱动设计》可能会好点了,但是里面提到的各种概念还是没能很清晰地理解。所谓“实践出真知”,只有通过不断地实践,才能学习到DDD的精髓,体会到它的魔力。下一篇文章,我们将开始通过实践一个简单的业务功能着手介绍DDD的各种理论知识。