重构:改善既有代码的设计-笔记(三)

501 阅读14分钟

重构:改善既有代码的设计-购书链接

重新组织数据

  1. 拆分变量

    一个变量只承担一种责任

    function distanceTravelled(){
        let acc = 0
        acc = ''
    }
    
    function distanceTravelled(){
        let acc = 0
        let str = ''
    }
    

    做法

    1. 在待分解变量的声明及其第一次被赋值处,修改其名称
    2. 如果可能的话,把新变量声明为不可修改
    3. 以该变量的第二次赋值动作为分解,修改此前对该变量的引用,引用新变量
    function distanceTravelled(scenario, time){
        let result
        let acc = scenario.primaryForce / scenario.mass
        let primaryTime = Math.min(time, scenario.delay)
        result = 0.5 * acc * primaryTime * primaryTime
        let secondTime = time - scenario.delay
        if(secondTime > 0){
            let primaryVelocity = acc * scenario.delay
            acc = (scenario.primaryForce + scenario.secondForce) / scenario.mass
            result += primaryVelocity * secondTime + 0.5 * acc * secondTime * secondTime
        }
        return result
    }
    
    function distanceTravelled(scenario, time){
        let result
        let primaryAcceleration = scenario.primaryForce / scenario.mass
        let primaryTime = Math.min(time, scenario.delay)
        result = 0.5 * primaryAcceleration * primaryTime * primaryTime
        let secondTime = time - scenario.delay
        if(secondTime > 0){
            let primaryVelocity = primaryAcceleration * scenario.delay
            let secondaryAcceleration = (scenario.primaryForce + scenario.secondForce) / scenario.mass
            result += primaryVelocity * secondTime + 0.5 * secondaryAcceleration * secondTime * secondTime
        }
        return result
    }
    
  2. 字段改名

    将不符合语义的字段名改为更加贴切的名字

    const organization = { name: 'Acme', country: 'GB' }
    
    class Organization {
        constructor(data){
            this._title = data.title || data.name
        }
        get title() {return this._title }
        set title(title) {this._title = title}
    }
    

    做法

    1. 如果记录未封装,则先封装记录
    2. 对字段改名,并将引用处同步更新
    const organization = { name: 'Acme', country: 'GB' }
    
    class Organization {
        constructor(data){
            this._title = data.title || data.name
            this._country = data.country
        }
        get title() {return this._title }
        set title(title) {this._title = title}
        get country() {return this._country}
        set country(country) {this._country = country}
    }
    const organization =  new Organization({ name: 'Acme', country: 'GB' })
    
  3. 以查询取代派生变量

    可变对数据是软件中最大对错误源头,对数据对修改常常导致代码各个部分以丑陋的形式互相耦合

    class ProductionPlan {
        constructor(adjustments){
            this._adjustments = adjustments
            this._production = this._adjustments.reduce((prev, item) => prev + item.amount, 0)
        }
        get production(){return this._production}
        applyAdjustment(anAdjustment){
            this._adjustments.push(anAdjustment)
            this._production += anAdjustment.amount
        }
    }
    
    class ProductionPlan {
        constructor(adjustments){
            this._adjustments = adjustments
        }
        get production(){return this._adjustments.reduce((prev, item) => prev + item.amount, 0)}
        applyAdjustment(anAdjustment){
            this._adjustments.push(anAdjustment)
        }
    }
    

    做法

    1. 识别出所有对变量做更新的地方
    2. 新建一个函数用于计算该变量的值
    class ProductionPlan {
        constructor(adjustments){
            this._adjustments = adjustments
            this._production = this._adjustments.reduce((prev, item) => prev + item.amount, 0)
        }
        get production(){return this._production}
        applyAdjustment(anAdjustment){
            this._adjustments.push(anAdjustment)
            this._production += anAdjustment.amount
        }
    }
    
    class ProductionPlan {
        constructor(adjustments){
            this._adjustments = adjustments
        }
        get production(){return this._adjustments.reduce((prev, item) => prev + item.amount, 0)}
        applyAdjustment(anAdjustment){
            this._adjustments.push(anAdjustment)
        }
    }
    
  4. 将引用对象改为值对象(将值对象改成引用对象)

    更新一个引用对象的属性值,直接替换整个内部对象

    class Person {
        constructor(){
            this._tele = new TelephoneNumber()
        }
    }
    class Tele{...}
    
    class Person {
        set officeAreaCode(arg){return this._tele = new Tele() }
        set officeNumber(arg){return this._tele = new Tele() }
    }
    class Tele{...}
    

    做法

    1. 检查重构目标是否为不可变的对象,或者是否修改为不可变对象
    2. 修改对象中的设值函数,创建一个新的对象
    class Person {
        constructor(name){
            this._name = name
            this._telephoneNumber = new TelephoneNumber()
        }
        get name(){return this._name }
        get telephoneNumber(){return this._telephoneNumber.toString() }
        get officeAreaCode(){return this._telephoneNumber.areaCode }
        set officeAreaCode(arg){return this._telephoneNumber.areaCode = arg }
        get officeNumber(){return this._telephoneNumber.number }
        set officeNumber(arg){return this._telephoneNumber.number = arg }
    }
    class TelephoneNumber{
        get number(){return this._number}
        set number(arg){return this._number = arg}
        get areaCode(){return this._areaCode }
        set areaCode(arg){return this._areaCode = arg}
        toString(){return `(${this._areaCode}) ${this._number}`}
    }
    
    class Person {
        constructor(name){
            this._name = name
        }
        get name(){return this._name }
        get telephoneNumber(){return this._telephoneNumber.toString() }
        get officeAreaCode(){return this._telephoneNumber.areaCode }
        set officeAreaCode(arg){return this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber) }
        get officeNumber(){return this._telephoneNumber.number }
        set officeNumber(arg){return this._telephoneNumber = new TelephoneNumber(this.areaCode, arg) }
    }
    class TelephoneNumber{
        constructor(areaCode, number){
            this._areaCode = areaCode
            this._number = number
        }
        get number(){return this._number}
        get areaCode(){return this._areaCode }
        toString(){return `(${this._areaCode}) ${this._number}`}
    }
    
  5. 将值对象改成引用对象(将引用对象改为值对象)

    过多的副本会难以维护,可以创建一个仓库存储

    class Order {
        constructor(data){
            this._customer = new Customer(data.customer)
        }
    }
    class Customer {}
    
    let _repository = {}
    _repository.customer = new Map()
    function registerCustomer(id){
        if(!_repository.customer.has(id)){
            _repository.customer.set(id, new Customer(id))
        }
        return findCustomer(id)
    }
    function findCustomer(id){
        return _repository.customer.get(id)
    }
    class Order {
        constructor(data){
            this._customer = registerCustomer(data.customer)
        }
    }
    class Customer {}
    

    做法

    1. 为相关的对象创建一个仓库
    2. 确保构造函数有办法找到关联对象的正确实例
    3. 修改宿主对象的构造函数,令其从仓库中获取关联对象
    class Order {
        constructor(data){
            this._number = data.number;
            this._customer = new Customer(data.customer)
        }
        get customer(){return this._customer}
    }
    class Customer {
        constructor(id){
            this._id = id
        }
        get id(){return this._id}
    }
    
    let _repositoryData
    function initialize(){
        _repositoryData = {}
        _repositoryData.customer = new Map()
    }
    function registerCustomer(id){
        if(!_repositoryData.customer.has(id)){
            _repositoryData.customer.set(id, new Customer(id))
        }
        return findCustomer(id)
    }
    function findCustomer(id){
        return _repositoryData.customer.get(id)
    }
    initialize()
    class Order {
        constructor(data){
            this._number = data.number;
            this._customer = registerCustomer(data.customer)
        }
        get customer(){return this._customer}
    }
    class Customer {
        constructor(id){
            this._id = id
        }
        get id(){return this._id}
    }
    

重新组织数据.png

简化条件逻辑

  1. 分解条件表达式

    把复杂的条件表达式分解成多个独立的函数

    function price(){
        ...
        if(aDate.isSummer || aDate.isSpring){
            charge = quantity * aPlan.summerRate
        }else{
            charge = quantity * aPlan.rate
        }
        ...
    }
    
    function price(aDate, aPlan){
        ...
        return summerOrSpring() ? summerCharge() : orderCharge()
        function summerOrSpring(){...}
        function summerCharge(){...}
        function orderCharge(){...}
    }
    

    做法

    1. 对条件判断和每个条件分支都使用提炼函数
    function price(aDate, aPlan){
        let charge
        let quantity = 100
        if(aDate.isSummer || aDate.isSpring){
            charge = quantity * aPlan.summerRate
        }else{
            charge = quantity * aPlan.rate
        }
        return charge
    }
    
    function price(aDate, aPlan){
        let quantity = 100
        return summerOrSpring() ? summerCharge() : orderCharge()
        function summerOrSpring(){
            return aDate.isSummer || aDate.isSpring
        }
        function summerCharge(){
            return quantity * aPlan.summerRate
        }
        function orderCharge(){
            return quantity * aPlan.rate
        }
    }
    
  2. 合并条件表达式

    把相同返回的条件合并成一处

    if(state == 1) return 0
    if(start == 2) return 0
    return 1
    
    if(state == 1 || start == 2) return 0
    return 1
    

    做法

    1. 确定条件表达式没有副作用
    2. 使用适当的运算符合并
    if(state == 1) return 0
    if(start == 2) return 0
    if(end == 3) return 0
    return 1
    
    if(state == 1 || start == 2 || end == 3) return 0
    return 1
    
  3. 以卫语句取代嵌套条件表达式

    多层的嵌套判断会减低可读性,可以使用卫语句进行提前返回

    function payAmount(employee){
        if(employee.isSeparated){
            ...
        }else{
            if(employee.isRetired){
                ...
            }else{
                ...
            }
        }
    }
    
    function payAmount(employee){
        if(employee.isSeparated) return 
        if(employee.isRetired) return 
        return 
    }
    

    做法

    1. 选中外层需要被替换的条件逻辑,替换成卫语句
    function payAmount(employee){
        let result
        if(employee.isSeparated){
            result = {amount: 0, reasonCode: 'SEP'}
        }else{
            if(employee.isRetired){
                result = {amount: 0, reasonCode: 'RET'}
            }else{
                result = {amount: 1000, reasonCode: ''}
            }
        }
        return result
    }
    
    function payAmount(employee){
        if(employee.isSeparated) return {amount: 0, reasonCode: 'SEP'}
        if(employee.isRetired) return {amount: 0, reasonCode: 'RET'}
        return {amount: 1000, reasonCode: ''}
    }
    
  4. 以多态取代条件表达式

    通过类的多态去改善比较复杂的条件表达式

    class Bird {
        constructor(name, type){
            switch (bird.type){
                case 'E': this.plumage = 'e'
                case 'N': this.plumage = 'n'
                default: this.plumage = 'unknown'
            }
        }
    }
    
    class Bird {
        get plumage(){
            return 'unknown'
        }
    }
    class E extends Bird{
        get plumage(){
            return 'e'
        }
    }
    class N extends Bird{
        get plumage(){
            return 'n'
        }
    }
    function createBird(...arg){
        switch (arg[1]){
            case 'E': return new E(...arg);
            case 'N': return new N(...arg);
            default: return new Bird(...arg);
        }
    }
    

    做法

    1. 如果现有的类不具备多态行为,就用工厂模式创建
    2. 在调用方代码中使用工程函数获得对象实例
    3. 将带有条件逻辑的函数移动到超类中
    4. 任选一个子类,在其中创建一个函数,使其复写超类中容纳条件表达式的那个函数
    5. 将于该子类相关的条件表达式分支复制到新函数中
    6. 处理完后将超类的函数声明为抽象函数
    function plumages(birds){
        return new Map(birds.map(b => [b.name, b.plumage]))
    }
    function speeds(birds){
        return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]))
    }
    function plumage(bird){
        switch (bird.type){
            case 'E': return 'a'
            case 'A': return bird.counts > 2 ? 't' : 'a'
            case 'N': return bird.voltage > 100 ? 's' : 'b'
            default: return 'unknown'
        }
    }
    function airSpeedVelocity(bird){
        switch (bird.type){
            case 'E': return 35
            case 'A': return 40 - bird.counts
            case 'N': return bird.voltage / 10 + 10
            default: return null
        }
    }
    class Bird {
        constructor(name, type, counts, voltage){
            this.name = name
            this.type = type
            this.counts = counts
            this.voltage = voltage
            this.plumage = plumage(this)
        }
    }
    
    function plumages(birds){
        return new Map(birds.map(b => [b.name, b.plumage]))
    }
    function speeds(birds){
        return new Map(birds.map(b => [b.name, b.airSpeedVelocity]))
    }
    class Bird {
        constructor(name, type, counts, voltage){
            this.name = name
            this.type = type
            this.counts = counts
            this.voltage = voltage
        }
        get plumage(){
            return 'unknown'
        }
        get airSpeedVelocity(){
            return null
        }
    }
    class E extends Bird{
        get plumage(){
            return 'a'
        }
        get airSpeedVelocity(){
            return 35
        }
    }
    class A extends Bird{
        get plumage(){
            return this.counts > 2 ? 't' : 'a'
        }
        get airSpeedVelocity(){
            return 40 - this.counts
        }
    }
    class N extends Bird{
        get plumage(){
            this.voltage > 100 ? 's' : 'b'
        }
        get airSpeedVelocity(){
            return this.voltage / 10 + 10
        }
    }
    function createBird(...arg){
        switch (arg[1]){
            case 'E': return new E(...arg);
            case 'A': return new A(...arg);
            case 'N': return new N(...arg);
            default: return new Bird(...arg);
        }
    }
    
    function rating(voyage, history){
        const vpf = voyageProfitFactor(voyage, history)
        const vr = voyageRisk(voyage)
        const chr = captainHistoryRisk(voyage, history)
        if(vpf * 3 > (vr + chr * 2)) return 'A'
        return 'B'
    }
    function voyageRisk(voyage){
        let result = 1
        if(voyage.length > 4) result += 2
        if(voyage.length > 8) result += voyage.length - 8
        if(['china', 'east-indies'].includes(voyage.zone)) result += 4
        return Math.max(result, 0)
    }
    function captainHistoryRisk(voyage, history){
        let result = 1
        if(history.length < 5) result += 4
        result += history.filter(v => v.profit < 0).length
        if(voyage.zone === 'china' && hasChina(history)) result -= 2
        return Math.max(result, 0)
    }
    function hasChina(history) {
        return history.some(v => v.zone === 'china')
    }
    function voyageProfitFactor(voyage, history){
        let result = 2
        if(voyage.zone === 'china') result += 1
        if(voyage.zone === 'east-indies') result += 1
        if(voyage.zone === 'china' && hasChina(history)){
            result += 3
            if(history.length > 10) result += 1
            if(voyage.length > 12) result += 1
            if(voyage.length > 18) result -= 1
        }else{
            if(history.length > 8) result += 1
            if(voyage.length > 14) result -= 1
        }
        return result
    }
    const voyage = { zone: 'west-indies', length: 10 }
    const history = [
        { zone: 'east-indies', profit: 5 },
        { zone: 'west-indies', profit: 15 },
        { zone: 'china', profit: -2 },
        { zone: 'west-africa', profit: 7 }
    ]
    console.log(rating(voyage, history))
    
    function rating(voyage, history){
        return createRating(voyage, history).value
    }
    function createRating(voyage, history){
        if(voyage.zone === 'china' && history.some(v => v.zone === 'china')) return new ExperienceChinaRating(voyage, history)
        return new Rating(voyage, history)
    }
    class Rating {
        constructor(voyage, history){
            this.voyage = voyage
            this.history = history
        }
        get value(){
            const vpf = this.voyageProfitFactor
            const vr = this.voyageRisk
            const chr = this.captainHistoryRisk
            if(vpf * 3 > (vr + chr * 2)) return 'A'
            return 'B'
        }
        get voyageProfitFactor(){
            let result = 2
            if(this.voyage.zone === 'china') result += 1
            if(this.voyage.zone === 'east-indies') result += 1
            result += this.historyLengthFactor
            result += this.voyageLengthFactor
            return result
        }
        get voyageLengthFactor(){
            return this.voyage.length > 14 ? 1 : 0
        }
        get historyLengthFactor(){
            return this.history.length > 8 ? 1 : 0
        }
        get voyageRisk(){
            let result = 1
            if(this.voyage.length > 4) result += 2
            if(this.voyage.length > 8) result += this.voyage.length - 8
            if(['china', 'east-indies'].includes(this.voyage.zone)) result += 4
            return Math.max(result, 0)
        }
        get captainHistoryRisk(){
            let result = 1
            if(this.history.length < 5) result += 4
            result += this.history.filter(v => v.profit < 0).length
            return Math.max(result, 0)
        }
    }
    class ExperienceChinaRating extends Rating{
        get captainHistoryRisk(){
            const result = super.captainHistoryRisk - 2
            return result
        }
        get voyageProfitFactor(){
            return super.voyageProfitFactor + 3
        }
        get voyageLengthFactor(){
            let result = 0
            if(this.voyage.length > 12) result += 1
            if(this.voyage.length > 18) result -= 1
            return result
        }
        get historyLengthFactor(){
            return this.history.length > 10 ? 1 : 0
        }
    }
    const voyage = { zone: 'west-indies', length: 10 }
    const history = [
        { zone: 'east-indies', profit: 5 },
        { zone: 'west-indies', profit: 15 },
        { zone: 'china', profit: -2 },
        { zone: 'west-africa', profit: 7 }
    ]
    console.log(rating(voyage, history))
    
  5. 引入特例

    将多个相同的特殊情况取值收拢于一处

    function customerName(aCustomer){
        if(aCustomer.toString() === 'unknown') return 'occupant'
        return aCustomer.name
    }
    class Customer {
        toString() {
            return this.name || 'unknown'
        }
    }
    
    function customerName(aCustomer){
        return aCustomer.name
    }
    class Customer {
        toString() {
            return this.name || 'unknown'
        }
    }
    const customer = enrichCustomer(new Customer())
    function enrichCustomer(aCustomer){
        const unknownCustomer = {
            name: 'occupant',
        }
        if(aCustomer.toString() === 'unknown') return unknownCustomer
        return aCustomer
    }
    

    做法

    1. 给重构目标添加检查特例的属性,令其返回false
    2. 创建一个特例对象,其中只有检查特例的属性,返回true
    3. 对“与特例值做对比”提炼函数,并确保客户端使用这个新函数
    4. 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成
    5. 修改特例对比函数的主题,在其直接使用检查特例的属性
    6. 使用函数组合成类、函数组合成变换,把通用的特例处理逻辑搬移到新建的特例对象中
    7. 对特例对比函数使用内联函数,将其内联到仍然需要的地方
    function customerName(aCustomer){
        if(aCustomer.toString() === 'unknown') return 'occupant'
        return aCustomer.name
    }
    function customerPlan(aCustomer){
        if(aCustomer.toString() === 'unknown') return 'basic plan'
        return aCustomer.plan
    }
    class Customer {
        constructor(name, plan) {
            this.name = name
            this.plan = plan
        }
        toString() {
            return this.name || 'unknown'
        }
    }
    
    function customerName(aCustomer){
        return aCustomer.name
    }
    function customerPlan(aCustomer){
        return aCustomer.plan
    }
    class Customer {
        constructor(name, plan) {
            this.name = name
            this.plan = plan
        }
        toString() {
            return this.name || 'unknown'
        }
    }
    const customer = enrichCustomer(new Customer())
    function enrichCustomer(aCustomer){
        const unknownCustomer = {
            name: 'occupant',
            plan: 'basic plan'
        }
        if(aCustomer.toString() === 'unknown') return unknownCustomer
        return aCustomer
    }
    
  6. 引入断言

    通过断言告诉阅读者,程序执行到这一点是,对当前状态做了何种假设 做法

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

简化条件逻辑.png

重构API

  1. 将查询函数和修改函数分离

    区分处有副作用和无副作用的函数

    function setOffAlarms(){//setTimeout}
    function alertForMiscreant(people) {
        for (const p of people) {
            if(p === 'oo'){
                setOffAlarms()
                return p
            }
        }
        return ''
    }
    
    function alertForMiscreant(people) {
        if(findMiscreant(people) !== '') setOffAlarms()
    }
    function findMiscreant(people) {// find }
    

    做法

    1. 复制整个函数,将其作为一个查询命名
    2. 从新建的查询中去掉所有存在副作用的语句
    3. 查找调用函数的地方,替换
    const people = ['xx', 'yy', 'zz', 'oo', 'zzz', 'cc']
    function setOffAlarms(){
        setTimeout(() => {
            console.log('!!!')
        }, 100)
    }
    function alertForMiscreant(people) {
        for (const p of people) {
            if(p === 'oo'){
                setOffAlarms()
                return p
            }
            if(p === 'cc'){
                setOffAlarms()
                return p
            }
        }
        return ''
    }
    
    function alertForMiscreant(people) {
        if(findMiscreant(people) !== '') setOffAlarms()
    }
    function findMiscreant(people) {
        for (const p of people) {
            if(p === 'oo' || p === 'cc'){
                return p
            }
        }
        return ''
    }
    const found = findMiscreant(people)
    alertForMiscreant(people)
    
  2. 函数参数化

    如果两个函数逻辑相似,只是字面量值不同,则可以合成一个函数,以参数的形式传入不同的值消除重复

    function tenPercentRaise(salary){
        return salary * 1.1
    }
    function fivePercentRaise(salary){
        return salary * 1.05
    }
    
    function raise(salary, factor=0){
        return salary * (factor + 1)
    }
    function tenPercentRaise(salary){
        return raise(salary, 0.1)
    }
    function fivePercentRaise(salary){
        return raise(salary, 0.05)
    }
    

    做法

    1. 从一组相似的函数中选择一个
    2. 把需要作为参数传入的字面量添加到参数列表中
    3. 修改函数所有的调用处,更新参数传入
    4. 修改函数体,使其使用新的参数
    function baseCharge(usage){
        if(usage < 0) return 0
        return topBand(usage) * 0.07 + bottomBand(usage) * 0.03 + middleBand(usage) * 0.05
    }
    function bottomBand(usage){
        return Math.min(usage, 100)
    }
    function topBand(usage){
        return usage > 200 ? usage - 200 : 0
    }
    function middleBand(usage){
        return usage > 100 ? Math.min(usage, 200) - 100 : 0
    }
    
    function baseCharge(usage){
        if(usage < 0) return 0
        return withinBand(usage, 0, 100) * 0.03 
        + withinBand(usage, 100, 200) * 0.05
        + withinBand(usage, 200, Infinity) * 0.07 
    }
    function withinBand(usage, bottom, top){
        return usage > bottom ? Math.min(usage, top) - bottom : 0
    }
    
  3. 移除标记参数

    标记参数有时候会让人难以理解那些函数可以调用

    function deliveryDate(anOrder, isRush){
        if(isRush){
            ...
        }else{
            ...
        }
    }
    
    function rushDeliveryDate(anOrder){
        ...
    }
    function regularDeliveryDate(anOrder){
        ...
    }
    

    做法

    1. 针对参数的每一种可能值新建一个函数
    2. 对于字面量作为参数的函数调用者,改为调用新建的明确函数
    function deliveryDate(anOrder, isRush){
        if(isRush){
            let deliveryTime
            if(['MA', 'CT'].includes(anOrder.deliveryState)) deliveryTime = 1
            else if(['NY', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 2
            else deliveryTime = 3
            return deliveryTime
        }else{
            let deliveryTime
            if(['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) deliveryTime = 2
            else if(['ME', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 3
            else deliveryTime = 4
            return deliveryTime
        }
    }
    
    function rushDeliveryDate(anOrder){
        let deliveryTime
        if(['MA', 'CT'].includes(anOrder.deliveryState)) deliveryTime = 1
        else if(['NY', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 2
        else deliveryTime = 3
        return deliveryTime
    }
    function regularDeliveryDate(anOrder){
        let deliveryTime
        if(['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) deliveryTime = 2
        else if(['ME', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 3
        else deliveryTime = 4
        return deliveryTime
    }
    
  4. 保持对象完整

    把一个对象内的字段拆成几个变量,再传入函数中,不如保持完整传入函数中去解析

    const low = dayTempRange.low
    const high = dayTempRange.high
    function withinRange(low, high){...}
    
    function withinRange(aNumberRange){...}
    

    做法

    1. 新建一个空函数,给它期望的参数列表
    2. 再新函数中调用旧函数,并将新参数映射到旧的参数列表中
    3. 逐一修改旧函数的调用者,令其使用新函数
    4. 使用内联函数将旧函数内联到新函数中,给新函数改名后,同时修改所有的引用点
    const dayTempRange = { low: 10, high: 40 }
    const low = dayTempRange.low
    const high = dayTempRange.high
    if(withinRange(low, high)){
        console.log('123')
    }
    function withinRange(low, high){
        return low > 9 && 41 > high
    }
    
    if(withinRange(dayTempRange)){
        console.log('123')
    }
    function withinRange(aNumberRange){
        return aNumberRange.low > 9 && 41 > aNumberRange.high
    }
    
  5. 以查询取代参数

    频繁的传递参数会让函数看起来变得复杂

    class Order {
        get finalPrice(){
            return this.discountedPrice(basePrice, discount)
        }
        discountedPrice(price, discount){...}
    }
    
    class Order {
        get finalPrice(){
            return this.discountedPrice()
        }
        get basePrice() {...}
        get discount() {...}
        discountedPrice(){...}
    }
    

    做法

    1. 使用提炼函数将参数的计算过程提炼到一个独立的函数中
    2. 将函数体内引用该参数的地方改为调用新建的函数
    class Order {
        constructor(quantity, price){
            this.quantity = quantity;
            this.price = price;
        }
        get finalPrice(){
            const basePrice = this.price * this.quantity
            let discount 
            if(this.quantity > 100) discount = 2
            else  discount = 1
            return this.discountedPrice(basePrice, discount)
        }
        discountedPrice(price, discount){
            switch(discount){
                case 1: return price * 0.9
                case 2: return price * 0.8
            }
        }
    }
    
    class Order {
        constructor(quantity, price){
            this.quantity = quantity;
            this.price = price;
        }
        get finalPrice(){
            return this.discountedPrice()
        }
        get basePrice() { return this.price * this.quantity }
        get discount() { return this.quantity > 100 ? 2 : 1 }
        discountedPrice(){
            switch(this.discount){
                case 1: return this.basePrice * 0.9
                case 2: return this.basePrice * 0.8
            }
        }
    }
    
  6. 以参数取代查询

    当遇到引用复杂,需要调用者弄清参数意义时,需要用参数取代查询

    const thermostat = {}
    class HeatingPlan {
        get targetTemperature(){
            if(thermostat.t > this.max) return this.max
            else if(thermostat.t < this.min) return this.min
            return thermostat.t
        }
    }
    
    class HeatingPlan {
        targetTemperature(t){
            if(t > this.max) return this.max
            else if(t < this.min) return this.min
            return t
        }
    }
    

    做法

    1. 对执行查询操作对代码提炼变量,将其从函数体中剥离出来
    2. 现有的函数体不再执行查询操作
    const thermostat = {
        selectTemperature: 20
    }
    function setToHeat(){
        thermostat.selectTemperature += 10
    }
    function setToCool(){
        thermostat.selectTemperature -= 10
    }
    class HeatingPlan {
        constructor(max, min){
            this.max = max;
            this.min = min;
        }
        get targetTemperature(){
            if(thermostat.selectTemperature > this.max) return this.max
            else if(thermostat.selectTemperature < this.min) return this.min
            return thermostat.selectTemperature
        }
    }
    
    function setToHeat(){
        thermostat.selectTemperature += 10
    }
    function setToCool(){
        thermostat.selectTemperature -= 10
    }
    class HeatingPlan {
        constructor(max, min){
            this.max = max;
            this.min = min;
        }
        targetTemperature(selectTemperature){
            if(selectTemperature > this.max) return this.max
            else if(selectTemperature < this.min) return this.min
            return selectTemperature
        }
    }
    
  7. 移除设值函数

    如果不可变的数据,不暴露修改的方法

    class Person {
        get name(){return this._name }
        set name(arg){ return this._name = arg }
    }
    
    class Person {
        get name(){return this._name }
    }
    

    做法

    1. 使用私有字段的实现方式
    2. 移除设值函数
    class Person {
        constructor(name){
            this._name = name;
        }
        get name(){return this._name }
        set name(arg){ return this._name = arg }
    }
    
    class Person {
        constructor(name){
            this._name = name;
        }
        get name(){return this._name }
    }
    
  8. 以工厂函数取代构造函数

    构造函数在部分普通场合难以适用时,可以将其改为工厂函数

    class Employee{
        constructor(name, typeCode){
            this. _name = name
            this._typeCode = typeCode
        }
    }
    
    function createEngineer(name){
        return new Employee(name, 'E')
    }
    

    做法

    1. 新建一个工厂函数,让它地道用现有的构造函数
    2. 把调用构造函数的方法改为调用工厂函数
    3. 尽量修改构造函数的可见范围
    class Employee{
        constructor(name, typeCode){
            this. _name = name
            this._typeCode = typeCode
        }
        get name(){return this._name }
        get type(){return Employee.legalTypeCode[this._typeCode] }
        static get legalTypeCode(){
            return { 'E': 'Engineer', 'M': 'Manager', 'S': 'Salesman' }
        }
    }
    
    class Employee{
        constructor(name, typeCode){
            this. _name = name
            this._typeCode = typeCode
        }
        get name(){return this._name }
        get type(){return Employee.legalTypeCode[this._typeCode] }
        static get legalTypeCode(){
            return { 'E': 'Engineer', 'M': 'Manager', 'S': 'Salesman' }
        }
    }
    function createEngineer(name){
        return new Employee(name, 'E')
    }
    
  9. 以命令取代函数

    当函数里拥有许多复杂操作时,可以改成命令对象模式去处理

    function score() {
        ...
    }
    
    function score() {
        return new Score().execute()
    }
    class Scorer{
        execute(){
            this.scoreSmoking()
            this.stateWithLowCertification()
        }
        scoreSmoking(){}
        stateWithLowCertification(){}
    }
    

    做法

    1. 创建一个空类,包含其目标函数
    2. 给每个参数都创建一个字段
    function score(candidate, medicalExam, scoringGuide) {
        let result = 0
        let healthLevel = 0
        let highMedicalRiskFlag = false
        if(medicalExam.isSmoker){
            healthLevel += 10
            highMedicalRiskFlag = true
        }
        let certificationGrade = 'regular'
        if(scoringGuide.stateWithLowCertification(candidate.originState)){
            certificateGrade = 'low'
            result -= 5
        }
        result -= Math.max(healthLevel - 5, 0)
        return result
    }
    
    function score(candidate, medicalExam, scoringGuide) {
        return new Score(candidate, medicalExam, scoringGuide).execute()
    }
    class Scorer{
        constructor(candidate, medicalExam, scoringGuide){
            this._candidate = candidate
            this._medicalExam = medicalExam
            this._scoringGuide = scoringGuide
        }
        execute(){
            this._result = 0
            this._healthLevel = 0
            this._highMedicalRiskFlag = false
            this.scoreSmoking()
            this.stateWithLowCertification()
            this._result -= Math.max(this._healthLevel - 5, 0)
            return this._result
        }
        scoreSmoking(){
            if(this._medicalExam.isSmoker){
                this._healthLevel += 10
                this._highMedicalRiskFlag = true
            }
        }
        stateWithLowCertification(){
            this._certificationGrade = 'regular'
            if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){
                this._certificationGrade = 'low'
                this._result -= 5
            }
        }
    }
    
  10. 以函数取代命令

    使用函数去完成比较简单的任务

    class ChargeCalculator{
        get basePrice(){}
        get charge(){}
    }
    function charge(){
        return new ChargeCalculator().charge
    }
    
    function charge(){
        ...
    }
    

    做法

    1. 对命令对象在执行阶段用到的函数使用内联函数
    2. 将构造函数中的参数转移到执行函数
    3. 对所有的字段在执行函数中找到引用的地方,并改为参数
    class ChargeCalculator{
        constructor(customer, usage, provider){
            this.customer = customer
            this.usage = usage
            this.provider = provider
        }
        get basePrice(){
            return this.customer.baseRate * this.usage
        }
        get charge(){
            return this.basePrice + this.provider.connectionCharge
        }
    }
    function charge(customer, usage, provider){
        return new ChargeCalculator(customer, usage, provider).charge
    }
    
    function charge(customer, usage, provider){
        const basePrice = customer.baseRate * usage
        return basePrice + provider.connectionCharge
    }
    

重构API.png

处理继承关系

  1. 函数上移

    如果某个函数在各个子类的函数体中相同,则将函数上移

    class Party {
    }
    class Employee extends Party {
        annualCost(){ ... }
    }
    class Department extends Party {
        annualCost(){ ... }
    }
    
    class Party {
        annualCost(){ ... }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
    

    做法

    1. 检查待提升函数,确定完全一致
    2. 检查函数体内引用的所有函数调用和字段都能从超类调用
    3. 如果待提升签名不同,则都需要修改成超类的字段名
    4. 在超类中新建一个函数,将某个待提升函数的代码复制到超类中
    5. 移除待提升的函数
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
    }
    class Employee extends Party {
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Department extends Party {
        get totalAnnualCost(){ return this.monthlyCost * 12 }
    }
    
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
    
  2. 字段上移

    如果字段在各个子类中,字段可提升到超类

    class Party {
    }
    class Employee extends Party {
        get annualCost(){ ... }
    }
    class Department extends Party {
        get annualCost(){ ... }
    }
    
    class Party {
        get annualCost(){ ... }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
    

    做法

    1. 针对待提升的字段,检查他们的所有使用点,确定以同样的方式使用
    2. 如果字段名称不同,则先用变量改名
    3. 在超类中新增字段
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
    }
    class Employee extends Party {
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Department extends Party {
        get totalAnnualCost(){ return this.monthlyCost * 12 }
    }
    
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
    
  3. 构造函数本体上移

    子类中存在共同实例属性,则可以提升到超类

    class Party {
    }
    class Employee extends Party {
        constructor(){
            super()
            this.name = name
        }
    }
    class Department extends Party {
        constructor(){
            super()
            this.name = name
        }
    }
    
    class Party {
        constructor(name){
            this.name = name
        }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
    

    做法

    1. 如果超类不存在构造函数,直接定义一个。确保子类调用超类到构造函数
    2. 使用移动语句将子类中构造函数中的公共语句,移动到超类到构造函数调用的语句
    3. 逐一移除子类间的公共代码,将其提升到超类的构造函数中
    class Party {
    }
    class Employee extends Party {
        constructor(name, id, monthlyCost){
            super()
            this.name = name
            this.id = id
            this.monthlyCost = monthlyCost
        }
    }
    class Department extends Party {
        constructor(name, staff){
            super()
            this.name = name
            this.staff = staff
        }
    }
    
    class Party {
        constructor(name){
            this.name = name
        }
    }
    class Employee extends Party {
        constructor(name, id, monthlyCost){
            super(name)
            this.id = id
            this.monthlyCost = monthlyCost
        }
    }
    class Department extends Party {
        constructor(name, staff){
            super(name)
            this.staff = staff
        }
    }
    
  4. 函数下移

    如果某个函数只一个或者几个子类调用,则将函数下移到子类中

    class Party {
        annualCost(){...}
    }
    class Employee extends Party {}
    class Department extends Party {}
    
    class Party {}
    class Employee extends Party {
        annualCost(){...}
    }
    class Department extends Party {}
    

    做法

    1. 将超类的函数移动到目标子类中
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
        annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
        get cost(){
            return this.annualCost() * 10
        }
    }
    class Department extends Party {
    }
    
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
    }
    class Employee extends Party {
        annualCost(){ return this.monthlyCost * 12 }
        get cost(){
            return this.annualCost() * 10
        }
    }
    class Department extends Party {
    }
    
  5. 字段下移

    如果某个字段只被一个子类用到,就将其搬移到需要该字段到子类中

    class Party {
       get annualCost(){...}
    }
    class Employee extends Party {}
    class Department extends Party {}
    
    class Party {}
    class Employee extends Party {
        get annualCost(){...}
    }
    class Department extends Party {}
    

    做法

    1. 将超类中到字段移动到目标子类
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
        get cost(){
            return this.annualCost() * 10
        }
    }
    class Department extends Party {
    }
    
    class Party {
        constructor(monthlyCost){
            this.monthlyCost = monthlyCost
        }
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
        get cost(){
            return this.annualCost * 10
        }
    }
    class Department extends Party {
    }
    
  6. 以子类取代类型码

    用多种子类替代类型码判断

    class Employee {
        validateType(type){...}
    }
    
    class Employee {}
    class Engineer extends Employee{
        get type(){}
    }
    class Salesman extends Employee{
        get type(){}
    }
    function createEmployee(name, type){
        switch(type){
            case 'engineer': return new Engineer(name)
            case 'salesman': return new Salesman(name)
        }
    }
    

    做法

    1. 封装类型码字段
    2. 任选一个类型码取值,创建一个子类,复写类型码类的取值函数,令其返回类型码的字面量
    3. 创建一个选择器的逻辑,把类型码参数映射到新的子类
    4. 正对每个类型码都重复创建子类
    class Employee {
        constructor(name, type){
            this.validateType(type)
            this._name = name
            this._type = type
        }
        validateType(type){
            if(!['engineer', 'salesman', 'manager'].includes(type)){
                throw new Error('Invalid type')
            }
        }
        toString() { return `${this._name} (${this._type})`}
    }
    
    class Employee {
        constructor(name){
            this._name = name
        }
        toString() { return `${this._name} (${this.type})`}
    }
    class Engineer extends Employee{
        get type(){ return 'engineer' }
    }
    class Salesman extends Employee{
        get type(){ return 'salesman' }
    }
    class Manager extends Employee{
        get type(){ return 'manager' }
    }
    function createEmployee(name, type){
        switch(type){
            case 'engineer': return new Engineer(name)
            case 'salesman': return new Salesman(name)
            case 'manager': return new Manager(name)
            default: throw new Error('Invalid type')
        }
    }
    
  7. 移除子类

    如果子类的用处太小,则可以移除子类,替换成超类的一个字段

    class Person{
        get genderCode(){}
    }
    class Male extends Person{
        get genderCode(){}
    }
    class Female extends Person{
        get genderCode(){}
    }
    function isMale(aPerson){ return aPerson instanceof Male}
    
    class Person{
        constructor(){
            this.genderCode = genderCode || 'X'
        }
        get isMale(){ ... }
    }
    

    做法

    1. 把子类的构造函数包装到超类的工厂中
    2. 新建一个字段用于代表子类的类型
    3. 将原来针对子类的判断函数修改未使用新建类字段
    class Person{
        constructor(name){
            this.name = name
        }
        get genderCode(){ return 'X' }
    }
    class Male extends Person{
        get genderCode(){ return 'X' }
    }
    class Female extends Person{
        get genderCode(){ return 'F' }
    }
    function isMale(aPerson){ return aPerson instanceof Male}
    
    class Person{
        constructor(name, genderCode){
            this.name = name
            this.genderCode = genderCode || 'X'
        }
        get isMale(){ return this.genderCode === 'X' }
    }
    
  8. 提炼超类

    如果两个类在做相似的事情,可以继承机制把他们的相似之处提炼到超类中

    class Employee {}
    class Department {}
    
    class Party{
        constructor(name){
            this.name = name
        }
    }
    class Employee extends Party{}
    class Department extends Party{}
    

    做法

    1. 为原本的类新建一个超类
    2. 把共同元素梳理出来移动到超类中
    class Employee {
        constructor(name, id, monthlyCost){
            this.name = name
            this.id = id
            this.monthlyCost = monthlyCost
        }
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Department {
        constructor(name, staff){
            this.name = name
            this.staff = staff
        }
        get totalMonthlyCost(){
            return this.staff.map(e => e.monthlyCost).reduce((prev, cur) => prev + cur, 0)
        }
        get totalAnnualCost(){
            return this.totalMonthlyCost * 12
        }
    }
    
    class Party{
        constructor(name){
            this.name = name
        }
        get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party{
        constructor(name, id, monthlyCost){
            super(name)
            this.id = id
            this.monthlyCost = monthlyCost
        }
    }
    class Department extends Party{
        constructor(name, staff){
            super(name)
            this.staff = staff
        }
        get monthlyCost(){
            return this.staff.map(e => e.monthlyCost).reduce((prev, cur) => prev + cur, 0)
        }
    }
    
  9. 折叠继承体系

    当子类与父类没多大差别时,直接折叠子类与超类

    做法

    1. 选择要移除的类
    2. 将其移动到目标类中
  10. 以委托取代子类

    继承存在局限性,应该用组合与继承相结合的方式去改造

    class Booking{}
    class PremiumBooking extends Booking{}
    
    class Booking{
        _bePremium(){
            this._premium = new PremiumBookingDelegate(this, extra)
        }
    }
    class PremiumBookingDelegate{}
    function createPremiumBooking(extra){
        booking._bePremium(extra)
    }
    

    做法

    1. 如果构造函数由多个调用者,先用工厂函数包裹起来
    2. 创建一个空的委托累,这个类的构造函数应该接受所有子类特有的数据项,并以参数的形式接受一个指回超类的引用
    3. 在超类中添加一个字段,用于安放委托对象
    4. 修改子类的创建逻辑,使其初始化上述的委托字段,放入一个委托对象实例
    5. 选择一个子类的函数,使用搬移函数移动函数放入委托类
    6. 如果被搬移函数还在子类之外被调用,就把留在源类中的委托代码从子类移到超类,并委托代码前加上卫语句,检查委托对象存在。
    7. 如果没有,则直接移除代码,重复上述过程,直到所有函数都移动到委托类
    class Booking{
        constructor(show, date){
            this.show = show
            this.date = date
        }
        get hasTalkBack(){
            return this.show.talkBack && !this.isPeakDay
        }
        get basePrice(){
            let result = this.show.price
            if(this.isPeakDay) result *= 1.15
            return result
        }
    }
    class PremiumBooking extends Booking{
        constructor(show, date, extra){
            super(show, date)
            this.extra = extra
        }
        get hasTalkBack(){
            return this.show.talkBack
        }
        get basePrice(){
            return super.basePrice * this.extra.fee
        }
        get hasDinner(){
            return this.extra.dinner && !this.isPeakDay
        }
    }
    
    class Booking{
        constructor(show, date){
            this.show = show
            this.date = date
        }
        get hasTalkBack(){
            return this._premiumDelegate ? this._premiumDelegate.hasTalkBack : this.show.talkBack && !this.isPeakDay
        }
        get basePrice(){
            let result = this.show.price
            if(this.isPeakDay) result *= 1.15
            return this._premiumDelegate ? this._premiumDelegate.extendBasePrice(result) : result
        }
        get hasDinner(){
            return this._premiumDelegate ? this._premiumDelegate.hasDinner : false
        }
        _bePremium(extra){
            this._premiumDelegate = new PremiumBookingDelegate(this, extra)
        }
    }
    class PremiumBookingDelegate{
        constructor(hostBooking, extra){
            this.host = hostBooking
            this.extra = extra
        }
        get hasTalkBack(){
            return this.host.show.talkBack
        }
        get hasDinner(){
            return this.extra.dinner && !this.host.isPeakDay
        }
        extendBasePrice(base){
            return base * this.extra.fee
        }
    }
    function createBooking(show, date){
        return new Booking(show, date)
    }
    function createPremiumBooking(show, date, extra){
        let result = new Booking(show, date)
        result._bePremium(extra)
        return result
    }
    
  11. 以委托取代超类

    优先使用继承,当继承有问题,使用委托取代超类

    class CatalogItem{...}
    class Scroll extends CatalogItem{...}
    
    class CatalogItem{...}
    class Scroll{
        constructor(catalogId, catalog){
            this.catalogItem = catalog.get(catalogId) // new CatalogItem()
        }
    }
    

    做法

    1. 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的实例
    2. 针对超类每个函数,在子类中创建一个转发函数,将请求转发给委托引用
    3. 当所有的超类函数都被转发函数覆盖后,去除继承关系
    class CatalogItem{
        constructor(id, title, tags){
            this.id = id
            this.title = title
            this.tags = tags
        }
        hasTag(arg){ return this.tags.includes(arg) }
    }
    class Scroll extends CatalogItem{
        constructor(id, title, tags, dateLastCleaned){
            super(id, title, tags)
            this.lastCleaned = dateLastCleaned
        }
        needsCleaning(targetDate){ 
            const threshold = this.hasTag('revered') ? 700 : 1500
            return this.daysSinceLastCleaning(targetDate) > threshold
        }
        daysSinceLastCleaning(targetDate){ 
            return this.lastCleaned.until(targetDate)
        }
    }
    
    class CatalogItem{
        constructor(id, title, tags){
            this.id = id
            this.title = title
            this.tags = tags
        }
        hasTag(arg){ return this.tags.includes(arg) }
    }
    class Scroll{
        constructor(id, dateLastCleaned, catalogId, catalog){
            this.id = id
            this.catalogItem = catalog.get(catalogId)
            this.lastCleaned = dateLastCleaned
        }
        get id(){return this.catalogItem.id}
        get title(){return this.catalogItem.title}
        get tags(){return this.catalogItem.tags}
        hasTag(arg){ return this.catalogItem.hasTag(arg) }
        needsCleaning(targetDate){ 
            const threshold = this.hasTag('revered') ? 700 : 1500
            return this.daysSinceLastCleaning(targetDate) > threshold
        }
        daysSinceLastCleaning(targetDate){ 
            return this.lastCleaned.until(targetDate)
        }
    }
    

处理继承关系.png