读《重构,改善既有代码的设计》的一些小总结

593 阅读7分钟

何时重构

  1. 老是编写重复的代码时

  2. 代码的设计不能帮助我们轻松添加所需的功能的时候

  3. 修补bug过程中发现代码可读性不好,可以运用重构改善代码

  4. 程序难以修改的四个原因:

    ①.难以阅读的程序;

    ②.逻辑重复的程序;

    ③.添加新行为需要修改已有代码的程序,

    ④.带复杂条件逻辑的程序

  5. 重构可以对软件内部做很多修改,但必须对软件可观察的外部行为只造成很小的变化。与之形成对比的是性能优化,和重构一样,性能优化通常不会改变组件的行为,只会改变其内部结构,但是出发点不同,性能优化往往使代码较难理解。

坏代码的味道

  1. 重复代码:多个地方出现相同程序结构
  2. 过长函数:函数内存在大量的参数和临时变量,阻碍代码阅读,让代码难以理解,尽量除去一些临时变量,它们会导致大量参数被传来传去,很容易跟丢,但这可能需要在性能上付出代价
  3. 字段函数命名:函数名称要体现函数做什么,不是怎么做。同时变量命名应该尽量清晰有含义。
  4. 过大的类:一个函数内部出现太多的实例变量和操作逻辑,一方面会有重复代码问题,另一方看起来会很混乱
  5. 过长参数列:太长的参数列会造成难以理解,不易使用,一旦需要更多的数据,就不得修改它,可以给函数传入一个对象
  6. 霰弹式修改:如果遇到某种变化,必须在不同的函数中做修改,这样不但很难找到它们,也很容易遗忘某个地方的修改
  7. 依恋情节:函数对某个类的兴趣高过对自己所处类的兴趣,通常是数据。比如某个函数为了计算某个值,从另外一个对象身上调用很多取值函数,可以把函数移到它该去的地方。原则是:判断那个类拥有最多被此函数使用的数据,就把这个函数和那些数据放在一起
  8. 数据泥团:在很多地方看到相同的几项数据,相同的字段,可以将他们提炼到一个独立的对象中
  9. 过度耦合的消息链:如果一个用户向一个对象请求另一个对象,然后后者再请求另一个对象,这样一旦对象间的关系发生变化,就得做出修改

重构原则

重新组织你的函数

  1. Extract Method(提炼函数)

    你有一段代码可以被组织在一起并独立出来:将这段代码放进一个独立的函数中,并让函数名称解释该函数的用途

  2. Inline Temp(将临时变量内联化)

    有个临时变量,只被一个简单的表达式赋值一次,将所有对改变量的引用,替换为对它赋值的那个表达式自身

  3. Replace Temp With Query(以查询取代临时变量)

    以一个临时变量保存某一表达式的运算结果。将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可以被其他函数使用。局部变量会使代码难以被提炼,所以尽可能将它们替换为查询式。

     //举例
     setTreeGrid: function (data) {
         let tabPanel = this.getTabPanel(),
             testData = {children: data.test},
             pageData = {children: data.page},
             testGrid = tabPanel.getTestGrid(),
             pageGrid = tabPanel.getPageTreeGrid(),
             pageHeaderCt = pageGrid.columnManager.headerCt,
             editColumn = pageHeaderCt.getComponent('edit'),
             readColumn = pageHeaderCt.getComponent('read'),
             selectColumn = testGrid.columnManager.headerCt.getComponent('selectcheckbox'),
             columnData = {select: selectColumn, edit: editColumn, read: readColumn},
             testForm = tabPanel.getTestForm();
         testForm.setValue(data);
         this.setColumnDisable(columnData, data);
         testGrid.loadData(testData);
         pageGrid.loadData(pageData);
         tabPanel.setActiveTab('test');
     },
    
     //结合1、2、3修改后
     setTreeGrid: function (data) {
         this.getTabPanel().getTestForm().setValue(data);
         this.setColumnDisable(this.getColumnData(), data);
         this.loadTreeGridData(data);
         this.getTabPanel().setActiveTab('test');
     },
     loadTreeGridData (data) {
         this.getTabPanel().testGrid().loadData({children: data.test});
         this.getTabPanel().pageGrid().loadData({children: data.page});
     }
     getColumnData () {
         let testHeaderCt = this.getTabPanel().getTestGrid().columnManager.headerCt,
             pageHeaderCt = this.getTabPanel().getPageTreeGrid().columnManager.headerCt;
    
         return {
             select: testHeaderCt.getComponent('selectcheckbox'),
             edit: pageHeaderCt.getComponent('edit'), 
             read: pageHeaderCt.getComponent('read')  
         }
     }
    
  4. Introduce Explaining Variable (引入解释变量)

    程序中有一个复杂的表达式,将该表达式或者其中的一部分放进一个临时变量,并用变量名解释其用途。

  5. Split Temporary Variable(分解临时变量)

    函数中有个临时变量被赋值超过一次,它既不是循环变量,也不用于收集运算结果,针对每次赋值,创建一个独立的,对应的临时变量,如果一个变量承担多个责任会让代码阅读者糊涂。

  6. Remove Assignments to Parameters(移除对参数的赋值)

    代码对一个参数赋值,以一个临时变量取代该参数的位置

重新组织你的数据

  1. Self Encapsulate Field(自封装字段)

    直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。为这个字段建立取值/设值函数,并且只以这些函数来访问字段。还有个好处就是可以在子类中复写取值设值函数改变数据的获取方式。

     //举例
     _createItems: function () {
         var baseParams = {
                 type: ['a'],
                 level: 2,
                 tag: []
                 search: ''
             };
         this.listMenuBaseParams = Object.assign({
             tab: 'test',
             id: this.getId()
         }, baseParams);
         this.gridBaseParams = lodash.cloneDeep(baseParams);
         this.overViewPanelBaseParams = lodash.cloneDeep(baseParams);
     }
    
     //修改
     _createItems: function () {
         this.setListMenuBaseParams({
             tab: 'test',
             id: this.getId()
         });
         this.setGridBaseParams(this.getBaseParams());
         this.setOverViewPanelBaseParams(this.getBaseParams());
         ...
     },
     getBaseParams () {
         return {
             type: ['a'],
             level: 2,
             tag: []
             search: '' 
         };
     },
     setListMenuBaseParams (params) {
         this.listMenuBaseParams = Object.assign(params, this.getBaseParams());
     } 
    
  2. Replace Array with Object(以对象取代数组)

    有一个数组 ,其中的元素各自代表不同的东西。以对象替换数组。对于数组中的每个元素,以一个字段来表示。

     let person = ['zhangsan', 18, 'man'];
     console.log(person[0] + person[1] + person[2]);
    
     //修改
     let person = {
         name: 'zhangsan',
         age: 18,
         sex: 'man'
     }
     console.log(person.name + person.age + person.sex);
    

简化条件表达式

  1. Decompose Conditional(分解条件式)

    有一个复杂的条件(if-then-else)语句。从if、then、else三个段落中分别提炼出独立函数。

  2. Co olidate Duplicate Conditional Fragments (合并重复的条件执行片段)

    在条件表达式的每个分支上有着相同的一段代码。将这段重复代码搬移到条件表达式之外。

  3. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件式)

    函数中的条件逻辑使人难以看清正常的分支执行路径。使用卫语句表现所有特殊情况。所 谓卫语句,如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。

     //例子
     renderer: (v, meta, rs) => {
         let curType = (rs.data || {}).type,
             isTypeA = this.isTypeA(curType);
    
         if (Array.isArray(v) && !v.length && curType === 'type_b' && !isTypeA) {
             return 'xxx';
         } else if (Array.isArray(v) && v.length && !isTypeA) {
             return 'yyy';
         }
         return '-';
    
     }
    
     //修改
      renderer: (v, meta, rs) => {
         let curType = (rs.data || {}).type;
    
         if (this.isTypeA(curType) || !Array.isArray(v)) {
             return '-';
         }
         if (v.length) {
             return 'yyy;
         } else {
             return curType ==='type_b' ? 'xxx' : '-';
         }
     }
    
  4. Remove Control Flag(移除控制标记)

    在一系列布尔表达式中,某个变量带有控制标记的作用,以break或者return取代控制标记

     //根据两点获取他们之间连线的状态
     getLineStatus: function (src, target) {
         var failLine = this.getParams().failLine || [],
             length = failLine.length,
             status;
         
         for (let i = 0; i < length; i++) {
             if (failLine[i][src.linkSelf] === target || failLine[i][target] === src.linkSelf) {
                 status = true;
                 break;
             }
         }
         return !status;
     },
    

简化函数调用

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

    某个函数既返回对象状态值,又修改对象状态,将修改和查询分离

  2. Preserve Whole Object(保持对象完整)

    从某个对象中取出若干值,将它们作为某一次函数调用时的参数。改为传递整个对象。传部分值的问题是如果要新增数据项,必须修改所有调用

     //例子
     initCube () {
         this.drawCube(this.getBaseOpt().length, this.getBaseOpt().width, this.getBaseOpt().height)
     }
    
     //改为
     initCube () {
         this.drawCube(this.getBaseOpt())
     }
    
  3. Replace Parameter with Method(以函数取代参数)

    一个对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数。让参数接受者去除该参数,并直接调用前一个函数

     //例子
     getPrice () {
         let basePrice = this.getBasePrice();
         let discountLevel;
         if (quantity > 100) {
             discountLevel = 2;
         } else {
             discountLevel = 1;
         }
         let finalPrice = discountdPrice(basePrice, discountLevel);
         return finalPrice;
     }
     getBasePrice () {
         return quantity * itemPrice;
     }
     discontedPrice (basePrice, discountLevel) {
         if (discountLevel === 2) {
             return basePrice *0.1
         } else {
             return basePrice * 0.05
         }
     }
    
     //修改
     getPrice () {
         this.discountedPrice();
     }
     getDiscountLevel () {
         if (quantity > 100) {
             return 2;
         } else {
             return 1;
         }
     }
     discountedPrice () {
         if (this.getDiscountLevel() === 2) {
             return this.getBasePrice() * 0.1;
         } else {
             return this.getBasePrice *0.05;
         }
     }
     getBasePrice () {
         return quantity * itemPrice;
     }
    
  4. Introduce Parameter Object(引入参数对象)

    某些参数总是很自然的同时出现,以一个对象取代这些参数,也更好扩展

     //例子
     formatData (start, end, pattern)
    
     //修改
     formatTime ({start: xxx, end: xxx, pattern: xxx});
    

处理概括关系

  1. Pull Up/Down Field(字段上/下移)

    成员变量上移。两个子类拥有相同的字段,将该字段移至基类。 基类中某个字段只与部分(而非全部)子类有关,将这个字段移到相关的那些子类中去。

  2. Pull Up/Down Method(函数上/下移)

    成员函数上移。两个子类拥有相同的函数(产生完全相同的结果),将该函数移至基类。 基类中某个函数只与部分(而非全部)子类有关,将这个函数移到相关的那些子类中去。