优化你的条件语句-《重构 改善既有代码的设计》第十章读书笔记

204 阅读13分钟

引言

在程序员刚刚从事编程这项工作的初期,因为经验的缺乏,编写的代码往往不甚理想。尽管如此,大部分时候程序还是能正常工作,项目最终上线。那么是不是说,对其结构“不甚清晰”的评价只是美学意义上的判断,只是对所谓丑陋代码的反感呢?毕竟编译器也不会在乎代码好不好看。但是,当我们需要修改系统时,就涉及了人,而人在乎这些。差劲的系统是很难修改的,因为很难找到修改点,难以了解做出的修改与现有代码如何协作实现我想要的行为。如果很难找到修改点,其他人就很有可能犯错,从而引入bug。

在平常的业务中,条件语句是我们最频繁使用的逻辑之一。程序的大部分威力都来自条件逻辑,但很不幸,程序的复杂度也大多来自条件逻辑。如何更好的让我们的逻辑语句能够清晰有条理的表达出来则显得尤为重要。

分解条件表达式

我们先看一段简单的代码:

假设我要计算购买某样商品的总价(总价=数量×单价),而这个商品在冬季和夏季的单价是不同的:

if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)){
    charge = quantity * plan.summerRate; 
} else{
    charge = quantity * plan.regularRate + plan.regularServiceCharge;  
}

当不了解这块业务的人阅读这份代码的时候,他需要理解if条件中aDate.isBefore(plan.summerStartaDate.isAfter(plan.summerEnd 每一个判断逻辑的含义,当条件更多的时候,语句的可读性则会大大降低。

我们可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。

我们先将这一大段判断条件提取成一个命名清晰的函数:

function summer() { 
    return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd); 
}

同理,再把两段不同判断分支的结果语句提取成函数:

function summerCharge() {
    return quantity * plan.summerRate; 
}
function regularCharge() { 
    return quantity * plan.regularRate + plan.regularServiceCharge; 
}

最后用三元运算符重新安排条件语句:

charge = summer() ? summerCharge() : regularCharge();

function summer() {
    return ! aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
    return quantity * plan.summerRate;
}
function regularCharge() {
    return quantity * plan.regularRate + plan.regularServiceCharge;
}

相较最开始的代码看起来代码量多了不少,不过往往当我们的业务逻辑较冗长的时候,分解条件表达式能让我们的代码更加具有可读性。

合并条件表达式

使用与或运算符(||,&&)将检查条件各不相同,最终行为却一致的判断条件合并为一个条件表达式。

我们先来看代码:

function disabilityAmount(anEmployee) {
    if (anEmployee.seniority < 2) return 0;
    if (anEmployee.monthsDisabled > 12) return 0;
    if (anEmployee.isPartTime) return 0;

这里有一连串的条件检查,都指向同样的结果。既然结果是相同的,就应该把这些条件检查合并成一条表达式。

if ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime)) 

合并完成后,再对这句条件表达式使用提炼函数

function disabilityAmount(anEmployee) {
    if (isNotEligableForDisability()) return 0;
    function isNotEligableForDisability() {
        return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime));
    }
}

这一部分的内容比较简单,相信绝大部分人在编码过程中已经使用过了。

之所以要合并条件代码,有两个重要原因。

首先,合并后的条件代码会表述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更 清晰。当然,合并前和合并后的代码有着相同的效果,但原先代码传达出的信息却是“这里有一些各自独立的条件测 试,它们只是恰好同时发生”。

其次,这项重构往往可以为 使用提炼函数做好准备。将检查条件提炼成一个独立的函数对于厘清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。

以卫语句取代嵌套条件表达式

如果某个条件极其罕见,单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。

例如:

if(a===0) return false;

我们先来看代码:

下面的代码用于计算要支付给员工(employee)的工资。只有还在公司上班的员工才需要支付工资,所以这个函数需要检查两种“员工已经不在公司上班”的情况。

function payAmount(employee) {
    let result
    if(employee.isSeparated) {
        result = {
            amount: 0,
            reasonCode: "SEP"
        };
    } else {
        if (employee.isRetired) {
            result = {
                amount: 0,
                reasonCode: "RET"
            };
        } else {
            lorem.ipsum(dolor.sitAmet);
            1 consectetur(adipiscing).elit();
            sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
            ut.enim.ad(minim.veniam);
            result = someFinalComputation();
        }
    }
    return result;
}  

上面的代码嵌套的条件逻辑让我们看不清代码真实的含义。只有当前两个条件表达式都不为真的时候,这段代码才真正开始它 的主要工作。

条件表达式通常有两种风格。第一种风格是:两个条件分支都属于正常行为。第二种风格则是:只有一个条件分支是正常行为,另一个分支则是异常的情况。

以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如果使用if-then-else结构,你对if分支和else分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”

我们现在来分析上面的代码:

有两种情况不是我们这个函数处理的重点,当遇到这两种情况,语句直接return出去让别人知道这个函数的重点:

1:当员工被裁的情况

2:当员工退休的情况

于是我们修改下代码:

function payAmount(employee) {
    let result;
    if (employee.isSeparated) return {
        amount: 0,
        reasonCode: "SEP"
    };
    if (employee.isRetired) return {
        amount: 0,
        reasonCode: "RET"
    };
    lorem.ipsum(dolor.sitAmet);
    consectetur(adipiscing).elit();
    sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
    ut.enim.ad(minim.veniam);
    result = someFinalComputation();
    return result;
}

此时,result变量已经没有用处了,所以我把它删掉:

function payAmount(employee) {
    if (employee.isSeparated) return {
        amount: 0,
        reasonCode: "SEP"
    };
    if (employee.isRetired) return {
        amount: 0,
        reasonCode: "RET"
    };
    lorem.ipsum(dolor.sitAmet);
    consectetur(adipiscing).elit();
    sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
    ut.enim.ad(minim.veniam);
    return someFinalComputation();;
}

条件反转后使用卫语句

这算是对卫语句的一个扩展应用,以及对合并表达式等的综合应用,思路上相差无几。

我们先看一个例子:(不需要理解这个函数在处理什么)

function adjustedCapital(anInstrument) {
    let result = 0;
    if (anInstrument.capital > 0) {
        if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
            result = (anInstrument.income / anInstrument.duration) * anInstrument.adjust mentFactor;
        }
    }
    return result;
}

首先,我们把条件反转:

function adjustedCapital(anInstrument) {
    let result = 0;
    if (anInstrument.capital <= 0) return result;
    if (anInstrument.interestRate <= 0 || anInstrument.duration <= 0) return result;
    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustment Factor;
    return result;
}

前两行逻辑语句引发的结果一样,所以我可以用合并条件表达式将其合并:

function adjustedCapital(anInstrument) {
    let result = 0;
    if (anInstrument.capital <= 0 || anInstrument.interestRate <= 0 || anInstrument.duration <= 0) return result;
    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustment Factor;
    return result;
}

此时result变量做了两件事:一开始我把它设为0,代表卫语句被触发时的返回值;然后又用最终计算的结果给它赋值。我可以彻底移除这个变量,避免用一个变量承担两重责任,而且又减少了一个可变变量。

function adjustedCapital(anInstrument) {
    if (anInstrument.capital <= 0 || anInstrument.interestRate <= 0 || anInstrument.duration <= 0) return 0;
    return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}

以多态取代条件表达式

我们先看一个简单的例子:(引自JavaScript设计模式与开发实践

假设我们要编写一个地图应用,现在有两家可选的地图 API 提供商供我们接入自己的应用。 目前我们选择的是谷歌地图,谷歌地图的 API 中提供了 show 方法,负责在页面上展示整个地图。 示例代码如下:

var googleMap = { 
  show: function(){ 
  console.log( '开始渲染谷歌地图' ); 
  } 
}; 
var renderMap = function(){ 
  googleMap.show(); 
}; 
renderMap(); // 输出:开始渲染谷歌地图

后来因为某些原因,要把谷歌地图换成百度地图,为了让 renderMap 函数保持一定的弹性, 我们用一些条件分支来让 renderMap 函数同时支持谷歌地图和百度地图:

var googleMap = { 
 show: function(){ 
 console.log( '开始渲染谷歌地图' ); 
 } 
}; 
var baiduMap = { 
 show: function(){ 
 console.log( '开始渲染百度地图' ); 
 } 
}; 
var renderMap = function( type ){ 
 if ( type === 'google' ){ 
 googleMap.show(); 
 }else if ( type === 'baidu' ){ 
 baiduMap.show(); 
 } 
}; 
renderMap( 'google' ); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图

可以看到,虽然 renderMap 函数目前保持了一定的弹性,但这种弹性是很脆弱的,一旦需要 替换成搜搜地图,那无疑必须得改动renderMap函数,继续往里面堆砌条件分支语句。修改代码总是危险的,修改的地方越多,程序出错的可能性就越大,而且当地图的种类越来越多时,renderMap 有可能变成一个巨大的函数。

多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。

把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放—封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。

在这个故事中,显示地图是不变的功能,但是选择哪家的地图是可变的。我们还是先把程序中相同的部分抽象出来,那就是显示某个地图:

var renderMap = function( map ){ 
 if ( map.show instanceof Function ){ 
 map.show(); 
 } 
};

现在来找找这段代码中的多态性。当我们向谷歌地图对象和百度地图对象分别发出“展示地图”的消息时,会分别调用它们的 show 方法,就会产生各自不同的执行结果。对象的多态性提示我们,“做什么”和“怎么去做”是可以分开的,即使以后增加了搜搜地图,renderMap 函数仍然不需要做任何改变。

var googleMap = { 
 show: function(){ 
 console.log( '开始渲染谷歌地图' ); 
 } 
}; 
var baiduMap = { 
 show: function(){ 
 console.log( '开始渲染百度地图' ); 
 } 
};
var sosoMap = { 
 show: function(){ 
 console.log( '开始渲染搜搜地图' ); 
 } 
 
var renderMap = function( map ){ 
 if ( map.show instanceof Function ){ 
 map.show(); 
 } 
 
 renderMap( sosoMap ); // 输出:开始渲染搜搜地图
};

另一种情况则是处理一个数组,里面每一个元素都是不同的类型,都有自己的处理方式。最明显的特征就是有好几个函数都有基于类型代码的switch 语句。若果真如此,我就可以针对switch语句中的每种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。

我们看一个稍显复杂的例子:下面这段程序用来查询不同品种的小鸟的羽毛外形以及飞行速度。

<!--遍历数组里面不同品种小鸟同时查询每只的羽毛外形-->
function plumages(birds) {
    return new Map(birds.map(b = >[b.name, plumage(b)]));
}
<!--遍历数组里面不同品种小鸟同时查询每只的飞行速度-->
function speeds(birds) {
    return new Map(birds.map(b = >[b.name, airSpeedVelocity(b)]));
}
<!--根据传入的鸟类输出羽毛外形-->
function plumage(bird) {
    switch (bird.type) {
    case 'EuropeanSwallow':
        return "average";
    case 'AfricanSwallow':
        return (bird.numberOfCoconuts > 2) ? "tired": "average";
    case 'NorwegianBlueParrot':
        return (bird.voltage > 100) ? "scorched": "beautiful";
    default:
        return "unknown";
    }
}
<!--根据传入的鸟类输出飞行速度-->
function airSpeedVelocity(bird) {
    switch (bird.type) {
    case 'EuropeanSwallow':
        return 35;
    case 'AfricanSwallow':
        return 40 - 2 * bird.numberOfCoconuts;
    case 'NorwegianBlueParrot':
        return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
    default:
        return null;
    }
}

这里有两个不同的操作,其行为都随着“鸟的类型”发生变化,因此可以创建出对应的类,用多态来处理各类型特有的行为。

我先对airSpeedVelocity和plumage两个函数使用函数组合成类

函数组合成类是一种重构方式,如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),就认为是时候组建一个类了。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。

function base(aReading) {...} 
function taxableCharge(aReading) {...} 
function calculateBaseCharge(aReading) {...}

组建成类

class Reading { 
    base() {...} 
    taxableCharge() {...} 
    calculateBaseCharge() {...} 
}

对airSpeedVelocity和plumage两个函数使用函数组合成类

function plumage(bird) {
    return new Bird(bird).plumage;
}
function airSpeedVelocity(bird) {
    return new Bird(bird).airSpeedVelocity;
}
class Bird {
    constructor(birdObject) {
        Object.assign(this, birdObject);
    }
    get plumage() {
        switch (this.type) {
        case 'EuropeanSwallow':
            return "average";
        case 'AfricanSwallow':
            return (this.numberOfCoconuts > 2) ? "tired": "average";
        case 'NorwegianBlueParrot':
            return (this.voltage > 100) ? "scorched": "beautiful";
        default:
            return "unknown";
        }
    }
    get airSpeedVelocity() {
        switch (this.type) {
        case 'EuropeanSwallow':
            return 35;
        case 'AfricanSwallow':
            return 40 - 2 * this.numberOfCoconuts;
        case 'NorwegianBlueParrot':
            return (this.isNailed) ? 0 : 10 + this.voltage / 10;
        default:
            return null;
        }
    }
}

现在所有的条件逻辑依然在Bird类中,而多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。我们应该将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。

所以我们现在要让每种类型的鸟自己负责自己的行为,针对每种鸟创建一个子类,并用一个工厂函数来实例化合适的子类对象。

function plumage(bird) {
    return createBird(bird).plumage;
}
function airSpeedVelocity(bird) {
    return createBird(bird).airSpeedVelocity;
}
function createBird(bird) {
    switch (bird.type) {
    case 'EuropeanSwallow':
        return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
        return new AfricanSwallow(bird);
    case 'NorweigianBlueParrot':
        return new NorwegianBlueParrot(bird);
    default:
        return new Bird(bird);
    }
}
<!--欧洲鸟-->
class EuropeanSwallow extends Bird {}
<!--非洲鸟-->
class AfricanSwallow extends Bird {}
<!--挪威蓝鹦鹉-->
class NorwegianBlueParrot extends Bird {}

我们往每种类型的鸟添加各自的羽毛形状和速度:

class EuropeanSwallow extends Bird {
    get plumage() {
        return "average";
    }
    get airSpeedVelocity() {
        return 35;
    }
}
class AfricanSwallow extends Bird {
    get plumage() {
        return (this.numberOfCoconuts > 2) ? "tired": "average";
    }
    get airSpeedVelocity() {
        return 40 - 2 * this.numberOfCoconuts;
    }
}
class NorwegianBlueParrot extends Bird {
    get plumage() {
        return (this.voltage > 100) ? "scorched": "beautiful";
    }
    get airSpeedVelocity() {
        return (this.isNailed) ? 0 : 10 + this.voltage / 10;
    }
}

完成以后代码大致如下:

function plumages(birds) {
    return new Map(birds.map(b = >createBird(b)).map(bird = >[bird.name, bird.plumage]));
}
function speeds(birds) {
    return new Map(birds.map(b = >createBird(b)).map(bird = >[bird.name, bird.airSpeedVelocity]));
}
function createBird(bird) {
    switch (bird.type) {
    case 'EuropeanSwallow':
        return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
        return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
        return new NorwegianBlueParrot(bird);
    default:
        return new Bird(bird);
    }
}
class Bird {
    constructor(birdObject) {
        Object.assign(this, birdObject);
    }
    get plumage() {
        return "unknown";
    }
    get airSpeedVelocity() {
        return null;
    }
}
class EuropeanSwallow extends Bird {
    get plumage() {
        return "average";
    }
    get airSpeedVelocity() {
        return 35;
    }
}
class AfricanSwallow extends Bird {
    get plumage() {
        return (this.numberOfCoconuts > 2) ? "tired": "average";
    }
    get airSpeedVelocity() {
        return 40 - 2 * this.numberOfCoconuts;
    }
}
class NorwegianBlueParrot extends Bird {
    get plumage() {
        return (this.voltage > 100) ? "scorched": "beautiful";
    }
    get airSpeedVelocity() {
        return (this.isNailed) ? 0 : 10 + this.voltage / 10;
    }
}

看着最终的代码,可以看出Bird超类并不是必需的。在JavaScript中,多态不一定需要类型层级,只要对象实现了适当的函数就行。但在这个例子中,我愿意保留这不必要的超类,因为它能帮助阐释各个子类与问题域之间的关系。

结语

这是这几天在阅读《重构 改善既有代码的设计》 第二版第十章做的一点总结,其中案例、核心思想皆引自本书以及《JavaScript设计模式与开发实践》,后续其实还有引入特例,引入断言等从优化条件语句的方式,因为个人水平问题,理解的也不深入。这篇文章只是当作读书笔记便于自身理解,同时也希望能够抛砖引玉共同进步。