作者范刚,航天信息总架构师,《大话重构》一书作者。 本文详细介绍了基于电商支付场景的领域驱动模型的实际应用。
2004年,软件大师Eric Evans的不朽著作《领域驱动设计:如何应对软件的核心复杂性》出版。 从书名可以看出,这是一本关于处理日益复杂的软件系统的方法论的书。 不过,当时中国的软件产业刚刚起步,软件系统还没有那么复杂。 即使维护了几年,软件也会退化并且变得难以维护。 把它拆掉再开发就可以了。 因此,过去很多年,真正使用领域驱动设计与开发(DDD)的团队并不多。 优秀的方法论由于实践阶段而一直不温不火。
然而,随着近年来我国软件产业的快速发展,软件规模越来越大,生命周期也越来越长。 拆除重建的成本和风险越来越大。 这时,软件团队渴望以较低的成本维护一个系统多年。 然而,相反的情况发生了。 随着时间的推移,程序越来越乱,维护成本越来越高,软件降级,成为无数软件团队的噩梦。
这时,微服务架构就成为了大规模软件的解决方案。 但微服务对设计提出了非常高的要求,强调“小、专、高内聚”。 否则,微服务的优势就无法发挥出来,甚至问题可能会更加严重。
因此,微服务的设计以及微服务的拆分需要领域驱动设计的指导。 那么,领域驱动为什么能够解决软件规模问题呢? 让我们从问题的根源开始,即软件退化。
软件退化的根本原因
互联网过去10年的发展,从电子商务到移动互联网,再到“互联网+”和传统行业的互联网改造,是一个非常痛苦的转型过程。 近年来人工智能和5G技术的发展将带动整个行业向大数据和物联网方向发展,又一轮技术变革已经开始。
所以,在这个过程中,一方面会给我们带来很多挑战,另一方面也会给我们带来无尽的机遇。 它将带来更多的新兴市场、新兴产业、新业态,也将给我们带来新的发展机遇。
然而,面对新业务、新增长点,我们能否抓住这样的机遇呢? 我们期望能够掌握它,但每次我们回到现实,回到正在维护的系统时,都会感到沮丧。 我们的软件总是要经历这样一个循环。 软件设计的最高质量是设计的第一个版本。 第一个版本设计完成后,各种需求变化就开始,这往往会破坏原来的设计。 设计。
因此,一旦需求发生变化,版本迭代一次,软件就会修改一次。 软件一旦修改,质量就会下降。 无论最初的设计质量有多高,仅经过几次修改,软件就会进入低质量、难以维护的状态。 而且,团队还要以高成本的方式持续维持这种状态很多年。
这个时候维持原有的业务就已经非常困难了,那么未来更多的新业务如何期待呢? 例如,这是一个电子商务网站的支付功能的设计。 初始版本的设计质量相当不错:
第一个版本上线后,很快就迎来了第一次变更,变更的需求是增加商品折扣功能,而这个折扣功能又分为限时折扣、限时折扣、某类商品折扣,以及一定的折扣。 商品折扣。 当我们收到这个需求后我们该怎么办? 很简单,加一个if语句,如果是限时折扣,如果是限时折扣,那就……代码开始膨胀了。
那么,第二个变化就是需要增加VIP会员。 除了增加各种金卡、银卡的折扣外,还要给会员提供各种福利,让会员享受各种优惠。 为了实现这些要求,我们需要在()方法中添加更多的代码。
第三个变化增加了付款方式。 除了支付宝支付外,还增加了微信支付、各种银行卡支付、各种支付平台支付。 这时候需要插入很多代码。 经过这三个改变,你可以想象()方法现在是什么样子了。 变更能否完成? 其实不,还会增加更多闪购、预购、闪购、众筹、各种优惠券。 程序变得越来越混乱,难以阅读和维护,每次更改都变得越来越困难。
问题出现了:为什么软件会随着变化而降级,设计质量也会下降? 在这个问题上,我们必须找到问题的根源,才能对症下药,解决问题。
要探究软件退化的根源,首先要从探究软件的本质和规律开始。 软件的本质是对现实世界的模拟,每一个软件都能在现实世界中找到它的影子。 因此,判断软件中的业务逻辑是否正确的唯一标准就是是否与现实世界一致。 如果一致,则软件正常;如果一致,则软件正常。 如果不一致,用户将报告错误和新要求。
这里发现了一个很重要的线索,那就是软件要变成什么样子,既不是由我们决定的,也不是由用户决定的,而是由客观世界决定的。 用户为什么总是改变需求,是因为他们不确定客观世界的规则,只有遇到问题才想到。 因此,对于我们来说,与其仅仅根据用户需求来制作软件,不如在充分了解业务的基础上对软件进行分析。 这将更有利于我们降低软件维护成本。
那么,我们开发软件的方式就是现实世界的样子,不是很简单吗? 事实上并非如此,因为现实世界非常复杂,深入理解现实世界中的这些业务逻辑需要一个过程。 因此,我们只能先认识现实世界中那些简单、清晰、易于理解的业务逻辑,并将其实现到我们的软件中。 就是每个软件的第一个版本的需求总是那么清晰,易于设计。
然而,当我们将第一个版本的软件交付给用户时,他们会发现,还有很多业务逻辑不简单、不清晰、难以理解,并没有包含在软件中。 这在使用软件的过程中非常不方便,并且与真实业务不符,因此用户会提出Bug和新的需求。
随着我们不断修复bug、实现新的需求,软件的业务逻辑将会越来越接近现实世界,让我们的软件更加专业,也更加方便用户使用。 然而,随着软件越来越接近现实世界,业务逻辑越来越复杂,软件规模也越来越大。
你必须有这样的认识:简单的软件有简单软件的设计,复杂的软件有复杂软件的设计。
比如现在的需求是按照“单价×数量”的公式计算用户订单的应付金额,那么在类中添加一个()方法就可以了。 这样的设计是没有问题的。 但是,如果我们现在需要计算支付过程中的各种折扣、各种优惠、各种返利,那么我们就不可避免地会创建一个复杂的程序结构。
然而,真实情况并非如此。 真实的情况是,我们一开始拿到的需求都是简单的需求,然后我们就根据简单的需求进行设计和开发。 然而,随着软件的不断变革,软件的业务逻辑越来越复杂,软件的规模不断扩大,逐渐从简单的软件转变为复杂的软件。
这时,如果想保持软件设计的质量不下降,就应该逐步调整软件的程序结构,逐渐从简单的程序结构转变为复杂的程序结构。 如果我们始终这样做,我们就能始终保持软件的设计质量。 但非常遗憾的是,我们在以往维护软件的过程中并没有这样做。 相反,我们继续改进原来简单软件的程序结构。 ()方法中塞满了代码,势必造成软件降级。
也就是说,软件退化的根本原因不是版本迭代和需求变化,版本迭代和需求变化只是诱因。 如果每次软件变更都及时进行解耦,扩展功能,实现新功能,就可以保持高质量的软件设计。 但如果每次软件变更时不调整程序结构,而是在原有的程序结构上不断添加代码,软件就会降级。 这是软件发展的规律,也是软件退化的根本原因。
防止软件降级:两顶帽子
前面提到,为了不降低软件设计的质量,每次需求发生变化时,都必须对原有的程序结构进行适当的调整。 那么应该如何调整呢? 我们回到之前电商网站支付功能的案例,看看每次需求变化时应该如何设计。
基于第一个版本的交付,第一个需求变更很快就到来了。 第一个需求变更的内容如下。
新增产品折扣功能,分为以下几种:
以前,当我们得到这个需求时,我们就开始烦躁地改代码,改成了下面的代码:
这里添加的if else语句并不是一个好的改变方式。 如果每次都这样修改,软件必然会降级,进入难以维护的状态。 为什么这个改变不好呢? 因为它违反了“开闭原则”。
开闭原则(OCP)分为开放原则和封闭原则两部分。
之前的设计中,在实现新功能时,新代码和旧代码在同一个类、同一个方法中,违反了“开闭原则”。 如何既满足“开闭原则”又实现新功能? 你发现原来的代码什么也做不了! “开闭原则”错了吗?
问题的关键在于,我们在实现新的需求时,应该采用“两顶帽子”的方式来设计,即要求每次变更都分为两步。
两顶帽子:
以上述案例为例,为了实现新的功能,我们在原有代码的基础上调整了原有的程序结构,没有增加新的功能。 我们提取了这样一个接口和“不打折”的实现类。 这时候原来的程序有变化吗? 不是。但是程序结构发生了变化,增加了这样一个接口,称为“扩展点”。 在此扩展点的基础上实施各种折扣,既可以满足“开放封闭原则”保证节目质量,又可以满足新的需求。 以后发生新的变化时,如果任何类型的折扣发生变化,都会修改实现类。 如果添加新的折扣类型,则会添加新的实现类。 维护成本将会降低。
“两顶帽子”的设计方式意义重大。 过去,我们每次设计软件时,总是担心未来的变化,所以没有冷静地设计了很多所谓的“灵活设计”。 然而,每一种“柔性设计”只能响应一种需求变化,而我们不是先知,不知道未来会发生什么变化。 最终的结果是我们预想的改变没有发生,我们所做的设计变成了装饰,没有起到任何作用,增加了程序的复杂度; 我们没想到的变化发生了,原来的程序仍然无法解决新的需求,程序又回到了原来的样子。 因此,这样的设计并不能真正解决未来变化的问题,被称为“过度设计”。
有了“两顶帽子”,我们就不再需要担心或过度设计。 正确的想法应该是“活在今天的格子里,做今天的事”,即针对当前的需求进行设计,使之能够满足当前的需求。 所谓“高质量的软件设计”就是要掌握一个平衡。 一方面要满足当前的需求,另一方面设计也要刚好满足需求,这样设计最简单,代码最少。 这样做不仅提高了软件设计的质量,而且设计难度也大大降低。
总之,让软件设计不恶化的关键在于每次需求变更的设计。 只有保证每次需求变化都做出正确的设计,才能使软件持续保持良性循环。 这种正确的设计方法就是“两顶帽子”。
然而,在练习“两顶帽子”的过程中,第一步是比较困难的。 在不增加新功能的情况下,很难重构代码、调整原有程序结构以适应新功能。 很多时候,在第一次、第二次、第三次变化的过程中,你仍然可以清楚地思考这些事情; 但第十次、第二十次、第三十次变化之后,你就不能再想这些事情了。 一旦变得清晰,设计就开始迷失方向。
那么,有没有办法让我们在第十次、第二十次、第三十次变更时仍然能够找到正确的设计呢? 是的,那就是“领域驱动设计”。
维护软件质量:领域驱动
前面说过,软件的本质是模拟现实世界。 因此,我们就会有一个想法,我们的软件设计能不能和现实世界对应起来呢? 现实世界是什么样子的? 那么软件世界应该如何设计。 如果是这样的话,那么每次需求发生变化时,将变化还原到现实世界中,看看现实世界是什么样子,并根据现实世界进行改变。 这样,无论以后怎么修改,或者修改多少轮,都按照这个方法进行设计,就不会迷失方向,设计质量也能得到保证。 这就是“领域驱动设计”的思想。
那么,我们如何对应现实世界和软件世界呢? 这种对应关系包括以下三个方面:

现实世界与软件世界的对应图
在领域驱动设计中,首先将上述三种对应关系做成领域模型,然后由这个领域模型指导程序设计; 每次需求发生变化时,首先将需求还原到领域模型中进行分析,并根据领域模型的背景对现实世界进行更改,然后根据领域模型的变化指导软件变更,提高设计质量可以改进。
结合电商支付DDD实际实践
现在,我们以某电商网站的支付功能为例,练习一下基于DDD的软件设计和变更流程。
使用 DDD 进行软件设计
开发者最初收到的用户付费功能需求描述如下:
以前,开发人员接到这个需求时,往往设计完成后就匆忙开始编码,设计的质量不高。
采用领域驱动的方法,在获得新的需求后,首先应该进行需求分析并设计领域模型。 根据以上业务场景,可以分析:
最后我们可以对订单进行“下单”、“支付”、“查看订单状态”等操作。 这产生了以下域模型图:
有了这样的领域模型,通过这个模型就可以进行如下的程序设计:
通过领域模型的引导,将“订单”分为订单和价值对象,“用户”分为用户和价值对象,“商品”分为商品和价值对象……然后,各个方法的实现以这个为基础。 。
产品折扣需求的变化
当电商网站的支付功能按照领域模型完成第一个版本的设计后,很快就迎来了第一个需求变化,就是增加折扣功能,并且折扣功能分为限时的折扣、有限折扣和某些折扣。 同类产品折扣、特定产品折扣、特定产品无折扣。 当我们遇到这样的需求时,我们应该如何设计呢? 显然,在()方法中插入if else语句是不行的。 这时,根据领域驱动设计的思想,应该将需求变更还原到领域模型中进行分析,然后根据领域模型背后的现实世界进行变更。
这是之前版本的领域模型。 现在我们要在这个模型的基础上增加折扣功能,而且还会分为限时折扣、限时折扣、特定商品折扣等不同类型。 这个时候我们应该如何分析设计呢?
首先,分析付款与折扣之间的关系。
付款和折扣之间有什么关系? 您可能认为折扣是在付款时应用的,因此应将折扣写入付款中。 这是正确的思考方式吗? 我们应该根据什么思路和原则进行设计? 这时候,另一个重量级的设计原则就应该出现了,那就是“单一职责原则”。
单一职责原则:软件系统中的各个元素只完成自己职责范围内的事情,其他的事情交给其他人来做。 我只是打电话给他们。
单一职责原则是软件设计中非常重要的原则,但如何正确理解它却成为一个非常关键的问题。 这句话中,准确理解的关键在于“责任”二字,即自己的责任范围是什么。 过去我们错误地理解这个“责任”就是做某件事,与这件事有关的一切都是它的责任。 因为这种错误的认识,带来了很多错误的设计,将折扣写入到支付功能中。 那么,对“责任”的正确理解是什么呢?
“责任是软件变革的理由”是著名软件大师Uncle Bob在他的《敏捷软件开发:原则、模式和实践》中所说的。 但这种表述过于简洁,很难深入理解其内涵。 我在这里仔细解释一下这句话。
首先想一下什么是高质量的代码? 你可能立刻想到“低耦合、高内聚”以及各种设计原则,但这些评价标准太“虚”了。 最直接、最实用的评价标准是,当用户提出需求变更时,为实现变更而修改软件的成本越低,软件的设计质量就越高。 当需求发生变化时,如何降低修改软件的成本? 如果为了实现这个需求,需要修改三个模块的代码,那么三个模块都需要进行测试,维护成本必然“高”。 那么我们怎样才能最小化它呢? 如果只需要修改一个模块就可以达到这一要求,维护成本会低很多。
那么,如何才能每次变更时只修改一个模块来实现新的需求呢? 这就需要我们在日常生活中不断地整理代码,将相同原因发生变化的代码放在一起,而将不同原因发生变化的代码分开,放到不同的模块、不同的类中。 这样,当因为这个原因需要修改代码时,需要修改的代码就都在这个模块和这个类中了。 修改范围缩小了,维护成本降低了,修改代码带来的风险自然就降低了,设计质量也提高了。 已经好转了
总之,单一责任原则要求我们在维护软件的过程中不断地组织代码,将相同软件变更原因的代码放在一起,将不同软件变更原因的代码分开。 根据这个设计原则,回到之前的案例,我们应该如何分析“支付”和“折扣”之间的关系呢? 只回答两个问题:
当这两个问题的答案是否定的时,就意味着“付款”和“折扣”是软件变更的两个不同原因。 那么,将它们放在同一个类、同一个方法中是否合适呢? 如果不合适,应该将“折扣”从“付款”中提取出来,放在一个单独的类中。
一样:
最后发现,不同类型的折扣也是软件变化不同的原因。 将它们放在同一个类和同一个方法中是否合适? 通过以上分析,我们做出如下设计:
本次设计中,将折扣功能与支付功能分离出来,做了一个接口,然后基于此设计了各类折扣实现类。 通过这样的设计,支付功能改变时折扣不会受到影响,折扣改变时支付也不会受到影响。 同样,“限时折扣”变更时,仅与“限时折扣”相关,而“限时折扣”变更时,仅与“限时折扣”相关,与“限时折扣”无关。其他折扣类型。 变更范围缩小,维护成本降低,设计质量提高。 这种设计就是“单一责任原则”的精髓。
然后,基于该版本的领域模型进行程序设计。 在设计的时候也可以加入一些设计模式,所以我们进行了如下的设计:
显然,设计中加入了“策略模式”的内容,将折扣函数做成了折扣策略接口以及各种折扣策略的实现类。 当折扣类型发生变化时,修改哪个折扣策略实现类; 当增加新的折扣类型时,又编写了一个折扣策略实现类,提高了设计质量。
VIP会员需求的变化
在第一次改变的基础上,第二次改变很快就来了。 这次是增加VIP会员。 业务需求如下。
新增VIP会员功能:
当我们遇到这样的需求时,我们应该如何设计呢? 同样,先回到领域模型,分析“用户”和“VIP会员”之间的关系,以及“支付”和“VIP会员”之间的关系。 分析的时候,还是回答这两个问题:
通过分析,我们发现“用户”和“VIP会员”是完全不同的两个东西。
通过上面的分析,我们做出了如下版本的领域模型:
有了这些领域模型的改变,我们就可以以此为基础来指导后续对程序代码的改变。
付款方式要求的变化
同样,第三个变化是增加更多的支付方式。 我们分析了领域模型中“支付”和“支付方式”之间的关系,发现它们也是软件变化不同的原因。 因此,我们果断做了这样的设计:
在设计和实现时,需要与各种第三方支付系统进行对接,即需要与外部系统进行对接。 为了尽量减少第三方外部系统的变化对我们的影响,果断在其中加入了“适配器模式”,设计如下:
通过添加适配器模式,订单支付时不再调用外部支付接口,而是调用“支付方式”接口,与外部系统解耦。 只要“支付方式”接口稳定,订单就会稳定。 例如:
无论以后进行什么样的改变,需要修改的代码范围都会减少,维护成本自然会降低,代码质量也会提高。