架构真意:企业级应用架构设计方法论与实践
上QQ阅读APP看书,第一时间看更新

2.3.4 领域驱动的变更设计

当电商网站的付款功能按照领域模型完成了第一个版本的设计后,很快就迎来了第一次需求变更,即增加折扣功能,并且该折扣功能分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣与不打折。当我们拿到这个需求时应当怎样设计呢?按照领域驱动设计的思想,应当将需求变更还原到领域模型中进行分析,进而根据领域模型背后的真实世界进行变更,如图2-11所示。

这是上一个版本的领域模型,现在要在这个模型的基础上增加折扣功能,并且还要分为限时折扣、限量折扣、某类商品的折扣等不同类型。这时,应当怎么分析设计呢?首先分析付款与折扣的关系。

你可能会认为折扣是在付款的过程中进行的折扣,因此就应当将折扣写到付款中。这样思考对吗?我们应当基于什么样的思想与原则来设计呢?这时,另外一个重量级的设计原则应该出场了,那就是“单一职责原则”。

图2-10 基于领域模型的设计

单一职责原则:

软件系统中的每个元素只完成自己职责范围内的事,而将其他的事交给别人去做,自己只是去调用。

单一职责原则是软件设计中一个非常重要的原则,但如何正确地理解它非常关键。在这句话中,准确理解的关键就在于“职责”二字,即自己职责的范围到底在哪里。以往,我们错误地理解了这个职责,认为就是做某事,与这个事情相关的所有事情都是它的职责。这个错误的理解带来了许多错误的设计,例如将折扣写到付款功能中。那么,怎样才是对“职责”正确的理解呢?

“一个职责就是软件变化的一个原因。”这是著名的软件大师Bob大叔在他的著作《敏捷软件开发:原则、模式与实践》中的表述,但这个表述过于精简,大家很难深刻地理解其中的内涵,因此也不能有效地提高我们的设计质量。这里我详细给大家解读一下这句话。

图2-11 折扣功能在领域模型中的分析设计

什么是高质量的代码?大家可能立即会想到低耦合、高内聚以及各种设计原则,但这些评价标准都太“虚”了。最直接、最落地的评价标准就是,当用户提出一个需求变更时,为了实现这个变更而修改软件的成本越低,那么软件的设计质量就越高。当出现了一个需求变更时,怎样才能让修改软件的成本降低呢?如果为了实现这个需求,需要修改3个模块的代码,修改完了这3个模块都需要测试,其维护成本必然“高”。因此为了降低维护成本,最现实的方案就是只修改1个模块,维护成本最低。

那么,怎样才能在每次变更的时候都只修改一个模块就实现新需求呢?这需要我们平时不断地整理代码,将那些因同一个原因而变更的代码都放在一起,而将因不同原因而变更的代码分开放,放在不同的模块、不同的类中。这样,当因为这个原因而需要修改代码时,需要修改的代码都在这个模块、这个类中,修改范围就缩小了,维护成本就降低了,自然设计质量就提高了。

总之,单一职责原则要求我们在维护软件的过程中不断地进行整理,将因同一个原因而变更的代码放在一起,将因不同原因而变更的代码分开放。按照这样的设计原则,回到前面那个案例中,应当怎样去分析“付款”与“折扣”之间的关系呢?只需要回答两个问题:

1)当“付款”发生变更时,“折扣”是不是一定要变?

2)当“折扣”发生变更时,“付款”是不是一定要变?

这两个问题的答案是否定的,就说明“付款”与“折扣”是软件变化的两个不同的原因,那么把它们放在一起,放在同一个类、同一个方法中,就不合适了,应当将“折扣”从“付款”中提取出来,单独放在一个类中。同样的道理,不同类型的折扣也是软件变化不同的原因,将它们放在同一个类、同一个方法中也是不合适的。通过以上分析,我们做出了如图2-12所示的设计。

图2-12 折扣功能的领域模型设计

在该设计中,我们将折扣功能从付款功能中独立出去,做了一个接口,然后以此为基础设计了各种类型的折扣实现类。当付款功能发生变更时不会影响折扣,而折扣发生变更的时候不会影响付款,变更的范围缩小了,维护成本就降低了,设计质量自然提高了。

同样,当限时折扣发生变更时只与限时折扣有关,限量折扣发生变更时也只与限量折扣有关,与其他折扣类型无关,变更的范围缩小了,维护成本就降低了,设计质量提高了。这就是“单一职责原则”的意义。

接着,我们在这个版本的领域模型的基础上进行程序设计,在设计时还可以加入一些设计模式的内容,如图2-13所示。

图2-13 折扣功能的程序设计

显然,在该设计中加入了“策略模式”的内容,将折扣功能做成了一个折扣策略接口与各种折扣策略的实现类。哪个折扣类型发生变更,就修改哪个折扣策略实现类,要增加新的类型的折扣,就再写一个折扣策略实现类,从而提高了设计质量。

在第一次变更的基础上,很快迎来了第二次变更,这次要增加VIP功能,业务需求如下。

增加VIP功能:

1)对不同类型的VIP(金卡会员、银卡会员)给予不同的折扣;

2)在支付时,为VIP发放福利(积分、返券等);

3)VIP可以享受某些特权。

我们拿到这样的需求又应当怎样设计呢?同样,先回到领域模型,分析“用户”与“VIP”的关系,“付款”与“VIP”的关系。在分析的时候,还是回答那两个问题:

1)“用户”发生变更时,“VIP”是否要变?

2)“VIP”发生变更时,“用户”是否要变?

通过分析发现,“用户”与“VIP”完全不同。“用户”要做的是用户的注册、变更、注销等操作,“VIP”要做的是会员折扣、会员福利与会员特权。而“付款”与“VIP”的关系是在付款的过程中去调用会员折扣、会员福利与会员特权。通过以上的分析,我们做出了如图2-14所示的版本的领域模型。

图2-14 VIP功能的领域模型设计

有了这些领域模型的变更,就可以以此为基础,指导后面的程序代码的变更了。

同样,第三次变更是增加更多的支付方式,我们在领域模型中分析“付款”与“支付方式”之间的关系,发现它们也是软件变化不同的原因,因此我们做出了如图2-15所示的设计。

在设计实现时,支付功能要与各个第三方的支付系统对接,也就是要与外部系统对接。为了将第三方的外部系统的变更对我们的影响最小化,我们在它们中间加入了“适配器模式”,设计如图2-16所示。

加入适配器模式后,订单Service在进行支付时调用的不再是外部的支付接口,而是我们自己的“支付方式”接口,与外部系统解耦。只要保证“支付方式”接口是稳定的,那么订单Service就是稳定的。当支付宝支付接口发生变更时,影响的只限于支付宝Adapter;当微信支付接口发生变更时,影响的只限于微信支付Adapter;当要增加一个新的支付方式时,只需要再写一个新的Adapter。日后不论哪种变更,要修改的代码范围缩小了,维护成本自然降低了,代码质量就提高了。

图2-15 不同支付方式的领域模型设计