2.3.2 两顶帽子的设计方式
前面谈到,要保持软件设计质量不退化,必须在每次需求变更的时候,对原有的程序结构进行适当的调整,那么应当怎样进行调整呢?我们还是回到前面电商网站付款功能的案例,看看每次需求变更应当怎样设计。
在交付第一个版本后,很快第一次需求变更就到来了。第一次需求变更要求增加商品折扣功能,该功能分为以下几种类型:
1)限时折扣;
2)限量折扣;
3)对某类商品打折;
4)对某个商品打折;
5)不打折。
以往我们拿到这个需求,可能很不冷静地开始改代码,修改成如图2-7所示的设计。
这里增加的一段if语句,并不是一种好的变更方式。如果每次变更都是这样加代码,那么软件必然会退化,进入难以维护的状态。那么这种变更为什么就不好呢?因为它违反了“开放-封闭原则”。
开放-封闭原则(OCP)包括开放原则与封闭原则两部分。
开放原则:我们开发的软件系统,对于功能扩展是开放的(Open for Extension),即当系统需求发生变更时,我们可以对软件功能进行扩展,使其满足用户的新需求。
封闭原则:对软件代码的修改应当是封闭的(Close for Modification),即在修改软件的同时,不要影响到系统原有的功能,所以应当在不修改原有代码的基础上实现新的功能。也就是说,在增加新功能的时候,新代码与老代码应当隔离,不能在同一个类、同一个方法中。
前面的设计,在实现新功能的同时,新代码与老代码在同一个类、同一个方法中了,就违反了封闭原则。然而,怎样才能同时满足开放原则和封闭原则呢?在原有的代码上,你发现你什么都做不了,难道“开放-封闭原则”错了吗?
图2-7 一段糟糕的需求变更设计
当我们实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式要求在每次变更时将变更分为两个步骤。
两顶帽子:
1)在不添加新功能的前提下,重构代码,调整原有程序结构,以适应新功能;
2)实现新的功能。
以上面的案例为例,为了实现新的功能,我们在原有代码的基础上,在不添加新功能的前提下调整原有程序结构,因此我们抽取出了Strategy这样一个接口和“不打折”这个实现类。这时,原有程序变了吗?没有。但是程序结构变了,增加了一个接口,我们称之为“可扩展点”。在这个可扩展点的基础上再实现各种折扣,既能满足“开放-封闭原则”,保证程序质量,又能够满足新的需求。当日后发生新的变更时,需要实现什么类型的折扣,就修改对应的实现类,添加新的折扣类型就增加新的实现类,维护成本得以降低。“两顶帽子”的变更设计代码如图2-8所示。
图2-8 “两顶帽子”的变更设计
“两顶帽子”的设计方式意义重大。过去,我们在每次软件设计时总是担心日后的变更,就设计了很多所谓的“灵活设计”。然而,每一种“灵活设计”只能应对一种需求变更,而我们又不是先知,不知道日后会发生什么样的变更。最后的结果往往是,我们期望的变更并没有发生,所做的设计都变成了摆设,它不起任何作用,还增加了程序复杂度。然而,我们没有期望的变更发生了,原有的程序依然不能解决新的需求,程序又被打回了原形。这样不能真正解决未来变更的问题的设计被称为“过度设计”。
有了“两顶帽子”,我们不需要再担心过度设计了。正确的思路应当是“活在今天的格子里做今天的事”,也就是为当前的需求进行设计,使其刚刚满足当前的需求。所谓的“高质量的软件设计”就是要掌握一个平衡,一方面要满足当前的需求,另一方面要让设计刚刚满足需求,从而使设计最简化,代码最少。这样做,不仅软件设计质量提高了,设计难度也得到了大幅度降低。
简而言之,保持软件设计不退化的关键在于每次需求变更的设计。只有保证每次需求变更时做出正确的设计,才能保证软件能不断维护下去,而这种正确的设计方式就是“两顶帽子”。但是,在实践“两顶帽子”的过程中,大家觉得比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚,但到了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。那么,有没有一种方法,让我们在面临数十次变更时依然能够找到正确的设计方向呢?有,那就是“领域驱动设计”。