条件逻辑有可能十分复杂,复杂的条件逻辑可能让复杂度快速上升,并有可能导致代码难以理解。因此,需要一些手段,来简化它们。
Decompose Conditional(分解条件表达式)
带有复杂逻辑的函数中,代码会告诉你发生的事情。但是十分冗长的条件表达,往往让后来者摸不着头脑。这种情况下,可以将它分解为独立的函数,并且为新函数命名(命名可以起到自注释的作用)。在原函数中调用新建函数,从而更清楚地表达自己的意图。举个例子
if(date.before(SUMMER_START) || date.after(SUMMER_END))
charge=quantity*_winterRate+_winterServiceCharge;
else
charge=quantity*_summerRate;
可以重构为
if(notSummer(date))
charge=winterCharge(quantity);
else
charge=summerCharge(quantity);
上面的例子就是将date.before(SUMMER_START) || date.after(SUMMER_END)
替代为notSummer(date)
,这种情况就一目了然。概括下这种重构方法的步骤:
- 将if段落提炼出来,构成一个独立的函数。
- 将then段落和else段落都提炼出来,各自构成一个独立的函数。
- 如果发现嵌套的条件逻辑,应该先看看是否可以使用以卫语句取代嵌套条件式。如果不行才开始分解其中的每个条件。
Consolidate Conditional Expression(合并条件表达式)
如果有一串条件检查,如果检查条件各不相同,但是最终导致的结果却一致。那么,就使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。举例:
double disabilityAmount() {
if (_seniority < 2) {
return 0;
}
if (_monthsDisabled > 12) {
return 0;
}
if (_isPartTime) {
return 0;
}
// compute the disability amount
// ...
}
由于都是返回0,因此可以将条件表达式合并
double disabilityAmount() {
if (isNotEligibleForDisability()) {
return 0;
}
// compute the disability amount
// ...
}
boolean isNotEligibleForDisability() {
return ((_seniority < 2) || (_monthsDisabled > 12) || (_isPartTime));
}
需要注意的是,要分清返回0是系统的行为是否在逻辑上一致,还是只是同时返回相同数值,在未来条件变动时还会有所改动。总结这种重构的做法为:
- 确定这些条件语句都没有副作用。
- 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个。
- 编译,测试。
- 对合并后的条件表达式实施Extract Method。
Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
你有时候会发现,条件语句不同分支执行了相同的片段。如果是这样,你就需要将这段代码搬移到条件表达式外面。举例子:
if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
重构后,可以将send()提取出来
if (isSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
send();
同样的,如果try catch中都有同样的代码,就可以放在final中去执行。总结这种重构的做法为:
- 鉴别出”执行方式不随条件变化而变化”的代码。
- 如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前。
- 如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后。
- 如果这些共通代码位于条件表达式中段,就需要观察共通代码之前或之后的代码是否改变了什么东西,如果的确有所改变,应该首先将共通代码向前或向后移动,移至条件表达式的起始处或尾端,再以前面所受的办法来处理。
- 如果共通代码不止一条语句,应该先使用Extract Method(提炼函数)将共通代码提炼到一个独立函数中,再以前面所说的办法来处理。
Remove Control Flag(移除控制标记)
在一系列的布尔表达式中,某个变量带有“控制标记”的作用,那么用break
或者return
语句取代控制标记。这样的控制标记带来的麻烦通常超过带来的便利。结构化编程告诉我们一个原则:每个子程序都只能有一个入口和一个出口,但是“单一出口”原则通常让我们在代码中加入许多的控制标记,这样就大大降低了表达式的可读性。使用break
或者return
可以让程序条理更加清晰。举例:
function checkSecurity(peoples) {
String found = '';
for(let i = 0; i < peoples.length; i++) {
if(!found) {
if(peoples[i] === 'Don' || peoples[i] === 'John') {
sendAlert();
found = peoples[i];
}
}
}
someLaterCode(found);
}
重构为
function checkSecurity(peoples) {
String found = foundMiscreant(peoples);
someLaterCode(found);
}
function foundMiscreant(peoples) {
for(let i = 0; i < peoples.length; i++) {
if(peoples[i] === 'Don' || peoples[i] === 'John') {
sendAlert();
return peoples[i];
}
}
return '';
}
这种重构的一般做法为:
使用extract method,整段逻辑提炼到一个独立函数中
- 找出跳出这段逻辑的控制标记值
- 找出对标记变量赋值的语句,代以恰当的return语句.
- 替换完成后,编译并测试。
Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
如果if else
分支都属于正常行为,那么建议正常使用if else
去表达。但是如果else分支中的情况极其罕见,那么建议采用卫语句进行表现。
function getPayAmount () {
let result
if (isDead) result = deadAmount()
else {
if (isSeparated) result = separatedAmount()
else {
if (isRetired) result = retiredAmount()
else result = normalPayAmount()
}
}
return result
}
可以重构为:
function getPayAmount () {
if (isDead) return deadAmount()
if (isSeparated) return separatedAmount()
if (isRetired) return retiredAmount()
return normalPayAmount()
}
这样代码条理更加清晰,可读性更高。这种重构方法的做法为:
- 对于每个检查,放进一个卫语句(卫语句要不就从函数返回,要不就抛出一个异常)。
- 编译测试。
Replace Conditional with Polymorphism(以多态取代条件表达式)
“多态”是面向对象的精髓所在,多态可以避免为不同类型代入不同行为。举个例子:
var makeSound = function(animal) {
if(animal instanceof Duck) {
console.log('嘎嘎嘎');
} else if (animal instanceof Chicken) {
console.log('咯咯咯');
}
}
var Duck = function(){}
var Chiken = function() {};
makeSound(new Chicken());
makeSound(new Duck());
重构后
var makeSound = function(animal) {
animal.sound();
}
var Duck = function(){}
Duck.prototype.sound = function() {
console.log('嘎嘎嘎')
}
var Chiken = function() {};
Chiken.prototype.sound = function() {
console.log('咯咯咯')
}
makeSound(new Chicken());
makeSound(new Duck());
多态背后的思想是将”做什么“和”谁去做以及怎样去做分开“。通常的做法是:
- 如果要处理的条件表达式是一个更大函数中的一部分。首先,对条件表达式进行分析,然后使用extract method将其提炼到一个独立函数去。
- 如果由必要,使用move method、将条件表达式放置到继承结构的顶端。
- 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新建函数中,并对其进行适当调整。
可能需要将超类中某些private字段声明为protected。 - 超类中删掉条件表达式内被复制了的分支。
- 将超类中容纳条件表达式的函数声明为抽象函数。
Introduce Null Object(引入Null对象)
这种重构方法在前端中不知如何使用,暂忽略。
Introduce Assertion(引入断言)
这种情况在程序中一般使用是非空判断,为了减少圈复杂度。我建议进行直接的空判断,而不是非空判断。
举例
if(response){
//do something
}
可以重构为
if(!response){
return;
}
//do something
本章重构方法比较简单,总的原则就是让代码清晰可懂,减少复杂度。这样的代码维护和交接都是一件非常愉悦的事情。