《重构改善设计》详细阅读总结

一、概念

什么是重构

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

为什么要重构

改善软件设计:如果不加重构维护程序设计往往会逐渐腐化。1、完成同样的需求,方案本身就有好有坏。2、即使是良好原始方案,也难以避免(没有吃透方案前)修改代码引起的结构流失。3、代码腐化后 添加新特性 需要耗费更多的工作量,未来更难以维护
提升可读性:早期重构,使代码更简洁便于理解,类似擦掉窗户上的污垢,让你看得更远。
帮助寻找bug:对代码重构的同时,可以加深对方案的理解,弄清程序结构,识别过程一些假设是否合理,揪出bug
提高编程速度:良好的设计使快速开发的根本,恶劣的设计一定会降低你的开发速度,花费更多调试定位时间。

开发者不愿重构常见理由: 不知道如何重构;重构追寻长期收益,当前投入效果不明显,后续可能开发者不再维护这些模块;代码重构是额外的工作,项目管理者不理解意义,只看重新规格开发;重构可能破环当前程序,引入bug

重构权衡要素

1、重构切入时机

添加功能时 / 修补错误时 / 复审代码时

2、重构难题区域

数据库:很多用户的商用程序与数据库耦合紧密 难以修改;改变数据结构引入的数据迁移问题。
修改接口:如果函数的所有调用者都在控制下可以放心修改,否则可能面临同时维护两套新旧接口的尴尬局面,特别已是发布接口。
难以重构的设计改动:方案重构都有复杂度,在初期设计方案时,如果这个方案可以方便的重构,那就放心的选择。相反,如果在局面不清晰时 选择了难以改动的方案(比如设计点非常分散),后期重构复杂度就越高。

3、何时不该重构

代码不能在大部分情况正确的运行 / 项目临近关键或者最后期限。

4 、事后重构 还是 预先设计

重构和设计是互补良好的预先设计可以节省返工的高昂成本,“有了设计,我可以思考得更快,但是其中从充满了漏洞”,也会受场景分析不全、过程不可预期因素调整影响。相反,重构也不能完全替代设计,这并不是最有效的途径。
建议:完全依赖预先设计正确,压力非常大,将来修改的代价也相对高昂,我们可以先选择一个足够合理的方案在分析变化的点建立灵活性(考验经验技巧), 在实现的过程加深理解,调整重构。

5、可读性 还是 性能 优先

常见性能管理方式:1、时间预算法(性能要求极高的实时系统),给每个组件预先分配好时间和执行轨迹,要个约束。2、持续关注法 要求每个人开发过程最求高性能,性能提升但是最终维护难度大大增加,过于狭隘。3、独立性能优化阶段(对大多数系统是更合理的选择),将性能调优与软件开发阶段分离,程序大半的时间往往消耗在一小部分不合理的代码,通过工具检查分析,发现热点,去除热点,直至用户满意。

6、添加 还是 删除 间接层

软件大部分问题都可以通过添加间接层,封装来解决问题,隔离变化、解除耦合。但是间接层的使用和维护都是有代价的,每多一个层次就会加深复杂度。何时应该抽象一个中间层 何时 应该删除一个中间层,需要权衡。

二、找准重构问题点(代码坏味道)

站在个人的角度,对代码坏味道进行如下分类。

1、重复代码,过于类似的实现

重复代码Duplicate Code:一个类两个函数含有相同表达式、两个互为兄弟子类相同表达式、两个不相干类出现大量重复。

2、过高或无必要的复杂度,难以理解使用

函数过长Long Method:函数越长越难理解,感觉需要注释来说明的时就要考虑拆分封装,并描述用途合理命名。
过大类LargeClass:当类做太多事,往往就有大量实例变量,分类提炼变量 抽象组件内部(如果适合作为子类就扩展)
过多入参Long ParameterList:太多参数容易引起前后不一致、不易用问题。
数据泥团 Data Clumps:数据管理容易 越聚越多,需要减肥时 思考 删除某些项目后 其他数据有没有失去意义。
基本类型偏执 Primitive Obsession:如果一组总是应该放在一起的字段 以基本类型放在一起,要考虑封装为类管理。
大量switch分支 Switch Statements: swich语言通常意味着重复,每添加一个cast,相关的switch语句就都需要修改,考虑多态替换。
令人迷惑的暂时字段 Temporary Field :某些实例变量 仅仅为某种特定的情况设置,正常情况下 一个实例对象大家认为其全部变量都是有含义有用的,未被使用的变量推测其设置目的很容易让人困惑。
异曲同工类 Alternative Classes with Different Interfaces :两个函数做同一件事却有不同的签名,就需要考按用途虑重新命名 或者 想办法消除重复内容。

3、模块设计不合理、变化难以收敛

发散式变化 Divergent Change模块的变化点应该是集中的,尽可能一点或者一个层次,如果一个类/模块状态 因为不同原因在不方向上变化,修改点 就必须在多个变化的地方适配,就很难控制修改范围。
散弹式修改 Shotgun Surgery:和 发散式变化 相反,如果遇到一中变化,需要引发多个类/模块 修改适配,就要考虑修改地方的集中,否则容易遗漏。
平行继承体系Parallel Inheritance Hierarchies: 当为某一个类添加子类 发现就必须给另一个类添加子类,就要考虑 让一个继承体系实例引用另一个继承体系的实例。
多度耦合的消息链 Message Chains : 用户连续向一个对象请求多个对象就叫消息链,查找过程的流程结构是紧密耦合的,一旦对象间关系发生变化时,客户端就不得不做出相应的修改。
不完美的库类 Incomplete Library Class : 库的目的就是复用,但是库的设计者往往无法在最初 弄清 要覆盖的全部场景 和 核心设计点,结构就是 库很难完美。此时就需要技巧修改部分函数 或者 新增 额外一组行为。
被拒绝的遗赠 Refused Bequest:如果某个类 只使用了少量父类的 数据 和 函数,意味着继承体系设计错误。

4、业务划分不合理、不合理的交互

依恋情节 Feature Envy:某个类/模块 的 行为 对 另一个类/模块兴趣高过自己,大多数情况是 数据管理不合理,大量查询取值接口,需要分析 数据是否应该提炼或者转移。
中间人 Middle Man : 为了封装细节,容易出现过多使用委托的情况,某个类接口有一半都是委托给其他类,这样就是过度运用。太多"不干实事"的函数出现时,将要考虑是否应该直接放进调用端 直接访问,或者变为实责对象的子类,扩展原有对象行为。
狎昵关系 Inappropriate Intimacy:两个模块花费太多时间探究对方private成分或内部实现细节,就要考虑拆散或者提炼合并。
纯稚的数据类 Data Class : 某个类 如果除了 包含 某些字段数据,以及访问这些数据的字段外 一无长物。基本就是一个容器,频繁的被外部类操作,作为一个起点小模块还好,如果参与了整个系统工作,就应该封装更多行为抽象。

5、规划变化造成的冗余设计,引入负担。

冗余类 Lazy Class: 类的维护都是有工作量的,如果 重构过程 或者 规划变化 某些模块/类 缩水,或者事前规划的可能变化点 实际没有发生,就应该消除他们。

6、对未来夸夸其谈,过设计引入负担

夸夸其谈未来性 Speculative Generality:只是觉得未来可能有需求,企图各种钩子和特殊情况来处理一些非必要的事前,往往引起系统难以理解和维护。如果所有设计都会被用到,那么就值得做,如果用不到 就应该搬开。

8、过度的注释

过度的注释 Comments :当你感觉需要填写某种确定的注释信息的时候,请先尝试运用重构手段(合理的函数名,合理的函数层次),让 所有注释 变得多余;相反,如果当前不知道做什么,记录将来打算 或者 没有十足把握的区域,此时就是注释的良好时间

三、重构前期准备(构建测试体系)

重构的前提就是有一个可靠的测试环境,几个关键点:
确保所有测试都完全自动化,让它们检查自己的测试接结果。
每次修改后进行相应测试工作,一套测试就是一个强大的bug侦测器,能够大大缩减查找bug时间。
编写测试代码最佳时间就是在添加特性前梳理接口功能明确工作结束标志
每当你收到bug报告时,先编写一个用例复现bug,集成到测试用例集合。
测试用例添加是风险驱动,先从你最担心出错的部分入手,能取得最大收益,好过无目的大量添加。
测试用例要集中在 出错的边界条件,包括寻找特殊的,可能引起失败的情况如读取空文件。
-------常见自动测试框架如Junit、Cunit

四、常见重构方法

1、表达式 简化 重构

分解条件表达式 Decompose Conditional : 有一个复杂的if-then-else语句,从if\then\else 三个段落 分别 提炼 独立函数,清楚的表明每个分支的作用。
合并条件表单式 Consolidate Conditional Expression : 如果存在一些列 条件测试,都得到相同的结果,将这些测试合并为一个表表达式,并提炼为一个独立的函数。
合并重复条件片段 Consolidate Duplicate Conditional Fragment:一组条件表达式所有分支都执行了相同的某段代码,如果这样 就应该把这些片段移动到表达式外面,保证全部执行。
移除控制标记 Remove Control Flag : 在循环语句中,如果出现某个变量 带有控制标记 flag作用,以break或者return语句取代控制标记。
卫语句取代嵌套条件表达式 Replace Nested Conditional with Gurad Clauses : 函数中的条件逻辑难以看清正常的执行路径,使用卫语句表现所有正常的执行路径。
以多态取代条件表达式 Replace Conditional with Polymorphism :当存在一个条件表达式,根据 对象类型不同而选择不同的行为,将这个表达式每个分支放进一个子类的覆写函数中,然后将原始函数申明为抽象函数。
引入Null对象 Inroduce Null Object : 当你需要再三检查某对象是否为null时,将null值替换为null对象。

2、函数 重新组织 重构

**提炼函数 Extract Method **:把过长函数中的一段代码,或者 需要注释 才能描述 的代码段 放进独立的短小、命名良好的函数中。这样的细粒度的函数天然具备了注释的效果,而且更容易被复用、优化覆写 和 阅读。
内联函数 Inline Method :和提炼函数相反,如果部分短小函数本身 语句可读性已经非常清晰了,或者 由于你的重构变得清晰,此时就要考虑把函数内容直接插入调用位置,减少间接调用。另一种情况,这个函数抽象组织不够好,考虑把函数内联回去 再使用 提炼函数 重新组织。
内联临时变量 Inline Temp : 如果一个临时变量只被简单表达式赋值一次使用,考虑将对变量的引用动作 替换为 表达式自身。
以查询取代临时变量 Replace Temp with Query : 如果一个临时变量保存某个表达式,而这个变量需要在多处使用,导致需要使用该变量的 行为都得集中到一个块,可以使用 封装表达式,让多个使用位置可以直接调用查看,删除临时变量。
引入解释性变量 Introduce Explaining Variable :有些复杂的表达式 难以阅读分析,这种情况下引入一个临时变量,通过变量名称明确定义用途,具备解释效果。
分解临时变量 Split Temporary V’ariable : 如果某个临时变量被赋值超过一次,但它既不是循环变量也不是收集计算结果,容易引起混淆,应该要把变量拆分,每次赋值,创造一个独立、对应的临时变量。
移除对参数的赋值 Remove Assignments to Parameters : 避免对入参赋值,以一个临时变量取代该参数的位置。
以函数对象取代函数 Replace Method with Method Object :如果一个函数中 局部变量泛滥成灾,分解过于困难,通过前面方法 也很难收敛,可以使用 对象替代 函数,把局部变量替换为其 字段。
替换算法 Subsitute Algorithm :把函数本体替换为另一个算法,让函数实现更清晰,取代原有复杂实现。

3、函数 简化调用 重构

函数改名 Rename Method : 函数名称 和 实际功能不匹配,修改函数名称。
添加参数 Add Parameter : 某个函数需要重从调用端 得到更多信息,为此函数添加一个对象参数,让该对象带进函数所需的信息。
移除参数 Remove Parameter : 函数本体不再需要某个函数,将该参数去除。
将查询函数和修改函数分离 Separate Query from Modifier : 某个函数返回对象的状态,有修改对象状态。建立两个不同的函数,一个负责查询,另一个负责修改。
令函数携带参数 Parameterize Method : 若多个函数做了类似的工作,但在函数本体中却包含 了不同的值,建立一个单一的函数,以参数表单那些不同的值。
以明确的函数取代参数 Replace Parameter with Explicit Methods : 若某函数 其中完全取决 于参数值而采取不同的行为,针对该参数的每一个可能值,建立一个独立的函数。
保持对象完整 Preserve Whole Object : 你从某个对象中取出若干值,将他们作为某一次函数调用时的参数,改为传递整个对象。(目的时 如果从这个对象取出参数逐渐变多,这样不用随时扩展修改)。
以函数取代参数 Replace Parameter with Methods : 对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数函数本身也能够调用前一个函数。将参数接受者去除该项参数,并直接调用前一个函数(如果函数能够从其他途径活得参数值,就不应该通过参数取得该值)。
引入参数对象 Introduce Parameter Object : 某些参数总是很自然的同时出现,以一个对象取代这些函数。
移除设值函数 Remove Setting Method : 类中的某个字段应该在创建的时候被设置,然后就不再改变,去掉该字段的所有设置值函数。
隐藏函数 Hide Method : 有一个函数,从来没有被其他任何类用到,将这个函数修改为private。
以工厂函数取代构造函数 Replace Constructor with Factory Method:你希望在创建对象时不仅仅时做简单的构建动作,将构造函数替换为工厂函数。(如果需要创建的类,产生了很多派生子类,使用者要根据类型码创建类,同时还可能需要创建其派生子类,如果使用工厂函数替代就能简化使用者)
封装向下转型 Encapsulate Downcast:某个函数返回对象,需要由函数调用者执行向下转型downcast,将向下转型动作移到函数中。
以异常取代错误码 Replace Error Code with Exception: 某一个函数返回一个特定的代码,用于表示某种错误的情况,改用异常。(异常将普通程序 和 错误处理 分开了,这使得程序更容易理解)
以测试取代异常 Replace Exception with Test : 面对一个函数可以预先检查条件,先在效用函数前检查条件,优于调用后检查异常。(异常应该值用于 异常、罕见的行为,预料之外的行为,可能能先判断)

4、数据 重新组织 重构

自封装字段 Self Encapsulated Field : 直接访问字段,但与字段之间的耦合关系逐渐笨拙,可以为字段建立 Get/Set函数通过它们访问。(直接使用 能方便阅读,但是产生的耦合关系 也 影响数据的重新组织/拆分/搬移)
**以对象取代数据值 Replace Data Value with Object ** : 数据一定要和行为一起才有价值,单存的数据无法体现它的含义概念,随着开发的进行,原来简单的数据不再那么简单,使用整改就更复杂。
将值的对象改为引用对象 Change Value to Reference: 如果一个类衍生出许多彼此相等的实例,希望将他们替换为同一个对象,可以将则会个值对象调整为引用对象。(可以动态的传入选择需要使用哪个衍生类的实现实例)
将引用对象改为值对象 Change Reference to Value : 如果由一个引用对象,很小且不可变,而且不易管理,就替换为值对象。(更稳定可靠)
以对象取代数组 Replace Array with Object : 你有一个数组,每个元素代表不同的东西,建议使用类替换,用不同的子手段表示其含义。(数组 应该只描述 某种顺序容纳的一组类似对对象)
复制“被监视数据” Duplicate Observed Data :如果一些数据置身于GUI控件中,而领域函数需要访问这些数据,将该数据复制到一个领域对象中,建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
将单向关联改为双向关联 Change Unidirectional Association to Bidirectional : 两个类都需要使用对方特性,但是其间只有一条单项连接。添加一个反向指针,并使修改函数能够同时更新两条连接。
将双向关联改为单向关联 Change Bidirectional to Unidirectional Association :如果两个类之间存在双向关联,但其中一个类如今不再需要另一个类的特性,去除不必要的关联。
封装字段 Encapsulate Field :把public字段调整为private,并提供相应的访问函数。
封装集合 Encapsulate Collection :有一个函数返回一个集合,让这个函数返回该集合的一个只读副本,并且在这个类中提供添加/移除集合元素的函数。
以数据类取代记录 Replace Record with Data Class : 当=需要面对传统编程环境中的记录结构,为该记录创建一个“哑”数据对象。
以类取代类型码 Replace Type Code with Class : 类之有一个数值类型码,但它并不影响类的行为,以一个新的类替换给数值类型码。
以子类取代类型码 Replace Type Code with Subclasses : 你有一个不变的类型码,它会影响类的行为,以子类取代这个类型码。
以state/strategy取代类型码 Replace Type Code with State/Strategy : 你有一个类型码,它会影响类的行为,但无法通过继承手法消除它,以状态对象取代类型码。
以字段取代子类 Replace Subclass with Fields : 你的各个子类唯一的差别只在 “返回常量数据” 的 函数上,修改这些函数,使它们返回超类中的某个 新增 字段,然后销毁子类。

5、对象(模块)特性转移 重构

在对象的设计过程,“决定把责任放在哪里” 基本是最重要的事情,预先设计很难保证一次作对。这种情况下就运用重构。
搬移函数 Move Method:当A类(模块)中某个函数 与 其 另一个B类(模块)进行更多的交流或者调用关系,耦合更紧密,应该在B类中建立该函数的细节实现,A中函数应该作为 单纯的委托函数 或者 完全移除。
搬移字段 Move Field : 同上面类似,如果发现某个字段,相对其所在的A类,更多的被 B类 使用,考虑直接移动到B类。
提炼类 Extract Class :某个类做了应该由两个类做的事情,建立一个新的类,将相关字段和函数搬移到新类。
类内联化 Inline Class :如果一个类没有做太多事情,将这个类的所有特性搬移到另一个类中,然后移除原类。
隐藏委托关系 Hide Delegate : 用户通过一个委托类来调用另一个对象,可以在服务(委托)类上建立客户所需要的全部函数,隐藏调用委托关系,单向调用,不看到实际实现的类。
移除中间人 Remove Middle Man :某个类做了过多的简单委托动作(随着受托类功能越来越多,大量简单委托函数出现),将客户直接调用 受托类。
引入外加函数 Introduce Foreign Method : 当需要对某个服务类添加函数实现,但却不能修改这个服务类的时候,可以在客户类建立一个函数,然后以第一个参数形式传入一个服务类实例中,提供服务使用。
引入本地扩展 Introduce Local Extension : 和上面类似,无法修改服务类 但需要添加一组额外参数,提供一个新类包含额外函数,让扩展品成为原类的子类或者包装类。
以字面常量取代魔法数 Replace Magic Number with Symbolic Constant:当字面数值 带有特别含义,创造一个常量,以其意义为它命名,并将上述字面数值替换为这个常量。

6、处理 对象 继承关系 重构

字段上移 Pull Up Field : 两个子类拥有相同的字段,将该字段移动至超类。
函数上移 Pull Up Method : 有些函数,在各个子类中产生完全相同的结果,将该函数移至超类。
构造函数本地上移 Pull Up Constructor Body : 如果各个子类中还拥有一些构造函数,它们本地几乎完全一致,在超类中新建一个构造函数,并在子类构造函数中调用它。
函数下移 Push Down Method : 超类中的某个函数只与部分(而非全部)子类相关,将这个函数移动到相关的子类去。
字段下移 Push Down Field: 超类中某个字段 只有部分(而非全部)子类用到,将这个字段移动到需要它的那些子类去。
提炼子类 Extract Subclass : 类中的某些特性只被某些(而非全部)实例用到,建立一个新的子类,将上面所说的那一部分特性移到子类中。
提炼超类 Extract Superclass:两个类有相似特性,为这两个类建立一个超类,将相同的特性移动枝超类。
提炼接口 Extract Interface : 若干客户使用类接口中的统一子集,或者两个类的接口有部分相同,将相同的子集 提炼到一个独立的接口中。
折叠继承体系 Collapse Hierarchy : 超类和子类间无太大区别,将它们合为一体。
塑造模板函数 Form Template Method : 如果存在一致子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作细节上有所不同,将这些操作分别放进独立的函数(保持相同的签名),然后将原函数上移动至超类。
以委托取代继承 Replace Inheritance with Delegation : 某个子类只使用了超类的一部分,或者 根本不需要继承而来的数据。在子类中新建一个字段用以保存超类,调整子类函数,令他改而委托超类,然后去掉两者之间的继承关系。
以继承取代委托 Replace Delegation with Inheritance : 你在两个类之间使用委托关系,并经常为整个接口编写许多极简单委托函数,让委托类继承受托类。

7、系统(整体方案)级别重构

梳理并分解继承体系 Tease Apart Inheritance : 某个继承体系同时承担两项责任,建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
将过程化设计转化为对象设计 Convert Procedural Design to Objects :针对原有的传统过程化风格的代码,将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中。
将领域和表述/显示分离 Separate Domain from Presentation : 某些GUI类之中包含了领域逻辑,将领域逻辑分离出来,为它们建立独立的领域类。(类似MVC模式,将 显示 和 领域逻辑 分离,使程序将来修改变得更容易)
提炼继承体系 Extract Hierarchy : 某个类做了太多工作,其中一部分工作使以大量条件表达式完成,建立继承体系,以一个子类表示一种特殊情况。

猜你喜欢

转载自blog.csdn.net/runafterhit/article/details/106416067