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

搬移特性

搬移函数(Move Function)

  • 示例

    1
    2
    3
    class Account{
    get overdraftCharge() {...}
    }

    重构为

    1
    2
    3
    class AccountType{
    get overdraftCharge() {...}
    }
  • 动机

    它频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少。此时,让它去与那些更亲密的元素相会,通常能取得更好的封装效果,因为系统别处就可以减少对当前模块的依赖。

  • 做法

    1. 检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否需要将它们一并搬移。
    2. 检查待搬函数是否具备多态性。
    3. 将函数复制一份到目标上下文中。调整函数,使它能适应新家。
    4. 执行静态检查。
    5. 设法从源上下文中正确引用目标函数。
    6. 修改源函数,使之成为一个纯委托函数。
    7. 测试。
    8. 考虑对源函数使用内联函数。

搬移字段(Move Field)

  • 示例

    1
    2
    3
    4
    class Customer{
    get plan() { return this._plan; }
    get discountRate() { return this._discountRate; }
    }

    重构为

    1
    2
    3
    4
    class Customer{
    get plan() { return this._plan; }
    get discountRate() { return this.plan.discountRate; }
    }
  • 动机

    1. 每当调用某个函数时,除了传入一个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数。总是一同出现、一同作为函数参数传递的数据,最好是规整到同一条记录中,以体现它们之间的联系。
    2. 如果修改一条记录时,总数需要同时改动另一条记录,那么说明很可能有字段放错了位置。
    3. 更新一个字段时,需要同时在多个结构中做出修改,那也是一个征兆,表明该字段需要被搬移到一个集中的地点,这样每次只需要修改一处地方。
  • 做法

    1. 确保源字段已经得到了良好封装。
    2. 测试。
    3. 在目标对象上创建一个字段(及对应的访问函数)。
    4. 执行静态检查。
    5. 确保源对象里能够正常引用目标对象。
    6. 调整源对象的访问函数,令其使用目标对象的字段。
    7. 测试。
    8. 移除源对象上的字段。
    9. 测试。

搬移语句到函数(Move Statements into Function)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    result.push(`<p>title:${person.photo.title}</p>`);
    result.concat(photoData(person.photo));

    function photoData(aPhoto){
    return[
    `<p>location:${aPhoto.location}</p>`,
    `<p>date:${aPhoto.date.toDateString()}</p>`
    ]
    }

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    result.concat(photoData(person.photo));

    function photoData(aPhoto){
    return[
    `<p>title:${person.photo.title}</p>`,
    `<p>location:${aPhoto.location}</p>`,
    `<p>date:${aPhoto.date.toDateString()}</p>`
    ]
    }
  • 动机

    如果我发现调用某个函数时,总有一些相同的代码也需要每次执行,那么我会考虑将此段代码合并到函数里头。这样,日后对这段代码的修改只需该一处地方,还能对所有调用者同时生效。

  • 做法

    1. 如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句将这些语句挪动到紧邻目标函数的位置。
    2. 如果目标函数仅被唯一一个源函数调用,那么只需将源函数中的重复代码段剪切并粘贴到目标函数中即可。然后运行测试。本做法的后续步骤至此可以忽略。
    3. 如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数,将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个临时的名称,只要易于搜索即可。
    4. 调整函数的其他调用点,令它们调用新提炼的函数。每次调整之后运行测试。
    5. 完成所有引用点的替换后,应用内联函数将目标函数内联到新函数里,并移除原目标函数。
    6. 对新函数应用函数改名,将其改名为原目标函数的名字。

搬移语句到调用者(Move Statements to Callers)

  • 示例

    1
    2
    3
    4
    5
    6
    emitPhotoData(outStream,person.photo);

    function emitPhotoData(outStream photo){
    outStream.write(`<p>title:${photo.title}</p>\n`);
    outStream.write(`<p>location:${photo.location}</p>\n`);
    }

    重构为

    1
    2
    3
    4
    5
    6
    emitPhotoData(outStream,person.photo);
    outStream.write(`<p>location:${photo.location}</p>\n`);

    function emitPhotoData(outStream photo){
    outStream.write(`<p>title:${photo.title}</p>\n`);
    }
  • 动机

    以往在多个地方共用的行为,如今需要在某些调用点面前变现出不同的行为。于是我们得把表现不同的行为从函数中挪出,并搬移到其调用处。

  • 做法

    1. 最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需把要搬移的代码从函数里剪切出来并粘贴回去调用端去即可,必要的时候做些调整。运行测试。如果测试通过,那就大功告成,本手法可以到此为止。
    2. 若调用点不止一两个,则需要先用提炼函数将你不想搬移的代码提炼成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。
    3. 对原函数应用内联函数。
    4. 对提炼出来的函数应用改变函数声明,令其与原函数使用同一个名字。如果你能想到更好的名字,那就用更好的那个。

以函数调用取代内联代码(Replace Inline Code with Funstion Call)

  • 示例

    1
    2
    3
    4
    5
    let appliesToMass = false;

    for(const s of states){
    if(s === "MA") appliesToMass = true;
    }

    重构为

    1
    appliesToMass = states.includes("MA");
  • 动机

    一个命名良好的函数,本身就能极好地解释代码的用途,使读者不必了解其细节。函数同样有助于消除重复,因为同一段代码我不需要编写两次,每次调用一下函数即可。此外,当我需要修改函数的内部实现时,也不需要四处寻找有没有漏改的相似代码。

  • 做法

    1. 将内联函数替代为一个既有函数的调用。
    2. 测试。

移动语句(Slide Statements)

  • 示例

    1
    2
    3
    4
    const pricingPlan = retrievePricingPlan();
    const order = retreiveOrder();
    let charge;
    const chargePerUnit = pricingPlan.unit;

    重构为

    1
    2
    3
    4
    const pricingPlan = retrievePricingPlan();
    const chargePerUnit = pricingPlan.unit;
    const order = retreiveOrder();
    let charge;
  • 动机

    让存在关联的东西一起出现,可以使代码更加容易理解。如果几行代码取用了同一个数据结构,那么最好是让它们一起出现,而不是夹杂在取用其他数据结构的代码中间。

  • 做法

    1. 确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构。
    2. 剪切源代码片段,粘贴到上一步选定的位置上。
    3. 测试。

拆分循环(Split Loop)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    let averageAge = 0;
    let totalSalary = 0;
    for (const p of people){
    averageAge += p.age;
    totalSalary += p.salary;
    }
    averageAge = averageAge / people.lenght;

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let averageAge = 0;
    for (const p of people){
    averageAge += p.age;
    }
    let totalSalary = 0;
    for (const p of people){
    totalSalary += p.salary;
    }
    averageAge = averageAge / people.lenght;
  • 动机

    拆分循环还能让每个循环更容易使用。如果一个循环只计算一个值,那么它直接返回该值即可;但如果循环做了太多件事,那就只能返回结构数据或者通过局部变量传值了。

  • 做法

    1. 复制一遍循环代码。
    2. 识别并移除循环中的重复代码,使每个循环只做一件事。
    3. 测试。

以管道取代循环(Replace Loop with Pipeline)

  • 示例

    1
    2
    3
    4
    5
    const name = [];
    for(const i of input){
    if(i.job === "programmer")
    names.push(i.name);
    }

    重构为

    1
    const name = input.filter(i=>i.job === "programmer").map(i=>i.name);
  • 动机

    我发现一些逻辑如果采用管道来编写,代码的可读性会更强

  • 做法

    1. 创建一个新变量,用以存放参与循环过程的集合。
    2. 从循环顶部开始,将循环里的每一块行为依次搬移出来,在上一步创建的集合变量上用一种管道运算替代之。每次修改后运行测试。
    3. 搬移完循环里的全部行为后,将循环整个删掉。

移除死代码(Remove Dead Code)

  • 示例

    1
    2
    3
    iffalse){
    doSomethingThatUsedToMatter();
    }

    重构为

    1
        
  • 动机

    一旦代码不再被使用,我们就该立马删除它。

  • 做法

    1. 如果死代码可以从外部直接引用,比如它是一个独立的函数时,先查找一下还有无调用点。
    2. 将死代码移除。
    3. 测试。

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