项目在不断演进过程中,代码不停地在堆砌。如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。造成这样的原因往往有以下几点:
编码之前缺乏有效的设计成本上的考虑,在原功能堆砌式编程缺乏有效代码质量监督机制。对于此类问题,业界已有有很好的解决思路:通过持续不断的重构将代码中的“坏味道”清除掉
SOLID原则
单一职责原则 一个类只负责完成一个职责或者功能,不要存在多于一种导致类变更的原因。
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
开放-关闭原则 添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
里氏替换原则 子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
子类可以扩展父类的功能,但不能改变父类原有的功能
父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。
接口隔离原则 调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
依赖反转原则 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
代码坏味道
(1)重复代码
(2)过长的函数或命名
(3)过大的类
定义:由于属性未分组和职责不单一而包含过多属性、方法和代码行的类
影响:随着属性、方法和代码行数的不断增加,重复代码接踵而至,最终走向混乱
改进目标:拆分过大的类,确保类职责单一
案例:https://bbs.huaweicloud.com/blogs/350056
(4)过长的参数列 (5)发散式变化
含义:
由于代码的各种修改或扩展,每次都要修改某个类
坏处:
可扩展性差,某个类干的事过多
案例:https://bbs.huaweicloud.com/blogs/349647
(6)弹式修改
(7)依恋情结
定义:依恋情节/特性依恋: 一个函数跟另一个模块中的函数或数据交流格外频繁,远胜于在自己所处模块内部的交流
影响:可读性、可维护性低:调用另一模块功能时往往需要打一套组合拳才能完成,需要知道过多的细节;往往会伴随有“内幕交易、重复代码、霰弹式修改……”
改进目标:将函数搬移到对应的类,解除跨模块的过多交流
方法:提炼函数、搬移函数
注:策略模式、访问者模式往往会带来依恋情节,这不是说这两个模式不可取。我们需要理解:从根本上,我们消除“依恋情节”和应用这些设计模式都是为了把一起变化的东西放到一块儿。
(8)数据泥团
定义:总是成块出现的相同数据项,包括多个类中相同的字段、多个方法签名中相同的参数等
影响:成块出现的重复参数过多,影响阅读和理解,难维护
改进目标:减少相同的字段及入参,缩短入参列,简化函数调用
方法:提炼类;引入参数对象;保持对象完整性
(9)基本类型偏执
定义: 对于具有意义的业务概念如钱、坐标、范围等,不愿意进行建模,而是使用基本数据类型进行表示
影响: 暴露较多细节,代码内聚性差,可读性差
改进目标: 消除基本类型,提升代码可修改性、内聚性、可读性
方法:
1、对象取代基本类型
2、子类取代类型
3、多态取代条件表达式
4、提炼类
5、引入参数对象
案例:https://bbs.huaweicloud.com/blogs/350040
(10)重复的Switch语句
定义:在不同的地方反复使用同样的switch逻辑
影响:影响可维护性:每当需要增加一个选择分支时,必须找到所有switch,并逐一更新
改进目标:消除重复switch,提升代码可修改/可扩展能力
方法:多态取代条件表达式
案例:https://bbs.huaweicloud.com/blogs/350046
(11)平行继承体系
平行继承体系其实是散弹式修改(Shotgun Surgery)的特殊情况。在这种情况下,每当你为某个类增加1个子类,必须也为另一个类相应增加1个子类。如果你发现某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同,便是闻到了这种坏味道。
消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。
如果再接再厉运用 Move Method (搬移函数)和Move Field (搬移字段),就可以将引用端的继承体系取消。
(12)冗余类
定义:冗赘的元素主要包括由于过度设计或在代码演进过程中,产生的冗余、废弃[1]或不足以独立承担其职责的类、方法、变量等
影响:代码不简洁,存在多余的元素,造成在维护时无用修改,难以维护,影响代码的可读性。
改进目标:消除冗赘的程序元素,提高代码的可读性、可维护性。
方法
•内联函数或内联类
•如果这个类处于一个继承体系中,可以使用折叠继承体系
•安全删除冗余元素。
(13)夸夸其谈的通用性
定义:过度的考虑程序的通用性
影响:过度的设计导致代码不易理解和维护
改进目标:删除过度设计的代码,
(14)令人迷惑的暂时值域
有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初设置目的,会让你发疯。 通常,临时字段是在某一算法需要大量输入时而创建。因此,为了避免函数有过多参数,程序员决定在类中创建这些数据的临时字段。这些临时字段仅仅在算法中使用,其他时候却毫无用处。 这种代码不好理解。你期望查看对象字段的数据,但是出于某种原因,它们总是为空
(15)过度耦合的消息链
消息链的形式类似于:obj.getA().getB().getC()
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。实际代码中你看到的可能是一长串 getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航紧密耦合。一旦对象间关系发生任何变化,客户端就不得不做出相应的修改
(16)中间转手人
问题原因
对象的基本特征之一就是封装:对外部世界隐藏其内部细节。封装往往伴随委托。但是人们可能过度运用委托。比如,你也许会看到一个类的大部分有用工作都委托给了其他类,类本身成了一个空壳,除了委托之外不做任何事情
解决方法
应该运用 移除中间人(Remove Middle Man),直接和真正负责的对象打交道
收益
减少笨重的代码
(17)狎昵关系
一个类大量使用另一个类的内部字段和方法。
(18)异曲同工的类
重复造轮子, 两个类中有着不同的函数,却在做着同一件事。
典型场景:
位操作 ani.bitmap.h bitops/ani_bitpointer.h
内存分配器 CodMemAlloc、MemAllocator
宏 CEIL、cosGetBitmapWords
(19)纯粹的数据类
指的是只包含字段和访问它们的 getter 和 setter 函数的类。这些仅仅是供其他类使用的数据容器。这些类不包含任何附加功能,并且不能对自己拥有的数据进行独立操作
(20)被拒绝的遗赠
定义: 被拒绝的遗赠是指:对于某个子类,它只想继承基类的部分函数和数据,不需要基类提供的全部内容,这些不需要的内容就成为了子类的负担
影响: 这种坏味道通常影响并不大,但如果子类拒绝实现部分接口或者基类的方法只适用于某个子类特定的方法,就会对可维护、可扩展性等造成较大影响。
改进目标: 改进不合理的继承体系,使代码结构清晰、可控。
方法:
•函数/字段下移,让超类只持有子类共享的东西[1]
•以委托取代超类/子类
案例:https://bbs.huaweicloud.com/blogs/350260
(21)过多的注释
如果你需要注释来解释一块代码做了什么,试试提炼函数(106);如果函数已经提炼出来,但还是需要注释来解释其行为,试试用改变函数声明(124)为它改名;如果你需要注释说明某些系统的需求规格,试试引入断言(302)。
Tip
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。