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

重构API

将查询函数和修改函数分离(Separate Query from Modifier)

  • 示例

    1
    2
    3
    4
    5
    function getTotalOutatandingAndSendBill(){
    const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
    sendBill();
    return result;
    }

    重构为

    1
    2
    3
    4
    5
    6
    function totalOutstanding(){
    return customer.invoices.reduce((total, each) => each.amount + total, 0);
    }
    function sendBill(){
    emailGateway.send(formatBill(customer));
    }
  • 动机

    任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离。

  • 做法

    1. 复制整个函数,将其作为一个查询来命名。
    2. 从新建的查询函数中去掉所有造成副作用的语句。
    3. 执行静态检查。
    4. 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试。
    5. 从原函数中去掉返回值。
    6. 测试。

函数参数化(Parameterize Function)

  • 示例

    1
    2
    3
    4
    5
    6
    function tenPercentRaise(aPerson){
    aPerson.salary = aPerson.salary.multiply(1.1);
    }
    function fivePercentRaise(aPerson){
    aPerson.salary = aPerson.salary.multiply(1.05);
    }

    重构为

    1
    2
    3
    function raise(aPerson, factor){
    aPerson.salary = aPerson.salary.multiply(1 + factor);
    }
  • 动机

    如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。

  • 做法

    1. 从一组相似的函数中选择一个。
    2. 运用改变函数声明,把需要作为参数传入的字面量添加到参数列表中。
    3. 修改该函数所有的调用处,使其在调用时传入该字面量值。
    4. 测试。
    5. 修改函数体,令其使用新传入的参数。每使用一个新参数都要测试。
    6. 对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试。

移除标记参数(Remove Flag Argument)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function setDismension(name, value){
    if(name === "height"){
    this._height = value;
    return;
    }
    if(name === "width"){
    this._width = value;
    return;
    }
    }

    重构为

    1
    2
    function setHeight(value) {this._height = value;}
    function setWidth(value) {this._width = value;}
  • 动机

    “标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。只有参数值影响了函数内部的控制流,这才是标记参数。移除标记参数不仅使代码更整洁,并且能帮助开发工具更好地发挥作用。

  • 做法

    1. 针对参数的每一种可能值,新建一个明确函数。
    2. 对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。

保持对象完整(Preserve Whole Object)

  • 示例

    1
    2
    3
    const low = aRoom.daysTempRange.low;
    const high = aRoom.daysTempRange.high;
    if (aPlan.withinRange(low.high))

    重构为

    1
    if(aPlan.withinRange(aRoom.daysTempRange))
  • 动机

    “传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,我就不用为此修改参数列表。并且传递整个记录也能缩短参数列表,让函数调用更容易看懂。如果有很多函数都在使用记录中的同一组数据,处理这部分数据的逻辑常会重复,此时可以把这些处理逻辑搬移到完整对象中去。

  • 做法

    1. 新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)。
    2. 在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据)。
    3. 执行静态检查。
    4. 逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
    5. 所有调用处都修改过去之后,使用内联函数把旧函数内联到新函数体内。
    6. 给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同时修改所有调用处。

以查询取代参数(Replace Parameter with Query)

  • 示例

    1
    2
    3
    4
    5
    availableVacation(anEmployee, anEmployee.grade);

    function availableVacation(anEmployee, grade){
    // calculate vacation...
    }

    重构为

    1
    2
    3
    4
    5
    6
    availableVacation(anEmployee);

    function availableVacation(anEmployee){
    const grade = anEmployee.grade;
    // calculate vacation...
    }
  • 动机

    如果想要去除的参数值只需要向另一个参数查询就能得到,这是以查询取代参数最安全的场景。如果可以从一个参数推导出另一个参数,那么几乎没有任何理由要同时传递这两个参数。

  • 做法

    1. 如果有必要,使用提炼函数将参数的计算过程提炼到一个独立的函数中。
    2. 将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试。
    3. 全部替换完成后,使用改变函数声明将该参数去掉。

以参数取代查询(Replace Query with Parameter)

  • 示例

    1
    2
    3
    4
    5
    6
    targetTemperature(aPlan)

    function targetTemperature(aPlan){
    currentTemperature = thermostat.currentTemperature;
    // rest of function
    }

    重构为

    1
    2
    3
    4
    5
    targetTemperature(aPlan,thermostat.currentTemperature)

    function targetTemperature(aPlan,currentTemperature){
    // rest of function
    }
  • 动机

    为了让目标函数不再依赖于某个元素,我把这个元素的值以参数形式传递给该函数。

  • 做法

    1. 对执行查询操作的代码使用提炼变量,将其从函数体中分离出来。
    2. 现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对这部分代码使用提炼函数。
    3. 使用内联变量,消除刚才提炼出来的变量。
    4. 对原来的函数使用内联函数。
    5. 对新的函数改名,改回原来函数的名字。

移除设值函数(Remove Setting Method)

  • 示例

    1
    2
    3
    4
    class Person{
    get name() {...}
    set name(aString) {...}
    }

    重构为

    1
    2
    3
    class Person{
    get name() {...}
    }
  • 动机

    如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段声明为不可变)。

  • 做法

    1. 如果构造函数伤无法得到想要设入字段的值,就使用改变函数声明将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值。
    2. 移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试。
    3. 使用内联函数消去设值函数。如果可能的话,把字段声明为不可变。
    4. 测试。

以工厂函数取代构造函数(Replace Constructor with Factory Function)

  • 示例

    1
    leadEngineer = new Employee(document.leadEngineer,'E');

    重构为

    1
    leadEngineer = createEngineer(document.leadEngineer);
  • 动机

    构造函数无法根据环境或参数信息返回子类实例或代理对象;构造函数的名字是固定的。无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的操作符来调用。

  • 做法

    1. 新建一个工厂函数,让它调用现有的构造函数。
    2. 将调用构造函数的代码改为调用工厂函数。
    3. 每修改一处就执行测试。
    4. 尽量缩小构造函数的可见范围。

以命令取代函数(Replace Function with Command)

  • 示例

    1
    2
    3
    4
    5
    function score(candidate, medicalExam, scoringGuide){
    let result = 0;
    let healthLevel = 0;
    // long body code
    }

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Scorer{
    constructor(candidate, medicalExam, scoringGuide){
    this._candidate = candidate;
    this._medicalExam = medicalExam;
    this._scoringGuide = scoringGuide;
    }

    execute(){
    this._result = 0;
    this._healthLevel = 0;
    // long bidy code
    }
    }
  • 动机

    与普通的函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。处理函数调用本身,命令对象还可以支持附加的操作,例如撤销操作。我可以通过命令对象提供的方法来设值命令的参数值,从而支持更丰富的生命周期管理能力。我可以借助继承和钩子对函数加以定制。

  • 做法

    1. 为想要包装的函数创建一个空的类,根据该函数的名字为其命名。
    2. 使用搬移函数把函数移到空的类里。
    3. 可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数。

以函数取代命令(Replace Command with Function)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ChargeCalculator{
    constructor(customer, usage){
    this._customer = customer;
    this._usage = uasge;
    }
    execute(){
    return this._customer.rate * this._usage;
    }
    }

    重构为

    1
    2
    3
    function charge(customer, usage){
    return customer.rate * usage;
    }
  • 动机

    如果函数不是太复杂,那么命令对象可能显得费而不惠,就应该考虑将其变回普通的函数。

  • 做法

    1. 运用提炼函数,把“创建并执行命令对象”的代码单独提炼到一个函数中。
    2. 对命令对象在执行阶段用到的函数,逐一使用内联函数。
    3. 使用改变函数声明,把构造函数的参数转移到执行函数。
    4. 对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数、每次修改后都要测试。
    5. 把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换命令对象的那个函数)。
    6. 测试。
    7. 用移除死代码把命令类消去。

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