learn-tech/专栏/DDD微服务落地实战/01DDD:杜绝软件退化的利器.md
2024-10-15 23:19:16 +08:00

141 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
01 DDD :杜绝软件退化的利器
2004 年,软件大师 Eric Evans 的不朽著作《领域驱动设计软件核心复杂性应对之道》面世从书名可以看出这是一本应对软件系统越来越复杂的方法论的图书。然而在当时中国的软件业才刚刚起步软件系统还没有那么复杂即使维护了几年软件退化了不好维护了推倒重新开发就好了。因此在过去的那么多年里真正运用领域驱动设计开发DDD的团队并不多。一套优秀的方法论因为现实阶段原因而一直不温不火。
不过,这些年随着中国软件业的快速发展,软件规模越来越大,生命周期也越来越长,推倒重新开发的风险越来越大。这时,软件团队急切需要在较低成本的状态下持续维护一个系统很多年。然而,事与愿违。随着时间的推移,程序越来越乱,维护成本越来越高,软件退化成了无数软件团队的噩梦。
这时,微服务架构成了规模化软件的解决之道。不过,微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能发挥出微服务的优势,甚至可能令问题更糟糕。
因此,微服务的设计,微服务的拆分都需要领域驱动设计的指导。那么,领域驱动为什么能解决软件规模化的问题呢? 我们先从问题的根源谈起,即软件退化。
软件退化的根源
最近 10 年的互联网发展,从电子商务到移动互联,再到“互联网+”与传统行业的互联网转型,是一个非常痛苦的转型过程。而近几年的人工智能与 5G 技术的发展,又会带动整个产业向着大数据与物联网发展,另一轮的技术转型已经拉开帷幕。
那么,在这个过程中,一方面会给我们带来诸多的挑战,另一方面又会给我们带来无尽的机会,它会带来更多的新兴市场、新兴产业与全新业务,给我们带来全新的发展机遇。
然而,在面对全新业务、全新增长点的时候,我们能不能把握住这样的机遇呢?我们期望能把握住,但每次回到现实,回到正在维护的系统时,却令人沮丧。我们的软件总是经历着这样的轮回,软件设计质量最高的时候是第一次设计的那个版本,当第一个版本设计上线以后就开始各种需求变更,这常常又会打乱原有的设计。
因此,需求变更一次,软件就修改一次,软件修改一次,质量就下降一次。不论第一次的设计质量有多高,软件经历不了几次变更,就进入一种低质量、难以维护的状态。进而,团队就不得不在这样的状态下,以高成本的方式不断地维护下去,维护很多年。
这时候,维护好原有的业务都非常不易,又如何再去期望未来更多的全新业务呢?比如,这是一段电商网站支付功能的设计,最初的版本设计质量还是不错的:
当第一个版本上线以后,很快就迎来了第一次变更,变更的需求是增加商品折扣功能,并且这个折扣功能还要分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣。当我们拿到这个需求时怎么做呢?很简单,增加一个 if 语句if 限时折扣就怎么怎么样if 限量折扣就怎么怎么样……代码开始膨胀了。
接着,第二次变更需要增加 VIP 会员,除了增加各种金卡、银卡的折扣,还要为会员发放各种福利,让会员享受各种特权。为了实现这些需求,我们又要在 payoff() 方法中加入更多的代码。
第三次变更增加的是支付方式,除了支付宝支付,还要增加微信支付、各种银行卡支付、各种支付平台支付,此时又要塞入一大堆代码。经过这三次变更,你可以想象现在的 payoff() 方法是什么样子了吧,变更是不是就可以结束了呢?其实不能,接着还要增加更多的秒杀、预订、闪购、众筹,以及各种返券。程序变得越来越乱而难以阅读,每次变更也变得越来越困难。
问题来了:为什么软件会退化,会随着变更而设计质量下降呢?在这个问题上,我们必须寻找到问题的根源,才能对症下药、解决问题。
要探寻软件退化的根源,先要从探寻软件的本质及其规律开始,软件的本质就是对真实世界的模拟,每个软件都能在真实世界中找到它的影子。因此,软件中业务逻辑正确与否的唯一标准就是是否与真实世界一致。如果一致,则软件是 OK 的;不一致,则用户会提 Bug、提新需求。
在这里发现了一个非常重要的线索,那就是,软件要做成什么样,既不由我们来决定,也不由用户来决定,而是由客观世界决定。用户为什么总在改需求,是因为他们也不确定客观世界的规则,只有遇到问题了他们才能想得起来。因此,对于我们来说,与其唯唯诺诺地按照用户的要求去做软件,不如主动地理解业务的基础上去分析软件,而后者会更有利于我们减少变更的成本。
那么,真实世界是怎样,我们就怎样开发软件,不就简单了吗?其实并非如此,因为真实世界是非常复杂的,要深刻理解真实世界中的这些业务逻辑是需要一个过程的。因此,我们最初只能认识真实世界中那些简单、清晰、易于理解的业务逻辑,把它们做到我们的软件里,即每个软件的第一个版本的需求总是那么清晰明了、易于设计。
然而,当我们把第一个版本的软件交付用户使用的时候,用户却会发现,还有很多不简单、不明了、不易于理解的业务逻辑没做到软件里。这在使用软件的过程中很不方便,和真实业务不一致,因此用户就会提 Bug、提新需求。
在我们不断地修复 Bug实现新需求的过程中软件的业务逻辑也会越来越接近真实世界使得我们的软件越来越专业让用户感觉越来越好用。但是在软件越来越接近真实世界的过程中业务逻辑就会变得越来越复杂软件规模也越来越庞大。
你一定有这样一个认识:简单软件有简单软件的设计,复杂软件有复杂软件的设计。
比如,现在的需求就是将用户订单按照“单价 × 数量”公式来计算应付金额,那么在一个 PaymentBus 类中增加一个 payoff() 方法即可,这样的设计没有问题。不过,如果现在的需求需要在付款的过程中计算各种折扣、各种优惠、各种返券,那么我们必然会做成一个复杂的程序结构。
但是,真实情况却不是这样的。真实情况是,起初我们拿到的需求是那个简单需求,然后在简单需求的基础上进行了设计开发。但随着软件的不断变更,软件业务逻辑变得越来越复杂,软件规模不断扩大,逐渐由一个简单软件转变成一个复杂软件。
这时,如果要保持软件设计质量不退化,就应当逐步调整软件的程序结构,逐渐由简单的程序结构转变为复杂的程序结构。如果我们总是这样做,就能始终保持软件的设计质量,不过非常遗憾的是,我们以往在维护软件的过程中却不是这样做的,而是不断地在原有简单软件的程序结构下,往 payoff() 方法中塞代码,这样做必然会造成软件的退化。
也就是说,软件退化的根源不是软件变更,软件变更只是一个诱因。如果每次软件变更时,适时地进行解耦,进行功能扩展,再实现新的功能,就能保持高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上不断地塞代码,软件就会退化。这就是软件发展的规律,软件退化的根源。
杜绝软件退化:两顶帽子
前面谈到,要保持软件设计质量不退化,必须在每次需求变更的时候,对原有的程序结构适当地进行调整。那么应当怎样进行调整呢?还是回到前面电商网站付款功能的那个案例,看看每次需求变更应当怎样设计。
在交付第一个版本的基础上,很快第一次需求变更就到来了。第一次需求变更的内容如下。
增加商品折扣功能,该功能分为以下几种类型:
限时折扣
限量折扣
对某类商品进行折扣
对某个商品进行折扣
不折扣
以往我们拿到这个需求,就很不冷静地开始改代码,修改成了如下一段代码:
这里增加了一段 if 语句,并不是一种好的变更方式。如果每次都这样变更,那么软件必然就会退化,进入难以维护的状态。这种变更为什么就不好呢?因为它违反了“开放-封闭原则”。
开放-封闭原则OCP 分为开放原则与封闭原则两部分。
开放原则我们开发的软件系统对于功能扩展是开放的Open for Extension即当系统需求发生变更时可以对软件功能进行扩展使其满足用户新的需求。
封闭原则对软件代码的修改应当是封闭的Close for Modification即在修改软件的同时不要影响到系统原有的功能所以应当在不修改原有代码的基础上实现新的功能。也就是说在增加新功能的时候新代码与老代码应当隔离不能在同一个类、同一个方法中。
前面的设计,在实现新功能的同时,新代码与老代码在同一个类、同一个方法中了,违反了“开放-封闭原则”。怎样才能既满足“开放-封闭原则”,又能够实现新功能呢?在原有的代码上你发现什么都做不了!难道“开放-封闭原则”错了吗?
问题的关键就在于,当我们在实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式就要求在每次变更时,将变更分为两个步骤。
两顶帽子:
在不添加新功能的前提下,重构代码,调整原有程序结构,以适应新功能;
实现新的功能。
按以上案例为例,为了实现新的功能,我们在原有代码的基础上,在不添加新功能的前提下调整原有程序结构,我们抽取出了 Strategy 这样一个接口和“不折扣”这个实现类。这时,原有程序变了吗?没有。但是程序结构却变了,增加了这样一个接口,称为“可扩展点”。在这个可扩展点的基础上再实现各种折扣,既能满足“开放-封闭原则”来保证程序质量,又能够满足新的需求。当日后发生新的变更时,什么类型的折扣就修改哪个实现类,添加新的折扣类型就增加新的实现类,维护成本得到降低。
“两顶帽子”的设计方式意义重大。过去,我们每次在设计软件时总是担心日后的变更,就很不冷静地设计了很多所谓的“灵活设计”。然而,每一种“灵活设计”只能应对一种需求变更,而我们又不是先知,不知道日后会发生什么样的变更。最后的结果就是,我们期望的变更并没有发生,所做的设计都变成了摆设,它既不起什么作用,还增加了程序复杂度;我们没有期望的变更发生了,原有的程序依然不能解决新的需求,程序又被打回了原形。因此,这样的设计不能真正解决未来变更的问题,被称为“过度设计”。
有了“两顶帽子”,我们不再需要焦虑,不再需要过度设计,正确的思路应当是“活在今天的格子里做今天的事儿”,也就是为当前的需求进行设计,使其刚刚满足当前的需求。所谓的“高质量的软件设计”就是要掌握一个平衡,一方面要满足当前的需求,另一方面要让设计刚刚满足需求,从而使设计最简化、代码最少。这样做,不仅软件设计质量提高了,设计难点也得到了大幅度降低。
简而言之,保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。这种正确的设计方式就是“两顶帽子”。
但是,在实践“两顶帽子”的过程中,比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚;但经历了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。
那么,有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是“领域驱动设计”。
保持软件质量:领域驱动
前面谈到,软件的本质就是对真实世界的模拟。因此,我们会有一种想法,能不能将软件设计与真实世界对应起来,真实世界是什么样子,那么软件世界就怎么设计。如果是这样的话,那么在每次需求变更时,将变更还原到真实世界中,看看真实世界是什么样子的,根据真实世界进行变更。这样,日后不论怎么变更,经过多少轮变更,都按照这样的方法进行设计,就不会迷失方向,设计质量就可以得到保证,这就是“领域驱动设计”的思想。
那么,如何将真实世界与软件世界对应起来呢?这样的对应就包括以下三个方面的内容:
真实世界有什么事物,软件世界就有什么对象;
真实世界中这些事物都有哪些行为,软件世界中这些对象就有哪些方法;
真实世界中这些事物间都有哪些关系,软件世界中这些对象间就有什么关联。
真实世界与软件世界的对应图
在领域驱动设计中,就将以上三个对应,先做成一个领域模型,然后通过这个领域模型指导程序设计;在每次需求变更时,先将需求还原到领域模型中分析,根据领域模型背后的真实世界进行变更,然后根据领域模型的变更指导软件的变更,设计质量就可以得到提高。
总结
总之,软件发展的规律就是逐步由简单软件向复杂软件转变。简单软件有简单软件的设计,复杂软件有复杂软件的设计。因此,当软件由简单软件向复杂软件转变时,就需要通过两顶帽子适时地对程序结构进行调整,再实现新需求,只有这样才能保证软件不退化。然而,在变更的时候,如何调整代码以适应新的需求呢?
DDD 给了我们思路:在每次变更的时候,先回到领域模型,基于业务进行领域模型的变更。然后,再基于领域模型的变更,指导程序的变更。这样,不论经历多少次需求变更,始终能够保持设计质量不退化。这样的设计,才能保障系统始终在低成本的状态下,可持续地不断维护下去。