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

重构名录-第一组重构

提炼函数(Extract Function)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    function printOwing(invoice){
    printBanner()
    let outstanding = calculateOutstanding();

    // print details
    console.log(`name:${invoice.customer}`);
    console.log(`amount:${outstanding}`);
    }

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function printOwing(invoice){
    printBanner()
    let outstanding = calculateOutstanding();
    printDetails(outstanding)

    function printDetails(outstanding){
    console.log(`name:${invoice.customer}`);
    console.log(`amount:${outstanding}`);
    }
    }
  • 动机

    “将意图和实现分开”:如果你需要花时间浏览一段代码才能弄清楚它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途。

  • 做法

    1. 创造一个新的函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)
    2. 待提炼的代码从源函数复制到新建的目标函数中
    3. 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的便令。若是,以参数的形式将它们传递给新函数。
    4. 所有变量都处理完之后,编译。
    5. 在源函数中,将被提炼代码替换为对目标函数的调用。
    6. 测试
    7. 查看其它代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数。

内联函数(Inline Function)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    function getRating(driver){
    return moreThanFiveLateDeliveries(dirver) ? 2 : 1;
    }

    function moreThanFiveLateDeliveries(driver){
    return driver.numberOfLateDeliveries > 5;
    }

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
        function getRating(driver){
    return (driver.numberOfLateDeliveries >5) ? 2 : 1;
    }
    ``

    * 动机
    1. 某些函数,其内部代码和函数名称同样清晰易读,你就应该去掉这个函数,直接使用其中的代码。
    2. 手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以喜欢的方式重新提炼出小函数
    3. 间接层有其价值,但不是所有间接层都有价值。通过内联手法,可以找出那些有用的间接层,同时将无用的间接层去除。
    * 做法
    1. 检查函数,确定它不具多态性。
    2. 找出这个函数的所有调用点。
    3. 将这个函数的所有调用点都替换成函数本体。
    4. 每次替换之后,执行测试。
    5. 删除该函数的定义。

    #### 提炼函数(Extract Variable

    * 示例

    ```js
    return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1,100)

    重构为

    1
    2
    3
    4
    const basePrice = order.quantity * order.itemPrice;
    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
    const shipping = Math.min(basePirce * 0.01, 100);
    return basePrice - quantityDiscount + shipping;
  • 动机

    1. 表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。
    2. 这样的变量在调试时也很方便,他们给调试器和打印语句提供了便利的抓手。
  • 做法

    1. 确认要提炼的表达式没有副作用。
    2. 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
    3. 用这个新变量取代原来的表达式。
    4. 测试。

内联变量(Inline Variable)

  • 示例

    1
    2
    let basePrice = anOther.basePrice;
    return (basePrice > 1000);

    重构为

    1
    return anOther.basePrice > 1000;
  • 动机
    在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。

  • 做法

    1. 检查确认变量赋值语句的右侧表达式没有副作用。
    2. 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。
    3. 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
    4. 测试。
    5. 重复前面两步,逐一替换其他所有使用该变量的地方。
    6. 删除该变量的声明点和赋值语句。
    7. 测试。

改变函数声明(Change Function Declaration)

  • 示例

    1
    function circum(radius){...}

    重构为

    1
    function circumference(radius){...}
  • 动机

    1. 一个好的函数名字能让我一眼看出函数的用途,而不必查看其实现代码。
    2. 函数的参数列表阐述了函数如何与外部世界共处。
    3. 修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件。
  • 做法

    简单的做法

    1. 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
    2. 修改函数声明,使其成为你期望的状态。
    3. 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
    4. 测试

    迁移式做法

    1. 如果有必要的话,先对函数体内部加以重构,使后面的提炼不走易于开展。
    2. 使用提炼函数方法将函数体提炼成一个新函数。
    3. 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
    4. 测试。
    5. 对旧函数使用内联函数。
    6. 如果新函数使用了临时的名字,再次使用改变函数声明将其改回原来的名字。
    7. 测试。

封装变量(Encapsulate Variable)

  • 示例

    1
    let defaultOwner = {firstName:"Martion",lastName:"Fowler"};

    重构为

    1
    2
    3
    let defaultOwnerData = {firstName:"Martion",lastName:"Fowler"};
    export function defaultOwner() { return defaultOwnerData; }
    export function setDefaultOwner(args) { defaultOwnerData = args; }
  • 动机

    1. 如果想要搬移一处被广泛使用的数据,最好的方法往往是先以函数形式封装所有对该数据的访问。
    2. 封装能提供一个清晰的观测点。可以由此监控数据的变化和使用情况;还可以轻松地添加数据被修改时的验证或后续逻辑。
  • 做法

    1. 创建封装函数,在其中访问和更新变量值。
    2. 执行静态检查。
    3. 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试。
    4. 限制变量的可见性。
    5. 测试。
    6. 如果变量的值是一个记录,考虑使用封装记录。

变量改名(Rename Variable)

  • 示例

    1
    let a = height * width

    重构为

    1
    let area = height * width
  • 动机

    1. 变量可以很好地解释一段程序在干什么–如果变量名起得好的话。
    2. 使用范围越广,名字的好坏就越重要。
  • 做法

    1. 如果变量被广泛使用,考虑运用封装变量将其封装起来。
    2. 找出所有使用该变量的代码,逐一修改。
    3. 测试。

引入参数对象(Introduce Parameter Object)

  • 示例

    1
    2
    3
    function amountInvoiced(startDate, endDate){...}
    function amountReceived(startDate, endDate){...}
    function amountOcerdue(startDate, endDate){...}

    重构为

    1
    2
    3
    function amountInvoiced(aDateRange){...}
    function amountReceived(aDateRange){...}
    function amountOcerdue(aDateRange){...}
  • 动机

    1. 将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得清晰。
    2. 经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。
    3. 会催生代码中更深层次的改变。一旦识别出新的数据结构,就可以重组程序的行为来使用这些结构。
  • 做法

    1. 如果暂时还没有一个合适的数据结构,就创建一个。
    2. 测试。
    3. 使用改变函数声明方法给原来的函数新增一个参数,类型是新建的数据结构。
    4. 测试。
    5. 调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
    6. 用新数据结构中每项元素,逐一取代参数列表中与之对应1的参数项,然后删除原来的参数。测试。

函数组合成类(Combine Functions into Class)

  • 示例

    1
    2
    3
    function base(aReading){...}
    function taxableCharge(aReading){...}
    function calculateBaseCharge(aReading){...}

    重构为

    1
    2
    3
    4
    5
    class Reading{
    base(){...}
    taxableCharge(){...}
    calculateBaseCharge(){...}
    }
  • 动机

    1. 如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个功用的环境,在对象内部调用这些函数可以少传很多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他地方。
    2. 除了可以把已有的函数组织起来,我们还可以去发现其他的计算逻辑,将它们也重构到新的类当中。
    3. 使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致。
  • 做法

    1. 运用封装记录方法对多个函数共用的数据记录加以封装。
    2. 对于使用该记录结构的每个函数,运用搬移函数方法将其移入新类。
    3. 用以处理该数据记录的逻辑可以用提炼函数方法提炼出来,并移入新类。

函数组合成变换(Combine Functions into Transform)

  • 示例

    1
    2
    function base(aReading){...}
    function taxableCharge(aReading){...}

    重构为

    1
    2
    3
    4
    5
    6
    function enrichReading(argReading){
    const aReading = _.cloneDeep(argReading);
    aReading.baseCharge = base(aReading);
    aReading.taxableCharge = taxableCharge(aReading);
    return aReading;
    }
  • 动机

    1. 把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。
    2. 数据变换(transform)函数:这种函数接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。
    3. 函数组合成类:先用源数据创建一个类,再把相关的计算逻辑搬移到类中。
    4. 区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储到新生成的记录中,一旦源数据被修改,就会遭遇数据不一致。
  • 做法

    1. 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
    2. 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
    3. 测试
    4. 针对其他相关的计算逻辑,重复上述步骤。

拆分阶段(Split Phase)

  • 示例

    1
    2
    3
    const orderData = orderString.split(/\s+/);
    const productPrice = priceList[orderData[0].split("-")[1]];
    const orderPrice = parseInt(orderData[1]) * productPrice;

    重构为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const orderRecord = parseOrder(order);
    const orderPrice = price(orderRecord, priceList);

    function parseOrder(aString){
    const values = aString.split(/\s+/);
    return ({
    productID: values[0].split("-")[1],
    quantity:parseInt(values[1]),
    });
    }
    function price(order, priceList) {
    return order.quantity * priceList[order.productID];
    }
  • 动机

    1. 每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块,因为这样到了需要修改的时候,我就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。
    2. 最简洁的拆分方法之一,就是把一大段行为分成顺序执行的两个阶段。可能你有一段逻辑处理,其输入数据的格式不符合计算逻辑的要求,所以你得先对输入数据做一番调整,使其便于处理。也可能是你把数据处理洛安吉分成顺序执行的多个步骤,每个步骤负责的任务全然不同。
  • 做法

    1. 将第二阶段的代码提炼成独立的函数。
    2. 测试。
    3. 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
    4. 测试。
    5. 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。每次搬移后都要执行测试。
    6. 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构。

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