type
Post
status
Published
date
Oct 20, 2022
slug
if-switch-refactoring
summary
条件语句是各种应用程序中必不可少的组成部分,有了它们程序才能更好处理复杂的业务需求,可它们也是让程序复杂度提高的原因之一,必要的时候你可以借助各种重构手法把条件语句变得更加容易理解和易于扩展,本文就具体的情况及其重构方法展开讲解
tags
Refactoring
category
读书笔记
icon
password
Property
Oct 21, 2022 06:16 AM
条件语句
人们无时无刻不在做决定,这些决定影响着他们生活中的大小事,让世间有了参差百态的人生,程序也一样,你可以在各种使用编程语言写就的计算机程序中,根据不同的输入作出决定并且采取行动,而能支持你达到这种目的的基本元素就是条件语句。
具体点来说,条件语句允许应用程序根据谓词表达式的结果是 true 或者 false 来决定运行的语句和分支,它常用的表达式结构有 if-else 和 switch-case 这两种。
第一种就是十分常见的
if-then-else
这种结构,虽然不同的编程语言的形式可能有所不同,但是大体上都准循着下面这种格式If (boolean condition) Then do something Else alternative End If
其中大部分编程语言都支持 if 和 else 表达式的嵌套以应对复杂的现实需求。
第二种就是 Switch 表达式,它会比较给定的值和表达式,根据结果来选取需要执行的语句,基本格式如下
switch (expression) { case value 1: run this code break; case value 2: run this code instead break; // include as many cases as you like default: actually no match, just run this code }
当然还有其他的语法可以作为条件语句来使用,比如模式匹配、Hash匹配等,具体的可以参看维基百科的词条,此处就不展开了,下面就针对这两种常见的条件语句讨论一下条件语句带来的坏味道和可用的重构手法。
坏味道的解决方法
一个“纯粹”的面向对象编程者眼里可能不会容忍任何的条件语句,尤其是 Switch 语句,因为他看到的都是条件语句带来的各种问题,比如职责混乱,逻辑无法灵活扩展,缺少值对象等,所以有时候可能会有这样的观点:所有条件逻辑都应该用多态取代,绝大多数 If 语句都应该被扫进历史的垃圾桶,但实际上我们并不需要如此极端,分析程序并采用合适的重构手法避免条件语句给程序带来的坏味道更加重要。
现在我们就具体的看一下条件语句带来的各种坏味道以及该如何消除它。
抽取过于复杂的 IF 语句
复杂的条件语句往往会导致程序复杂度上升,因为条件分支需要逐个判断和处理,又要根据不同的条件分支来执行不同的业务逻辑,很快一个长长长的函数就出现了,代码就慢慢变得不可阅读和修改。
常用的重构手法就是抽取函数,可以把条件和各个执行语句根据需要抽取到单独的函数中
代码示例
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) charge = quantity * plan.summerRate; else charge = quantity * plan.regularRate + plan.regularServiceCharge; | Extract method V if (summer()) charge = summerCharge(); else charge = regularCharge(); | Ternary operator V charge = summer() ? summerCharge() : regularCharge();
合并过于分散的条件分支
有时候我们很容易发现这样的条件分支多次出现,他们的条件表达式不一样,但是结果是一样的,需要执行的语句是一致的,这个时候如果他们出现的位置过于分散,也会导致程序的可读性大大下降。
常用的重构手法是合并条件分支,把结果相同且处理逻辑相同的分支语句使用“逻辑或”和“逻辑与”合并为一个条件表达式并抽取到方法中,更加清晰的表达意图,因为它等于把描述“做什么”的语句换成了“为什么这样做”
📢 需要确保这些独立的分支都没有副作用才能合并,如果有副作用,可以先把查询和修改分离,然后选取合适的条件合并到其他分支
代码示例
if (anEmployee.seniority < 2) return 0; if (anEmployee.monthsDisabled > 12) return 0; if (anEmployee.isPartTime) return 0; | V if (isNotEligibleForDisability()) return 0; function isNotEligibleForDisability() { return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime)); }
卫语句替代具有特殊检查的条件分支的嵌套语句
一般来说,条件语句有两种形态,一种是每个条件分支都属于正常流程,都比较重要,一种是只有一个分支是核心分支,其他分支都是异常的或者特殊的检查分支,对于第二种情况,我们应该使用“卫语句”来替代。
“卫语句”用来检查先决条件或者特殊条件,以便程序更快的退出或者跳转,使用“卫语句”替代特殊情况下的条件分支嵌套能够使得程序更容易被理解,它的隐藏语义是给某一条正常的分支以特别的重视,它告诉代码阅读者:“当前这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”
代码示例
function getPayAmount() { let result; if (isDead) result = deadAmount(); else { if (isSeparated) result = separatedAmount(); else { if (isRetired) result = retiredAmount(); else result = normalPayAmount(); } } return result; } | V function getPayAmount() { if (isDead) return deadAmount(); //Guard Clauses if (isSeparated) return separatedAmount(); //Guard Clauses if (isRetired) return retiredAmount(); //Guard Clauses return normalPayAmount(); }
以多态取代条件语句
如果一段具有复杂判断分支的条件语句被多次重复,就是一个很明显的坏味道,首先是重复带来的散弹式修改问题,其次是条件语句的低可扩展性在重复的时候让程序难以维护,解决方法就是使用面向对象的多态来取代条件语句。
重构的时候可以针对条件语句中的每种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。如果有一个基础逻辑,在其上又有一些变体,可以把基础逻辑放进超类,这样可以先理解这部分逻辑,暂时不管各种变体,然后可以把每种变体逻辑单独放进一个子类,其中的代码着重强调与基础逻辑的差异。
代码示例
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; } } | V 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; } }
引入特例来取代对特殊值的判断分支
一种常见的重复代码是这种情况,通常一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同,这样这些代码就不断的重复出现。处理这种情况的一个好办法是使用“特例”(Special Case)模式:创建一个特例元素,用以表达对这种特例的共用行为的处理,这样就可以用一个函数调用取代大部分特例检查逻辑。
一个通常需要特例处理的值就是
null
,所以以前这个手法也经常被称为引入 Null 对象,但是 null 其实也是特例的一种特例,所以此处就统称特例模式。代码示例
if (aCustomer === "unknown") customerName = "occupant"; customerName = aCustomer.name; | V class UnknownCustomer extends Customer{ get name() {return "occupant";} } customerName = aCustomer.name;
结束语
以上内容大部分参考自 Martin Flowler 的大作《重构-改善既有代码的设计 第二版》,可以算是读书笔记一篇,强烈推荐此书。
说起重构,它能工作的前提是不能引发新的缺陷,所以用测试构筑的护城河应该是重构的前置条件之一,其次十分推荐使用 IDE 的重构功能避免遗漏和出错,最后熟能生巧,重构也需要不断的练习。