分享

重构-改善既有代码的设计

 马无夜草不肥啊 2012-01-09
最深感受:看到的不爽的东西不能得过且过, 也不能一味的责骂不爽而搞坏自己的情绪,持续的重构是解决之道。对待需求变化应该始终以一种乐观的态度去接受,坚持重构,适应变化。用重构去持续的改善和保证代码质量

第一章 重构,第一个案例 Refactoring, a First Example
1.1 如果你发现自己需要为程序添加一个特性,而代码的结构使你无法很方便的那么做,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性
1.2 重构之前,首先检测自己是否有一套可靠的测试机制。这些测试必须有自我检验(self-checking)能力。
1.3 重构技术要以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
  为代码更名是绝对值得的,好的代码应该清楚表达出自己的功能,变量名称是代码清晰的关键。
  任何一个傻瓜都能写出计算机可以理解的代码,惟有写出人类容易理解的代码,才是优秀程序员
 
第二章 重构原则 Principles in Refectoring
2.1 重构(名词):对软件内部结构的一种调整,目的是在不改变软件外在行为的前提下,提高其可理解性,降低其修改成本。
  重构(动词):使用一系列重构准则(手法),在不改变软件外在行为的前提下,调整其结构。
  使用重构技术开发软件时,把自己的时间分配给两种截然不同的行为:“添加新功能”和“重构”。添加新功能时,不应该修改既有代码,只管添加新功能。重构时不能再添加功能,只管改进程序结构。
2.2 为何重构?重构改进软件设计;重构使软件更易被理解;重构能帮助找bug;重构能提高编程速度
  我是个懒惰的程序员,我的懒惰表现形式之一就是:总是记不住自己写过的代码。事实上对于任何可查阅的东西我都故意不去记忆它,因为我怕把自己的脑袋塞爆。我总是尽量把该记的东西写进程序里,这样就不必记住它了。
  我不是个伟大的程序员,我只是个有着一些优秀习惯的好程序员而已。
2.3 何时重构?事不过三,三则重构;添加功能时一并重构;修改错误时一并重构;复审代码时一并重构。
  重构是一个快速流畅的过程,一旦完成重构,新特性的添加就会更快速、更流畅。
  垃圾代码的特征:难以阅读;逻辑重复;添加新行为时需修改既有代码;复杂的条件逻辑
2.5 重构的难题:涉及到数据库的重构可能会比较麻烦;如果接口被修改了,任何事情都有可能发生;一般比较倾向于选择简单的设计,哪怕它不能覆盖所有潜在的需求;当应该重新编写所有代码的时候不要选择重构;如果项目已经非常接近最后期限,则不应该分心重构。
2.6 有了设计,我可以思考更快,但是其中充满小漏洞
  要建造一个灵活的解决方案,所需的成本难以估算。只管建造可运行的最简化系统,至于灵活而复杂的设计大多数时候都是不必要的。
  哪怕你完全了解系统,也请实际测量它的热点而不要臆测。
 
第三章 代码的坏味道 Bad Smells in Code
3.1 Duplicated Code 重复的代码
  同一个类内的成员函数重复;相同基类的子类内代码重复;毫不相干的两处重复
3.2 Long Method 过长函数
  当感觉需要用注释来说明点什么时可能在暗示需要拆分函数;条件和循环也是函数拆分的信号
3.3 Large Class 过大类
  类内有着相同前缀活后缀的成员变量通常可以拆到独立的类中;如果类的访问接口过大,应该根据用户的使用情况进行分割
3.4 Long Parameter List 过长参数列
  函数需要的多半参数信息可以通过其宿主类得到;不过明确希望降低函数行为和宿主之间的耦合性,较长的相对稳定的参数也是可以理解的
3.5 Divergent Change 发散式变化
  当软件需要修改时最好只是修改class内的一处,而不是多处,将变化封装到另外的class中
3.6 Shotgun Surgery 散弹式修改
  当软件需要修改时最好最好不要四处修改很多个类,而应该将这些涉及到的类封装到一个单一的点中
3.7 Feature Envy 依恋情节
  将总是变化的东西放在一块
3.8 Data Clumps 数据泥团
  对于经常耦合在一起的数据,可以考虑封装成对象,比如用于减少类中的字段数或者缩短函数的参数
3.9 Primitive Obsession 基本型别偏执
  不要过分在乎内嵌类型和自定义类型之间的性能差别,尽量的向OO的世界靠近
3.10 Switch Statements
  switch一般可以用多态来消除,但局部的switch并没有必要这样做
3.11 Parallel Inheritance Hierarchies 平行继承体系
  如果为某class增加一个subclass时也必须为另一个class增加一个相应的subclass,有点不爽
3.12 Lazy Class 冗赘类
  所创建的每一个class,都得有人去理解它、维护它,这些工作都是要花钱的。
3.13 Speculative Generality 夸夸其谈未来性
  用不到的装置和组件就是垃圾。
3.14 Temporary Field 令人迷惑的暂时值域
  通常要做到对象在所有时候都需要它的所有变量
3.15 Message Chains 过度耦合的消息链 // 翻译的好恶心啊
  行为和对象结构的强烈耦合是不好的
3.16 Middle Man 中间转手人
  过多的代理会让class的行为混乱
3.17 Inappropriate Intimacy 暧昧关系 // 恶心的翻译
  类之间的关系不能过于亲密,要严守清规
3.18 Alternative Classes with Different Interfaces 异曲同工的类
  不同的名字缺干着相同的事情,改之
3.19 Incomplete Library Class 不完美的程序类库
3.20 Data Class 纯粹的数据类
  这往往意味着public的字段,系统级别的危害很大
3.21 Refused Bequest
  如果子类不想使用基类的字段,一般是可以忍受的,但如果不想使用基类的方法,或许应该抛弃继承
3.22 Comments 过多的注释
  当觉得需要写注释时,可以先尝试重构
 
第四章 构筑测试体系 Building Tests
4.1 自我测试代码(Self-testing Code)的价值
  编写测试代码的最好时间是在开始编程之前。
  编写测试代码能使注意力集中到接口而非实现上。
  重构之前可能需要给代码加上自我测试功能
4.2 JUnit测试框架
  频繁的运行测试,每次编译把测试代码也考虑进去,每天至少执行每个测试一次
  当最终用户或黑盒测试发现bug后,应该先针对该bug变成单元测试代码以浮现,然后再着手解决问题
4.3 添加更多测试
  只对可能出bug的地方编写测试
  考虑可能出错的边界条件,把测试火力集中在这里
  在应该出错的情况下,检测异常是否被正确的抛出
  单元测试不可能发现所有的bug,但能发现大多数的,尤其在系统重构的时候尤为重要(我想对于动态语言可能会更重要)
 
第五章 重构名录 Toward a Catalog of Refactorings
 
第六章 重新组织你的函数 Composing Methods
6.1 Extract Method 提炼函数
  将一段代码放到一个独立的函数中,并然函数名解释函数的用途
6.2 Inline Method 将函数内联化
  在函数调用点插入函数本体,然后移除该函数
  非必要的间接性总是让人不舒服
  如果函数具有多态性,比如被子类override,则不要进行Inline Method
6.3 Inline Temp 将临时变量内联化
  将所有对该变量的引用动作,替换为对它赋值的那个表达式自身
  如果该变量未被修饰为const,应先加上const修饰,编译,看是否还有其他地方对该变量赋值
6.4 Replace Temp with Query 以查询取代临时变量
  将表达式提炼到一个独立的函数中
6.5 Introduce Explaining Variable 引入解释性变量
  将复杂表达式(或其中一部分)的结果放进一个临时变量里,以此变量的名字来解释该表达式的用途
  Extract Method和该手法类似,并会取得更好的效果,因为提供了小函数被重用的机会
6.6 Split Temporary Variable 剖解临时变量
  针对每次赋值,创造一个独立的、对应的临时变量
  同一个临时变量只承担一个责任
6.7 Remove Assignment to Parameters 移除对参数的赋值操作
  以一个临时变量取代要被赋值的参数
6.8 Replace Method with Method Object 以函数对象取代函数
  一个大型函数中对局部变量的使用过于复杂,导致无法采用Extract Method,应将这个函数放进一个单独对象中,这样局部变量就变成对象内的值域,然后就可以在同一个对象中将此大型函数分解为几个小函数。
6.9 Substitute Algorithm 替换你的算法
  将函数本体替换为另一个算法
 
第七章 在对象之间搬移特性 Moving Freatures Between Objects
7.1 Move Method 搬移函数
  若某成员函数与另一个类的交互更密切,有必要考虑将该函数搬到对应的类中,原来的就函数可以变成一个单纯的调用委托甚至移除
7.2 Move Field 搬移值域
  若某字段被另外一个class过多的使用,则可以考虑将该字段搬到对应的类中,并修改原field所有的用户
7.3 Extract Class 提炼类
  若某class做了应该由两个class做的事情,则应该新建一个class并将功能分离
7.4 Inline Class 将类内联化
  若某个class没有做太多实质性的事情,则可以将其功能搬到现有class并将其删除
7.5 Hide Delegate 隐藏"委托关系"
  可以在将一堆客户要用的函数组织到一个单独的类中,对用户隐藏实现的细节
7.6 Remove Middle Man 移除中间人
  移除不必要的中间层,让用户的访问更直接
7.7 Introduce Foreign Method 引入外加函数
  若无法给某类添加成员方法,则可以在外部实现一个传入this的扩充方法
7.8 Introduce Local Extension 引入本地扩展
  若针对某类的扩充方法太多时,可以将这些扩充方法组织成subclass或者wrapper
 
第八章 重新组织数据 Organizing Data
8.1 Self Encapsulate Field 自封装值域
  为field添加get、set函数而不是直接去访问
8.2 Relpace Data Value with Object 以对象取代数据值
  将class中一组相关的具有复杂行为的field封装成另外的class
8.3 Change Value to Reference 将实值对象改为引用对象
  value type和reference type之间的切换
8.4 Change Reference to Value 将引用对象改为实值对象
  value type和reference type之间的切换
  分布式系统和并发系统中value type比较好用,因为不需考虑对象的同步问题
8.5 Replace Array with Object 以对象取代数组
  通用数组若各个元素表示的意义不同时,可以考虑将其封装为对象。data[0] = name; data[1] = age
8.6 Duplicate Observed Data 复制"被监视数据"
  将UI层零散的数据封装到一个object中,然后采用Observer模式将该object和前台UI建立联系。避免UI层过多的耦合逻辑和数据
8.7 Change Unidirectional Association to Bidirectional 将单向关联改为双向
  给单向指针添加一个反向指针实现双向关联,使得修改函数能够同时更新关联的对象
8.8 Change Bidirectional Association to Unidirectional 将双向关联改为单向
  不恰当的双向关联可能会造成“僵尸对象”,已经不再使用的对象由于其他对象持有它的引用而不能及时回收
  只在必要时才使用双向关联
8.9 Replace Magic Number with Symbolic Constant 以符号常量/字面常量取代魔法数
  Magic number是历史最悠久的不良现象之一
  常量不会造成任何性能开销,却可以大大提高代码的可读性
8.10 Encapsulate Field  封装值域
  将public的field修改为private并添加访问函数
8.11 Encapsulate Collection 封装群集
  将返回集合的函数改为返回集合的只读封装,然后添加add、remove成员函数用以实现集合修改
8.12 Replace Record with Data Class 以数据类取代记录
  用有行为意义的class取代raw struct
8.13 Replace Type Code with Class 以类取代型别码
  针对类中的enum,若该enum不影响class的行为,可以创建一个class包含该enum字段,并在该class内部创建需要的static instance来保存相应的enum value
8.14 Replace Type Code with Subclasses 以子类取代型别码
  针对类中的const enum,若它的值会影响class的行为,可以采用多态来进行行为区分
  多态将“对不同行为的了解”从用户那儿转移到了class自身。如果需要加入新的行为变化,只需添加新的subclass,而不需逐一修改所有的条件式
8.15 Replace Type Code with State/Strategy 以State/Strategy取代型别码
  对于影响class行为的type code而不方便采用subclass时,可以考虑将其封装成一个专门描述状态的state object
8.16 Replace Subclass with Fields 以值域取代子类
  若subclass的惟一差别只在“返回常量数据”的函数上,可以给super class中添加一个field和一个实用函数来完成类型的功能,这批subclass则可以删除
 
第九章 简化条件表达式 Simplifying Conditional Expressions
9.1 Decompose Conditional 分解条件式
  将判断分支中的代码封装成单独的函数
  复杂的条件逻辑是最常导致复杂度上升的因素之一
9.2 Consolidate Conditional Expressions 合并条件式
  将一系列if(xxx) return 0代码应用逻辑and or合并起来,甚至可以合并成单一的if(check(xxx)) return 0
9.3 Consolidate Duplicate Conditional Fragments 合并重复的条件片段
  将条件分支中每个分支都执行的代码放到条件外部
9.4 Remove Control Flag 移除控制标记
  能不用bool flag作为循环或分支的控制标记时尽量不用
9.5 Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件式
  若if-else中有一个分支为异常行为处理,则最好采用短路形式,而不是疯狂的if-else嵌套。
9.6 Replace Conditional with Polymorphism 以多态取代条件式
  如果条件根据对象型别不同而分支,可以将条件式的每个分支放进subclass内重写基类的抽象函数
9.7 Introduce Null Object 引入Null对象
  将无效值替换为无效的对象,这样就不用到处判断if(customer != null) doif(); else doelse();,只要直接customer.dosomething()就ok了
9.8 Introduce Assertion 引入断言
  对于必须阅读代码才能看出来的假设最好都以assert的形式体现
 
第十章 简化函数调用 Making Method Calls Simpler
  最简单也是最重要的一件事就是命名和重命名
  良好的接口只想用户展现必须展现的东西
10.1 Rename Method 重新命名函数
  给函数命名的好方法:首先考虑应该给函数写上怎样的注释,然后想办法将注释变成函数名
  如果你看到一个函数名称不能很好的表达用途时应该马上修改,当然重排参数顺序也很重要
10.2 Add Parameter 添加参数
  为函数添加一个参数,这样就能为该函数带进来更多的信息
10.3 Remove Parameter 移除参数
  将无用的参数删掉
10.4 Separate Query from Modifier 将查询函数和修改函数分离
  若某函数即返回对象状态又修改对象,则应创建两个独立的函数各负其责
10.5 Parameterize Method 令函数携带参数
  一组签名相同的函数干了类似的事情,则可以将这些函数合并,并添加一个参数用来区分不同的行为
10.6 Replace Parameter with Explicit Methods 以明确函数取代参数
  若函数的行为完全依赖于参数的取值,则可以将该函数分裂成根据各个参数值不同而行为不同的一组小函数
10.7 Preserve Whole Object 保持对象完整
  若需要从某对象中捡取一组值然后传递给函数,可以考虑将该对象直接传递给函数,然后在函数内部再捡取这组需要的信息
10.8 Replace Parameter with Method 以函数取代参数
  在A(B(obj))中,若A可以直接B函数,可以修改为A(obj),在A内部完成对B(obj)的调用。
  如果函数可以通过非参数的形式获得信息,那就不应该采用参数
  若参数的存在是为了保持将来的弹性,那应该选择删除,只有在必要关头才添加参数
10.9 Introduce Parameter Object 引入参数对象
  若一组参数总是同时传递,则可以用一个参数对象来封装这组参数(此招我以前用过好几次,都非常有效)
10.10 Remove Setting Method 移除设值函数
  如果不希望对象的某个field被修改,请不要提供set函数,可以在对象初始化时给予该field合适的值
10.11 Hide Method 隐藏某个函数
  未被外部引用的函数应该写为private,对外接口越小越好
10.12 Replace Constructor with Factory Method 以工厂函数取代构造函数
  若创建对象时不仅仅做了简单的构造工作时可以考虑将构造函数替换为工厂函数
10.13 Encapsulate Downcast 封装"向下转型"动作
  将向下转型封装到单独的函数中
10.14 Replace Error COde with Exception 以异常取代错误码
  代码的可理解性应该是我们虔诚追求的目标
10.15 Replace Exception with Test 以测试取代异常
  对于可预见的合法性检测不应该用异常来实现,异常和错误应该明确的分开
 
第十一章 处理概括关系 Dealing with Generalization
11.1 Pull Up Field 值域上移
  若subclass含有相同的field,则可将此field移至superclass
11.2 Pull Up Method 函数上移
  若subclass含有相同的method,则可将此method移至superclass
11.3 Pull Up Constructor Body 构造函数本体上移
  若subclass中拥有一些代码几乎相同的构造函数,则可以在superclass中新建一个函数,并在subclass构造函数中调用它
11.4 Push Down Method 函数下移
  若superclass中的某个函数只与部分subclass相关,则将该函数移动到相关的的subclass中
11.5 Push Down Field 值域下移
  superclass中的某个field只被部分subclass引用,则可将该field移动到相关的subclass中
11.6 Extract Subclass 提炼子类
  class中的某些特性(features)只被某些实体用到,则可新建一个subclass,将相关部分的特性移到subclass中
11.7 Extract Superclass 提炼超类
  两个class有相似的特性,则可为这两个class建议一个superclass,并将相同的特性移至superclass
11.8 Extract Interface 提炼接口
  若干地方使用class接口中的同一子集,或者两个class接口有部分相同,则可将相同的子集提炼到独立的接口中
11.9 Collapse Hierarchy 折叠继承体系
  若superclass和subclass之间并无太大区别,则可将它们合为一体
11.10 From Template Method 塑造模板函数
  将子类中相似函数中的不同部分提炼为单独的函数,并将相同部分提炼到父类
11.11 Replace Inheritance with Delegation 以委托取代继承
  若subclass只使用superclass接口中的一部分,或是根本不需要集成而来的数据,可在subclass中新建一个field来持有superclass,将原来的继承关系变为聚合关系
11.12 Replace Delegation with Inheritance 以继承取代委托
  若两个class使用聚合关系,并经常需要将内部类的整个接口通过转接函数导出,则可以考虑将聚合关系改为继承关系
 
第十二章 大型重构 Big Refactorings
  不必一开始就对整个系统重构,重构程度只要能满足其他任务的需求就可以了,反正后面可以进行持续的重构
  进行大型重构时,有必要为整个团队建立共识,让大家意识到一件事情正发生着
12.1 Tease Apart Inheritance 梳理并分解集成体系
  若某个集成体系同时承担两项责任,则可以建立两个集成体系并通过转接让其中一个可以访问另一个
  比较安全的态度是一次一小步,不要过于躁进
  以退为进,走得更远
12.2 Convert Procedural Design to Objects 将过程设计转化为对象设计
  将数据记录变成对象,将行为分开,并将行为移入相关对象之中
12.3 Separate Domain from Presentation 将领域和表述/显式分离
  将GUI代码中的应用相关逻辑代码抽离出来
12.4 Extract Hierarchy 提炼继承体系
  若某class做了太多的工作,其中一部分工作是以大量条件式完成的,则可以建立继承体系,并以subclass表示一个特殊情况
 
第十三章 重构,复用与现实 Refactoring, Reuse, and Reality
  我们有责任努力发展自己的思想,并将它清楚的表达出来
  对C++程序来说,每次迭代(重新编译+测试)的成本太高了,所以C++程序员往往不太乐意经常做小改动
 
第十四章 重构工具 Refactoring Tools
  重构的最大障碍之一就是:几乎没有工具对它提供支持
 
第十五章 集成 Put It All Together
  做好重构:不论别人留下的代码多么杂乱无章,你都可以将它变好,好到足以进行后续的开发
  知道什么时候可以自信的停止重构
  只要有光,你就可以前进,虽然谨慎却仍然自信;但是一旦太阳下山,你就应该停止前进;夜晚你应该睡觉,并且相信明天早上太阳仍旧升起。
  你应该超目标前进,达到目标后就停止
  之所以重构,不是为了真善美(至少不全是),而是为了系统更容易的被人理解,为了防止程序变得散乱
  没有把握就停下来,要么发布成果,要么就撤销所有修改
  大规模的重构只会带来灾难。当看到混乱代码时应该慢慢的去解决问题。
  重构的时候不要修改功能性代码,即使发现可能的bug也先保留并记录下来,这些工作应该去单独的完成而不是和重构混杂在一起
 
原音重现 List of Soundbites
When you find you have to add a feature to a program, and the program's code is not structured in a convenient way to add the feature, first refactor the program to make it easy to add the feature, then add the feature.
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便的那么做,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
Before you start refactoring, check that you have a solid suite of tests. These tests must be self-checking.
重构前,先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力。
 
Refactoring changes the programs in small steps. If you make a mistake, it is easy to find the bug.
重构技术系以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
 
Any fool you can write code that a computer can understand. Good programmers write code that humans can understand.
任何一个傻瓜都能写出计算机可以理解的代码。惟有写出人类容易理解的代码,才是优秀的程序员。
 
Refactoring(noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior of the software.
重构(名词):对软件内部结构的一种调整,目的是在不改变“软件之可察行为”前提下,提高其可理解性,降低其修改成本。
 
Refactor(verb): to restructure software by applying a series of refactorings without changing the observable behavior of the software.
重构(名词):使用一系列重构准则(手法),在不改变“软件在可察行为”前提下,调整其结构
 
Three strikes and you refactor.
事不过三,过三重构。
 
Don't publish interfaces prematurely. Madify your code ownership policies to smooth refactoring.
不要过早发布接口。请修改你的代码拥有权政策,使重构更顺畅
 
When you feel the need to write a comment, first try to refactor the code so that any commentt becomes superfluous.
当你感觉需要撰写注释,请先尝试重构,试着让所有注释变得多余
 
Make sure all tests are fully antomatic and that they check their own results.
确保所有测试都完全自动化,让它们检查自己的测试结果
 
A suite of tests is a powerful bug detector that decapitates the time it takes to find bugs.
一整组测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间
 
Run your tests frequently. Localize tests whenever you compile - every test at least every day.
频繁的运行测试。每次编译请把测试也考虑进去——每天至少执行每个测试一次
 
When you get a bug report, start by writing a unit test that exposes the bug.
当你得到一个bug报告时,请先编写一个单元测试来重现此bug
 
It is better to write and run incomplete tests than no to run complete tests.
编写尚未完善的测试并实际运行,好过对完美测试的无尽等待
 
Think of the boundary conditions under which things might go wrong and concentrate your tests there.
考虑可能出错的边界条件,把测试火力集中在那儿
 
Don't forget to test that exceptions are raised when things are expected to go wrong.
当事情被大家认为应该会出错时,别忘了检查此时是否有异常被如期的抛出
 
Don't let the fear that testing can't catch all bugs stop you from writing the tests that will catch most bugs.
不要因为“测试无法捕捉所有的bug”,就不撰写测试代码,因为测试的确可以捕捉到大多数的bug。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多