【重构】重构技术汇总

重构: 

【名词】对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

【动词】使用一系列重构手法,在不改变软件观察行为的前提下,调整其结构

【意义】重构使软件更容易理解,填补“想要他做什么”,和“准确说出我所要的”之间的间隙

【范围】在不同的领域中有着不同的重构手法,例如多线程环境和单线程环境,函数式编程和命令式编程语言等,更多要求的是你自己本身具有一定的创造力,发现适合你的重构技巧。

理念篇:

  代码是随着需求的易变性,不同的抽象的模型,从而具有变化性和自生长性的,把软件工程师比喻为城市规划师,对软件的规划就要有了,那么对软件的演进也是要有;

  软件系统管理的最佳手段,必然是重构了,我们知道治理代码的时候,一般想到的是坏味道代码的治理,也就会用到重构手段去把坏味道代码改为好味道代码;但其实对于变化来说,意味着好的代码也需要重构,只是好的代码会更容易重构;

  1. 如果你想给程序添加一个特性但程序因没有良好的结构而难以进行,那么先重构程序使得其容易添加特性然后再添加特性;
  2. 重构前先保证是否有一套测试框架,这些框架要有一定的自己检测能力;
  3. 重构技术是以微小的步伐修改程序,如果你发现错误可以很容易发现它;
  4. 重构要经常切换两顶帽子,尽量不要同时进行,要么加新特性,要么调整结构;
  5. 当你感觉需要撰写注释的时候,请先尝试重构,试着让所有注释都变得多余;

重构时机:

  什么时候应该重构呢,当闻到代码“坏味道”的时候,以下是味道:

  1. 神秘命名;
  2. 重复代码;
  3. 过长函数;
  4. 过长参数列表;
  5. 过长的类
  6. 全局数据:可以通过建立一个类,然后把行为搬迁过去;
  7. 可变数据;
  8. 发散式变化:不同的变化都在改同一个地方;
  9. 霰弹时修改:同一个变化修改不提的地方;
  10. 依恋情结:某函数和外部的依赖远多于在内部的依赖,那么把这份依恋转移过去;
  11. 数据泥团:请把相关数据集合在一起;
  12. 重复的switch;
  13. 沉赘的元素:多此一举的类啊,这样;
  14. 过度设计;
  15. 被拒绝的遗赠:子类继承了多余的东西,可以尝试弄个兄弟类类装;

重构技术篇:

  Extract Method 抽离函数出来,其实和整洁代码的理念差不多;让Method在同一层次中很重要哦;这点我又想起微服务了;什么样才是足够小呢?

  • 定义:有一段代码可以组织在一起被单独提取出来;
  • 意义:什么时候才要提炼函数呢?将意图和实现分离,这点也是用意图取代注释的体验
  • 名称:很多小型函数有很多好处,而且只有在你为小型函数真正命好名的时候才会凸显其作用,如果你想不出一个好名称,那就不要提
  • 长度:函数长度其实不是问题,问题在于函数名称和函数语义之间的距离;既然函数名称比函数体还长也没关系。另外,如果函数体本身清晰易读,就没有必要提取了,关键在于意图;
  • 难点:局部变量。

  Inline Method 内联函数,一个和Extract Method相反的用法;

  • 通常没必要的间接层只会是累赘;
  • 如果小函数划分混乱的时候,也可以应该方法先合并为大函数,再拆分小函数;

  Replace Temp With Query ,临时变量坏处多,用查询改变临时变量是好的,可能会增加性能开销,但不要担心,优化的时候才是你需要担心的,阻塞才是要害怕的;

  • 定义:将一个表达式提炼到一个独立的函数中,将这个临时变量的所有引用点替换为对新函数的调用,此后,新函数就可被其他函数使用。
  • 查询:查询就是赋值给临时变量的那个表达式,该表达式可以利用Extract Method被提炼出一个函数;
  • 问题:临时变量的问题在于,他们只是暂时的,而且只能在所属函数中使用,它会驱使你写长函数,而且让代码不清晰,不整洁。

  Extract Variable 提炼变量:引入解析性变量;

  Inline Variable 内联变量:去除没意义变量;

  Encapsulate Variable 封装变量:把变量或者变量集合的访问封装到函数中,控制访问,也可以用作发挥重构调整 数据元素 的途径;

  Introduce Parameter Object 引入参数对象,一组数据总是结伴同行,出没于一个又个函数,我们称之为数据泥团,这个时候应该把她们封装为类,这样可以减少函数入参,但这个的意义在于,我们通常可以催生更深层次的改变——我们可以重组程序的行为来使用这些数据结构,我们可能会创新一些新的函数来组织这些行为,通常它们只是一些共用的函数;这个行为会提升代码的概念景图,将这些数据提升为新的抽象概念;

  Combine Function into Class 如果几个函数一直在操作相同的数据,那么可以把这些函数和数据组合成类,如果说把数据泥团组合成类很有意义,那么函数组合成类一样很有意义,通过几个明确的函数组合类后,我们或许能发现更多的隐晦的函数组合到这个新类中;这个手法和《Combine Function Into Transform》函数组合成变换有时候可以替换,关键是前者包含数据,后者是专门包含做数据转换,见《重构2》149页。

  • 几个函数一直在操作相同的数据,那么可以把这些函数和数据组合成类
  • 类无需做参数传递,而是操作对象内部属性,封装性高
  • 如果操作的参数多,可以先做引入参数记录手法

  Combine Function into Transform 如果需要对参数进行计算产生派生参数,那么这些计算逻辑的函数可以组合成类,然后让该类接受rawParmas,产生新的enrich后的aParams;这个过程raw不可变,类函数会对raw做深拷贝;

  Encapsulate Record 封装记录,把对记录的访问用函数封装起来,类也行;

  Encapsulate Collection 把集合封装起来,只暴露对集合的元素的访问控制粒度,如果要提供整个集合,最好提供克隆本;

  Replace Primite with Object 把基本类型封装为类对象,然后搬迁更多行为进来,你会发现很有用,其实就是基本类型已经产生了抽象意义了;

  Extract Class 提炼类,很明显我们的代码都是在一个类中是可能随着需求的增长慢慢变的冗长不易理解的,所以这时候我们就需要提炼一个类出来了;

  Inline Class 内联类,和提炼类相反,当一个类不在承担足够的职责,就应该内联进去;还有一种情况是,如果你有两个类,想重整他们的职责,也可以把他们都内联掉,然后在Extract Class分开;

  Hide Delegate 隐藏委托关系,A要调用B获取C,再得到D,那么可以直接让A调用B得到D,把委托隐藏意味着封装,减少依赖,对于变化比较容易适应;但如果A要通过B获取C,从C中得到D、E、F等等,那么就不需要隐藏了,因为反而会让B承受了更多不应该的职责,反而让变化更难进行;

  Remove Middle Man 移除中间人,其实就是隐藏委托关系的反向手法,如果中间人帮忙做的事情太多,那么就不中间人了;

  Split Loop 拆分循环,你常常能够看到一些身兼多职的循环,它们一次可以做两三件事,不为别的只为循环一次;以后你在修改循环的时候就不得不理解多件事,而且难以拆分出小函数。

  Split Variable 拆分变量,也叫变量单一职责,除了循环变量和结果收集变量外,其他变量不应该被复值多次,应该每次做一个解析性变量出来。

  Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式,if-else的分支如果是同一个等级,同等表达主逻辑,那么不需要用卫语句,如果某个条件特别罕见,或者属于异常情况,那么立马用卫语句返回;

  重构前:

function getPayAmount(){
  let result;
  if(isDead)
     resule = deadAmount();
  else{
     if(isSepareted)
         result=separetedAmount();
     else{
         if(isRetired)
            result = retiredAmout();
         else{
            result = normalPayAmount();
         }
     }
  }           
  return result;
   
}

  重构后:

function getPayAmount(){
   if (isDead) return deadAmount();
   if (isSepareted) return separeteAmount();
   if (isRetired) return retiredAmount();
   return normalPayAmount();  
}

  Replace Type Code with State/Strategy 如果一个类有状态类型,最好转换为状态类、或者策略类;把行为封装进去;

  • 如何去掉switch语句,switch一般是根据状态而选择行为,有必要的时候,去掉它,状态机中就是如此,但比较简单的可以暂时先不急着下手;其实就是用好状态模式提供的多态能力;

  Introduce Explaining Variable 引入解析性变量;将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解析表达式用途。该方法和Extract Method有相同的效果,如果是量小的情况下尽量用后者,而很多临时变量或者临时变量要用多次为提高性能,我们也应该引入解析性变量;

  Split Temporary Variable,分解临时变量,临时变量有各种不同用途,其中某些用途会很自然导致临时变量被多次复赋值,如“循环变量”、“结果收集变量”就是两个例子;分解临时变量就是要注意,一个变量不应该承担一个以上的责任。

  Remove Assignments to Parameters,移除对参数的赋值;一般参数职责为:被传入的某些东西;

  Separate Query From Modifier 将查询函数和修改函数分离,函数具有“有副作用”和“没有副作用”之分,我们提前没有副作用的函数,如果一个函数把修改和查询混合在一起,请拆开它;

  Remove Flag Argument 移除标记参数,一个函数里面的逻辑,是用某个参数作为判断调用不同逻辑,那么这个参数就叫标记参数,这会让方法隐含很多不知道的知识,因为看代码无法知道到底有什么标记参数。

  • 这其实也是减少参数的方式之一,

核心手法:以多态取代条件表达式

  Replace Candition with Polymorphism ,复杂的条件表达式是编程中最难理解的东西之一,很多时候他们可以拆分为不同的场景(或者叫高阶用例),从而拆分复杂的逻辑条件;使用类和多态可以使得拆分更清晰;

  做法

  • 如果现有的类不具有多态行为,先用工厂函数创建之,另工厂函数返回适当的对象实例
  • 在调用方法代码中使用工厂函数获取对象实例
  • 将带有逻辑表示分支的代码转移到超类中
    • 如果条件表达式还没提炼至独立函数,就对其使用提炼函数,使得分段
  • 任意选一个子类,在其中建立一个函数,使之覆盖超类中容纳条件表达式的那个函数;将与该子类相关的条件表达式分支复制到新函数中
  • 重复以上过程知道分支处理完毕
  • 在超类中保持默认的逻辑,或者声明为Abstrace 

核心手法:引入特例对象

  这个手法出自引入null对象,一种常见的重复代码是这样的:一个数据结构的使用者,都在检查某个特殊的值,并且当这个值出现时所作的处理也都相同,那么就可以把他们收拢在一起。

  • 其中一个好的处理方式,就是引入特例对象
  • 第二就是把数据结构封装为一个对象

  可以有用的关联重构手法有:《提取函数》《函数组合成类》,或者《函数组合成变换》。

  • 提取函数:在散落各处的检查特例的代码,都提取为函数。

核心精华:重构就是调整元素

  重构的作用,就是调整程序中的元素,函数的调整相对容易一些,因为函数只有一种用法,那就是调用,在改名或者搬移函数的过程中,总是可以比较容易地保存旧函数作为转发函数,从而简化重构过程,但调整程序数据就要麻烦得多,因为没有办法设计这种转发机制,如果我把数据搬走,我就必须修改所有对它的引用,这也是为啥全局变量是大麻烦的原因;

  但有这么一个好办法,那就是先把数据封装起来(Encapsulate Variable),就是通过函数来访问数据,然后对数据的所有访问改为对该函数调用,就把重新组织数据转化为重新组织函数了;

核心精华:模块化、上下文环境与元素重整

  (摘-重构2-198页)任何函数都必须具备上下文环境才能存活,该环境可能是全局的,但它更可能是某种模块所提供;对于面向对象的程序设计语言,类是最主要的模块化手段;而对于函数式语言而言,通过函数嵌套,外层函数也能是内层函数的上下文环境;不同语言的模块化机制不同,但他们的共同点都是为函数提供了一个可以赖以存活的上下文环境;例如Java的包访问权限,其实也是证明了包,其实也是一个上下文环境;

  模块化是优秀软件设计的核心所在,模块不仅仅指的是包、还可以是文件、类、作用域、函数等,好的模块化能够让我在修改程序时只需理解程序的一小部分,为了设计出高度模块化的程序,我得保证互相关联的软件要素都能集中到一起,并确保块与块之间的联系易于查找、直观易懂。同时,我对于模块设计的理解并不是一成不变的,随着我对问题的理解和加深,我会知道哪些软件要素如何组织最为恰当。要将这些理解反映到代码上,就得不断地搬迁这些元素;而重构,就是调整元素;

 

posted @ 2019-01-19 02:04  饭小胖  阅读(528)  评论(0编辑  收藏  举报