重构-改善既有代码的设计(五)

重新组织数据

拆分变量(Split Variable)

  • 示例

    1
    2
    3
    4
    let temp = 2 * (height + width);
    console.log(temp);
    temp = height * width;
    console.log(temp);

    重构为

    1
    2
    3
    4
    let perimeter = 2 * (height + width);
    console.log(perimeter);
    let area = height * width;
    console.log(area);
  • 动机

    如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。

  • 做法

    1. 在待分解变量的声明及其第一次被赋值处,修改其名称。
    2. 如果可能的话,将新的变量声明为不可修改。
    3. 以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量。
    4. 测试。
    5. 重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一次赋值。

字段改名(Rename Field)

  • 示例

    1
    2
    3
    class Organization{
    get name() { ... }
    }

    重构为

    1
    2
    3
    class Organization{
    get title() { ... }
    }
  • 动机

    命名很重要,对于程序中广泛使用的记录结构,其中字段的命名格外重要。数据结构对于帮助阅读者理解特别重要。

  • 做法

    1. 如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的步骤就都不需要了。
    2. 如果记录还未封装,请先使用封装记录。
    3. 在对象内部私有字段改名,对应调整内部访问该字段的函数。
    4. 测试。
    5. 如果构造函数的参数使用了旧的字段名,运用改变函数声明将其改名。
    6. 运用函数改名给访问函数改名。

以查询取代派生变量(Replace Derived Variablewith Query)

  • 示例

    1
    2
    3
    4
    5
    6
    get discountedTotal() {return this._discountedTotal;}
    set discount(aNumber){
    const old = this._discount;
    this._discount = aNumber;
    this._discountedTotal += old - aNumber;
    }

    重构为

    1
    2
    get discountedTotal() {return this._baseTotal - this._discount;}
    set discount() {this._discount = aNumber;}
  • 动机

    有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也算朝着消除可变性的方向迈出了一大步。计算常能更清晰地表达数据的含义,而且也避免了“源数据修改时忘了更新派生变量”的错误

  • 做法

    1. 识别出所有对变量做更新的地方。如有必要,用拆分变量分隔各个更新点。
    2. 新建一个函数,用于计算该变量的值。
    3. 用引入断言断言该变量和计算函数始终给出同样的值。
    4. 测试。
    5. 修改读取该变量的代码,令其调用新建的函数。
    6. 测试。
    7. 用移除死代码去掉变量的声明和赋值。

将引用对象改为值对象(Change Reference to Value)

  • 示例

    1
    let customer = new Customer(customerData);

    重构为

    1
    let customer = customerRepository.get(customerData.id);
  • 动机

    把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有这些实体对象,只为每个实体创建一次对象,以后始终从仓库中获取该对象。

  • 做法

    1. 为相关对象创建一个仓库(如果还没有这样一个仓库的话)。
    2. 确保构造函数有办法找到关联对象的正确实例。
    3. 修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试。

简化条件逻辑

分解条件表达式(Decompose Conditional)

  • 示例

    1
    2
    3
    4
    if(!aData.isBefore(plan.summerStart) && !aData.isAfter(plan.summerEnd))
    charge = quantity * plan.summerRate;
    else
    charge = quantity * plan.regularRate + plan.regularServiceCharge;

    重构为

    1
    2
    3
    4
    if(summer())
    charge = summerCharge();
    else
    charge = regularCgharge();
  • 动机

    对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

  • 做法

    1. 对条件判断和每个条件分支分别运用提炼函数手法。

合并条件表达式(Consolidate Conditional Expression)

  • 示例

    1
    2
    3
    if(anEmployee.seniority < 2return 0;
    if(anEmployee.mothsDisabled > 12return 0;
    if(anEmployee.isPartTimereturn 0;

    重构为

    1
    2
    3
    4
    5
    if(isNotEligibleForDisability()) return 0;

    function isNotEligibleForDisability(){
    return ((anEmployee.seniority < 2) || (anEmployee.mothsDisabled > 12) || (anEmployee.isPartTime));
    }
  • 动机

    1. 合并后的条件代码会表述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。
    2. 将检查条件提炼成一个独立的函数对于厘清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。
  • 做法

    1. 确定这些条件表达式都没有副作用。
    2. 使用适当的逻辑运算符,将两个相关条件表达式合并为一个。
    3. 测试。
    4. 重复前面的合并过程,直到所有相关的条件表达式都合并到一起。
    5. 可以考虑对合并后的条件表达式实施提炼函数。

以卫语句取代嵌套条件表达式(Replace NestedConditional with Cuard Clauses)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function getPayAmount(){
    let result;
    if(isDead)
    result = deadAmount;
    else{
    if(isSeparated)
    result = separatedAmount();
    else{
    if(isRetired)
    result = retiredAmount();
    else
    result = normalPayAmount();
    }
    }
    return result;
    }

    重构为

    1
    2
    3
    4
    5
    6
    function getPayAmount(){
    if(isDead) return deadAmount();
    if(isDead) return separatedAmount();
    if(isDead) return retiredAmount();
    return normalPayAmount();
    }
  • 动机

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

  • 做法

    1. 选中最外层需要被替换的条件逻辑,将其替换成卫语句。
    2. 测试。
    3. 有需要的话,重复上述步骤。
    4. 如果所有卫语句都引发同样的结果,可以使用合并条件表达式合并之。

以多态取代条件表达式(Replace Conditional with Polymorphism)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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";
    }

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class EuropeanSwallow{
    get plumage(){
    return "average";
    }
    }
    class AfricanSwallow{
    get plumage(){
    return (bird.numberOfCoconuts > 2) ? "tired" : "average";
    }
    }
    class NorwegianBlueParrot{
    get plumage(){
    return (bird.voltage > 100) ? "scorched" : "beautiful";
    }
    }
  • 动机

    我发现可以将条件逻辑拆分到不同的场景,从而拆解复杂的条件逻辑。这种拆分有时用条件逻辑本身的结构就足以表达,但使用类和多态能把逻辑的拆分表述得更清晰。

  • 做法

    1. 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
    2. 在调用方代码中使用工厂函数获得对象实例。
    3. 将带有条件逻辑的函数移到超类中。
    4. 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
    5. 重复上述过程,处理其他条件分支。
    6. 在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为 abstract,或在其中直接抛出异常,表明计算责任都在子类中。

引入特例(Introduce Special Case)

  • 示例

    1
    if(aCustomer === "unknown") customerName = "occupant";

    重构为

    1
    2
    3
    class UnknownCustomer{
    get name() {return "occupant";}
    }
  • 动机

    创建一个特例元素,用以表达对这种特例的共用行为的处理,这样我就可以用一个函数调用取代大部分特例检查逻辑。

  • 做法

    1. 给重构目标添加检查特例的属性,令其返回 false。
    2. 创建一个特例对象,其中只有检查特例的属性,返回 true。
    3. 对“与特例值做比对”的代码运用提炼函数,确保所有客户端都是用这个新函数,而不再直接做特例的比对。
    4. 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成。
    5. 修改特例比对函数的主体,在其中直接使用检查特例的属性。
    6. 测试。
    7. 使用函数组合成类或函数组合成变换,把通用的特例处理逻辑都搬移到新建的特例对象中。
    8. 对特例比对函数使用内联函数,将其内联到仍然需要的地方。

引入断言(Introduce Assertion)

  • 示例

    1
    if(this.discountRate) base = base - (this.discountRate * base);

    重构为

    1
    2
    3
    4
    assert(this.discountRate >= 0);
    if(this.discountRate)
    base = base - (this.discountRate * base);
    }
  • 动机

    断言是一种很有价值的交流形式——它们告诉阅读者,程序在执行到这一点时,对当前状态做了何种假设。另外断言对调试也很有帮助。

  • 做法

    1. 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况。

重构-改善既有代码的设计(五)
http://blog.chcaty.cn/2021/06/20/chong-gou-gai-shan-ji-you-dai-ma-de-she-ji-wu/
作者
caty
发布于
2021年6月20日
许可协议