最近在看《重构》一书,收获颇多。
重构,是有迹可循的。某些模式的代码,向我们昭示着重构的可能,书中作者称之为“代码的坏味道”。
一:重复的代码
在程序中出现两次以上的程序结构,应该进行重构:
1:在一个函数中出现重复的结构(如:多个if语句),就要考虑优化算法,使用更简洁、高效的写法。
2:同一个类中出现两次以上相同结构的代码,则提取出来作为一个函数。
3:两个互为兄弟的子类之间含相同代码,则先提取出来作为独立函数,然后上推到父类中。
4:两个互为兄弟的子类之间含相似代码,则先把相同部分提取作为函数1,不同部分作为函数2,然后在函数3中调用函数1、函数2,并把相同签名的函数上推到父类。【这样,由于子类各自重写了函数1、2,那么子类的函数3产生的结果就不同。此法名为“塑造模版函数”】
5:两个毫不相关的类中重复代码,则把重复代码提取到一个新的独立类中,然后在原来的类中通过新类进行调用。(例如:提取到工具类中)
二:过长函数
函数是功能的基本单元,一个函数一个尽量承担一个职责。如果一个函数中,做了多步工作,则应该进行重构:
1:我们在编写函数时,如果需要用注释来说明某一块代码时,则应该优先考虑把这部分代码作为一个函数来定义,并且通过函数名来说明其用途;在重构长函数时,这也是特征之一 —— “函数中哪里需要用注释说明其用途,则尝试提取出来作为独立函数,用函数名表达其用途”
2:对已有长函数进行分解:以单一功能为指标,提取每一部分代码进独立函数,最后原函数只需通过一系列调用语句,引用被提取出去的函数即可。
3:长函数中的临时变量:在原函数中,如有使用一些临时变量来接收某个函数调用结果的,则把这些临时变量直接用函数调用语句代替。
4:过长函数参数列:过长函数参数列表是函数调用出错的主要原因,可以新建一个参数类,把参数作为类成员,而调用时只需传递一个参数类对象即可。
5:有太多临时变量和参数不能替代或提取:使用函数对象法:新建一个新的类,在其中通过一个成员变量,引用原来的类;把原函数中用到的临时变量、参数,全部作为类成员字段;定义一个函数,通过使用成员字段,实现与原函数一样的功能;最后,将原函数改造为:新建函数功能类对象(this,原临时变量,参数 作为构造参数),调用功能函数,并把结果返回。
6:条件表达式改造:用于if语句的判断表达式往往是造成代码可读性下降的原因之一,某些判断语句用到的数据需要通读上下文才能理解。可以将条件语句提取出来,作为一个独立函数,通过函数名表达其判断内容,而函数內根据判断语句返回true 或 false 即可。
7:循环语句改造:循环语句块同样可以承担单一职责,因此可以提取出来作为独立函数,函数名表达其用途。
三:过大的类
类是面向对象而设计的,如果一个类中包含了太多与其本身无关的功能时,就要考虑重构:
1:将与本类无关的变量、函数,提取出来作为一个新类;
2:如果提取出来的变量、函数,适合作为一个子类,则使用提取子类法;
四:过长参数列表
1:如果参数是某个函数的调用结果,则直接使用函数调用语句作为参数;
2:如果某几个参数是属于某一个类的字段,则使用该类的对象作为参数,以保持对象的完整性;
3:剩下的杂乱无章、缺乏归属对象的参数,则为它们制造一个类,用以容纳这些参数,以参数类对象作为函数参数。
五:发散式变化【一个类受多种变化影响】
类的设计要有可扩展性,并且修改要容易进行。如果一个类需要引入不同变化时,对于每种变化,需要修改多个地方,则需要进行重构:
以变化为基本单元,对于某一种变化,所引起的修改,提取到一个新的类中,使得每种变化都分别对应于一个类而进行。
六:霰弹式变化【一个变化,影响多个类】
如果有一种变化,需要在多个不同类中进行修改,则需要进行重构:
根据这个变化所引起的修改,把它们全部提取到一个类中。
七:放错位置
如果一个类中,有函数对另一个类的内容调用颇多,则需要进行重构:
1:如果是一个函数过多调用另一个类的数据,则把该函数“搬移”到被那个类去;
2:如果是一个函数中部分代码过多调用另一个类中数据,则先把该部分代码提炼为独立函数,然后再搬移。
八:零散数据
如果有一些数据,常常在一起出现,例如:作为函数参数经常出现,则需要进行重构:
把这些零散的数据提炼到一个新的类中,以对象为单位来组织、使用这些数据。
九:替换基本数据类型
对于某些小规模、少字段的信息,虽然可以用几个基本数据类型来表达出来,但是这些零散数据一旦分开使用,就让人摸不清用途。所以需要重构:
1:用小对象组合零散数据:例如:带有数值与货币种类的money类、由start和end字段组成的range类等,类名清晰易懂。
2:不影响类行为,只用于表示某内容的类型码替换:
3:影响类行为的类型码替换:
十:switch语句块重构
将switch语句提炼到独立函数,尽量用多态来取代case判断语句的基本数据类型,最后把该函数上推到父类中去。
十一:平行继承
如果有两个继承体系,体系一的子类用途与体系二的子类用途相似,则需要重构:
在体系一中引用体系二子类实例,将原子类中的函数改写为调用体系二子类实例中函数。
十二:冗余类
如果有一些子类、独立类,没有承担明确的用途,那么就需要重构:
1:如果是没明确职责的子类,则折叠继承体系——把子类内容合并到父类去,取消子类。
2:如果是没明确职责的独立类,则将其内容合并到最频繁调用它的类中去。
十三:过长的调用链
如果存在一个类调用类2,类2调用类3...造成一长串调用关系,则需要重构:
1:隐藏委托关系:把 类1对象.类2对象字段.getXX() 形式的代码,在类1中进行封装,定义 get类2XX() 函数,在函数中通过类2对象.getXX()调用,并把结果返回。
十四:去掉中间人
1: 过度委托(一类中超过一半方法需要靠委托类来调用其他类方法):则隐藏中间人,去掉委托类,让调用者直接与负责的对象打交道。
2:如果只有少数函数需要委托类来调用其他类方法:则把这些函数放进调用端,直接用调用端.XX()调用即可。
3:如果委托类还有其他行为,则使用“继承取代委托”,继承实际负责类作为子类,从而扩展原对象的行为,又可以调用原对象的方法。
十五:访问私有
如果两个类之间彼此过多访问private内容,则需要重构:
1:把经常需要互相调用的内容提取到一个新的类中,在新类中光明正大地直接调用;
2:子类可以独立成类:则用委托取代继承。将子类作为一个独立的类来定义,在其中使用一个原父类对象进行内容调用。
十六:异曲同工的内容
如果有功能相同的函数、代码,则将它们进行统一。
十七:为原有类库添加函数
如果需要在原有的类库基础上添加新函数,可以使用继承原类库的功能类,在子类中添加新函数,在程序中使用自定义的子类即可。
十八:类中的字段封装
类中的字段应该保持私密性:
1:普通字段封装:将public改为private,并定义public修饰的setter/getter函数。
2:集合字段封装:对于集合类型的字段,定义public修饰的remove/add函数,在函数中通过集合字段本身调用removeadd操作。
3:只读字段:对于一些定义了之后就不再修改的字段,我们应该在类的构造函数中进行赋值,然后只提供getter函数,隐藏setter函数。
十九:拒绝继承
如果一个子类只需要父类中的少数内容,那就应该用委托取代继承,避免在子类中无谓地实现父类的接口。
二十:注释过多
注释可以增强代码可读性,但是注释也为我们指明了重构的方向。
1:需要用注释来说明一个代码块的用途时,尝试将其提取为独立函数,用函数名来表达用途;
2:需要注释来说明某种条件、状态时,使用断言。