重构 之 总结代码的坏味道 Bad Smell (一) 重复代码 过长函数 过大的类 过长参数列 发散式变化 霰弹式修改
膜拜下 Martin Fowler 大神 , 开始学习 圣经 重构-改善既有代码设计 .
代码的坏味道就意味着需要重构, 对代码的坏味道了然于心是重构的比要前提;
.
作者 : 万境绝尘
转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/20009689
.
1. 重复代码 (Duplicated Code)
用到的重构方法简介 : Extract Method(提炼函数), Pull Up Method(函数上移), From Template Method(塑造模板函数), Substitute Algorithm(替换算法), Extract Class(提炼类);
-- Extract Method(提炼函数) : 将重复的代码放到一个函数中, 并让函数名称清晰的解释函数的用途;
-- Pull Up Method(函数上移) : 将函数从子类移动到父类中;
-- From Template Method(塑造模板函数) : 不同子类中某些函数执行相似操作, 细节上不同, 可以将这些操作放入独立函数中, 这些函数名相同, 将函数上移父类中.
-- Substitute Algorithm(替换算法) : 将函数的本体替换成另外一个算法;
-- Extract Class(提炼类) : 建立一个新类, 将相关的函数 和 字段 从旧类搬移到新类;
重复代码坏处 : 重复的代码结构使程序变得冗长, 这个肯定要优化, 不解释;
同类函数重复代码 : 同一个类中 两个函数 使用了相同的表达式;
-- 解决方案 : 使用 Extract Method(提炼函数) 方法提炼出重复的代码, 两个函数同时调用这个方法, 代替使用相同的表达式;
兄弟子类重复代码 : 一个父类有两个子类, 这两个子类中存在相同的表达式;
-- 代码相同解决方案 : 对两个子类 使用 Extract Method(提炼函数)方法, 然后将提炼出来的代码 使用 Pull Up Method(函数上移)方法, 将这段代码定义到父类中去;
-- 代码相似解决方案 : 使用 Extract Method(提炼函数)方法 将相似的部分 与 差异部分 分割开来, 将相似的部分单独放在一个函数中;
-- 进一步操作 : 进行完上面的操作之后, 可以运用 From Template Method(塑造模板函数) 获得一个 Template Method 设计模式, 使用模板函数将相似的部分设置到模板中, 不同的部分用于模板的参数等变量;
-- 算法切换 : 如果模板中函数的算法有差异, 可以选择比较清晰的一个, 使用Substitute Algorithm(替换算法) 将不清晰的算法替换掉;
不相干类出现重复代码 : 使用Extract Class(提炼类) 方法, 将重复的代码提炼到一个重复类中去, 然后在两个类中 使用这个提炼后的新类;
-- 提炼类存在方式 : 将提炼后的代码放到两个类中的一个, 另一个调用这个类, 如果放到第三个类, 两个类需要同时引用这个类;
2. 过长函数(Long Method)
用到的重构方法 : Extract Method(提炼函数), Replace Temp with Query(以查询取代临时变量), Introduce Parameter Object(引入参数对象), Preserve Whole Object(保持对象完整), Decompose Conditional(分解条件表达式);
-- Extract Method(提炼函数) : 将代码放到一个新函数中, 函数名清晰的说明函数的作用;
-- Replace Temp with Query(以查询取代临时变量) : 程序中将表达式结果放到临时变量中, 可以将这个表达式提炼到一个独立函数中, 调用这个新函数 去替换 这个临时变量表达式, 这个新函数就可以被其它函数调用;
-- Introduce Parameter Object(引入参数对象) : 将参数封装到一个对象中, 以一个对象取代这些参数;
-- Preserve Whole Object(保持对象完整) : 从某个对象中取出若干值, 将其作为某次函数调用时的参数, 由原来的传递参数 改为 传递整个对象, 类似于 Hibernate;
-- Replace Method with Method Object(以函数对象取代函数) : 大型函数中有很多 参数 和 临时变量, 将函数放到一个单独对象中, 局部变量 和 参数 就变成了对象内的字段, 然后可以在 同一个对象中 将这个 大型函数 分解为许多 小函数;
-- Decompose Conditional(分解条件表达式) : 将 if then else while 等语句的条件表达式提炼出来, 放到独立的函数中去;
小函数优点 : 小函数具有更强的 解释能力, 共享能力, 选择能力, 小函数维护性比较好, 拥有小函数的类活的比较长;
-- 长函数缺点 : 程序越长越难理解;
-- 函数开销 : 早期编程语言中子程序需要额外的开销, 所以都不愿意定义小函数. 现在面向对象语言中, 函数的开销基本没有;
-- 函数名称 : 小函数多, 看代码的时候经常转换上下文查看, 这里我们就需要为函数起一个容易懂的好名称, 一看函数名就能明白函数的作用, 不同在跳转过去理解函数的含义;
分解函数结果 : 尽可能分解函, 即使函数中只有一行代码, 哪怕函数调用比函数还要长, 只要函数名能解释代码用途就可以;
-- 分解时机 : 当我们需要添加注释的时候, 就应该将要注释的代码写入到一个独立的函数中, 并以代码的用途命名;
-- 关键 : 函数长度不是关键, 关键在于 函数 是 "做什么", 和 "如何做";
常用分解方法 : Extract Method(提炼函数) 适用于 99% 的过长函数情况, 只要将函数中冗长的部分提取出来, 放到另外一个函数中即可;
参数过多情况 : 如果函数内有大量的 参数 和 临时变量, 就会对函数提炼形成阻碍, 这时候使用 Extract Method(提炼函数) 方法就会将许多参数 和 临时变量当做参数传入到 提炼出来的函数中;
-- 消除临时变量 : 使用 Replace Temp with Query(以查询取代临时变量) 方法消除临时元素;
-- 消除过长参数 : 使用 Introduce Parameter Object(引入参数对象) 和 Preserve Whole Object(保持对象完整) 方法 可以将过长的参数列变得简洁一些;
-- 杀手锏 : 如果使用了上面 消除临时变量和过长参数的方法之后, 还存在很多 参数 和 临时变量, 此时就可以使用 Replace Method with Method Object(以函数对象取代函数方法) ;
提炼代码技巧 :
-- 寻找注释 : 注释能很好的指出 代码用途 和 实现手法 之间的语义距离, 代码前面有注释, 就说明这段代码可以替换成一个函数, 在注释的基础上为函数命名, 即使注释下面只有一行代码, 也要将其提炼到函数中;
-- 条件表达式 : 当 if else 语句, 或者 while 语句的条件表达式过长的时候, 可以使用Decompose Conditional(分解条件表达式) 方法, 处理条件表达式;
-- 循环代码提炼 : 当遇到循环的时候, 应该将循环的代码提炼到一个函数中去;
3. 过大的类 (Large Class)
用到的重构方法 : Extract Class(提炼类), Extract Subclass(提炼子类), Extract Interface(提炼接口), Duplicate Observed Data(复制被监视的数据);
-- Extract Class(提炼类) : 一个类中做了两个类做的事, 建立一个新类, 将相关的字段和函数从旧类中搬移到新类;
-- Extract Subclass(提炼子类) : 一个类中的某些特性只能被一部分实例使用到, 可以新建一个子类, 将只能由一部分实例用到的特性转移到子类中;
-- Extract Interface(提炼接口) : 多个客户端使用类中的同一组代码, 或者两个类的接口有相同的部分, 此时可以将相同的子集提炼到一个独立接口中;
-- Duplicate Observed Data(复制被监视的数据) : 一些领域数据放在GUI控件中, 领域函数需要访问这些数据; 将这些数据复制到一个领域对象中, 建立一个观察者模式, 用来同步领域对象 和 GUI对象的重要数据;
实例变量太多解决方案 : 使用 Extract Class (提炼类) 方法将一些变量提炼出来, 放入新类中;
-- 产生原因 : 如果一个类的职能太多, 在单个类中做太多的事情, 这个类中会出现大量的实例变量;
-- 实例变量多的缺陷 : 往往 Duplicate Code(重复代码) 与 Large Class(过大的类)是一起产生的;
-- 选择相关变量 : 选择类中相关的变量提炼到一个新类中, 一般前缀, 后缀相同的变量相关性较强, 可以将这些相关性较强的变量提炼到一个类中;
-- 子类提炼 : 如果一些变量适合作为子类, 使用Extract Subclass(提炼子类) 方法, 可以创建一个子类, 继承该类, 将提炼出来的相关变量放到子类中;
-- 多次提炼 : 一个类中定义了20个实例变量, 在同一个时刻, 只使用一部分实例变量, 比如在一个时刻只使用5个, 在另一时刻只使用4个 ... 我们可以将这些实例变量多次使用 提炼类 和 子类提炼方法;
代码太多解决方案 :
-- 代码多的缺陷 : 太多的代码是 代码重复, 混乱, 最终走向项目死亡的源头;
-- 简单解决方案 : 使用 Extract Method (提炼函数) 方法, 将重复代码提炼出来;
-- 提炼类代码技巧 : 使用 Extract Class(提炼类) 和 Extract Subclass(子类提炼) 方法对类的代码进行提炼, 先确定客户端如何使用这个类, 之后运用 Extract Interface(提炼接口) 为每种使用方式提炼出一个接口, 可以更清楚的分解这个类;
-- GUI类提炼技巧 : 使用 Duplicate Observed Data(复制被监视的数据) 方法, 将数据 和 行为 提炼到一个独立的对象中, 两边各保留一些重复数据, 用来保持同步;
4. 过长参数列 (Long Parameter List)
使用到的重构方法简介 : Replace Parameter with Method(以函数取代参数), Preserve Whole Object(保持对象完整), Introduce Parameter Object(引入参数对象);
-- Replace Parameter with Method(以函数取代参数) : 对象调用 函数1, 将结果作为 函数2 的参数, 函数2 内部就可以调用 函数1, 不用再传递参数了;
-- Preserve Whole Object(保持对象完整) : 将对象中的一些字段是函数的参数, 直接将对象作为函数的参数, 由传递多个参数改为传递封装好的对象;
-- Introduce Parameter Object(引入参数对象) : 将函数参数封装在一个对象中;
参数列过长 :
-- 函数数据来源 : ① 参数, 将函数中所需的数据都由参数传入; ② 将函数中所用到的数据设置在全局数据中, 尽量不要使用全局数据;
-- 对象参数 : 使用对象封装参数, 不需要把函数需要的所有数据用参数传入, 只需要将函数用到的数据封装到对象中即可;
-- 面向对象函数参数少 : 面向对象程序的函数, 函数所用的数据通常在类的全局变量中, 要比面向过程函数参数要少;
普通参数和对象参数对比 :
-- 参数过长缺陷 : 太多的参数会造成函数 调用之间的 前后不一致, 不易使用, 一旦需要更多数据, 就要修改函数参数结构;
-- 对象参数优点 : 使用对象传递函数, 如果需要更多的参数, 只需要在对象中添加字段即可;
参数的其它操作 :
-- 函数取代参数 : 在对象中 执行一个 函数1 就可以取代 函数2 的参数, 就要使用 Replace Parameter with Method(以函数取代参数) 方法;
-- 对象代替参数 : 函数中来自 同一个对象的 多个参数 可以封装在这个对象中, 可以将这个封装好的对象当做参数, 使用Preserve Whole Object(保持对象完整) 方法;
-- 创建参数对象 : 如果找不到合适的对象封装这些参数数据, 可以使用 Introduce Parameter Object(引入参数对象) 方法制造一个参数对象;
对象依赖与函数参数之间的平衡 : 二者是相对的, 下面一定要选择一种不利状况;
-- 避免依赖 : 函数参数传递对象, 那个函数所在的对象 与 这个参数对象依赖关系很紧密, 耦合性很高, 这时候就要避免依赖关系, 将数据从对象中拆出来作为参数;
-- 参数太长 : 如果参数太长, 或者变化太频繁, 就要考虑是否选择依赖;
5. 发散式变化 (Divergent Change)
对于这个在我所在的研发团队中这个问题很严重, 因为做的是远程医疗系统, 在Android上要支持许多医疗设备, 每次添加医疗设备都会死去活来;
使用到的重构方法简介 : Extract Class(提炼类);
期望效果 : 当我们添加新功能的时候, 只需要修改一个地方即可, 针对外界变化相应的修改, 只发生在单一类中, 如果做不到这一点, 就意味着程序有了坏味道 Divergent Change;
发散式变化 :
-- 出现效果 : 如果对程序进行例行维护的时候, 添加修改组件的时候, 要同时修改一个类中的多个方法, 那么这就是 Divergent Change;
-- 修改方法 : 找出造成发散变化的原因, 使用 Extract Class(提炼类) 将需要修改的方法集中到一个类中;
6. 霰弹式修改 (Shotgun Surgery)
使用到的重构方法简介 : Move Method(搬移函数), Move Field(搬移字段), Inline Class(内联化类);
-- Move Method(搬移函数) : 类A 中的 方法A 与 类B 交流频繁, 在类B中创建一个与 方法A 相似的 方法B, 从方法A 中 调用 方法B, 或者直接将方法A删除;
-- Move Field(搬移字段) : 类A 中的 字段A 经常被 类B 用到, 在类B 中新建一个字段B, 在类B 中尽量使用字段B;
-- Inline Class(内联化类) : 类A 没有太多功能, 将类A 的所有特性搬移到 类B中, 删除类A ;
霰弹式修改坏味道 : 遇到的每种变化都需要在许多不同类内做出小修改, 即要修改的代码散布于四处, 不但很难找到, 而且容易忘记重要的修改, 这种情况就是霰弹式修改;
-- 注意霰弹式修改 与 发散式变化 区别 : 发散式变化是在一个类受多种变化影响, 每种变化修改的方法不同, 霰弹式修改是 一种变化引发修改多个类中的代码;
-- 目标 : 使外界变化 与 需要修改的类 趋于一一对应;
重构霰弹式修改 :
-- 代码集中到某个类中 : 使用 Move Method(搬移函数) 和 Move Field(搬移字段) 把所有需要修改的代码放进同一个类中;
-- 代码集中到新创建类中 : 没有合适类存放代码, 创建一个类, 使用 Inline Class(内联化类) 方法将一系列的行为放在同一个类中;
-- 造成分散式变化 : 上面的两种操作会造成 Divergent Change, 使用Extract Class 处理分散式变化;
.
作者 : 万境绝尘
转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/20009689
.