读书笔记_重构,改善既有代码的设计

来源:互联网 发布:批量上传淘宝宝贝软件 编辑:程序博客网 时间:2024/05/18 08:50

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

个人总结:以前对面向对象总觉得很一般的感觉,无法体会出面向对象真正带来的便利。而对于继承和多态,也只是了解知道怎么用,但是看了这本书才真正对于继承和多态有了进一步的理解,“原来继承和多态是这么用的”这种感觉,对于好的代码应该是怎样子的,也有了更具体一点的认知。对于以前自己写的代码,回想起来,根本就不是面向对象的,不堪回首……当然以后我还会不断发现自己以前写的代码很烂,这样的话,说明我在不断进步。

 

为什么重构:改善既有代码的设计;持续纠偏和改进软件设计;帮助发现隐藏的代码缺陷;从长远来看,有助于提高编程效率。(使代码更易于理解、扩展和修改)

第一章 重构,第一个案例

任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。

Ø  代码被阅读和修改的次数远远多于它被编写的次数。

Ø  外国人代码:各种类、接口、继承、方法调用。每个方法做的事情少,通过类图就可以看出逻辑。一个方法或类做的事情太多了,修改起来就很可能带来很多Bug,松耦合!

Ø  重构第一步:建议一组可靠的测试环境。好的测试是重构的根本。

Ø  代码块越小,代码的功能就越容易管理,代码的处理和移动也就越轻松。

Ø  任何不会被修改的变量都可以被我当成参数传入新的函数,至于会被修改的变量就要格外小心,如果只有一个变量会被修改,我可以把它当作返回值。

Ø  重构步骤的本质:由于每次修改的幅度都很小,所以任何错误都很容易发现。(基于测试)

Ø  如果为了提高代码的清晰度,需要修改某些东西的名字,那么就大胆去做吧。

Ø  我喜欢尽量除去这一类临时变量。临时变量往往引发问题,它们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易跟丢它们,尤其在长长的函数中根式如此。(当然这么做会也需付出性能上的代价,但是可以被优化。)

Ø  最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是别人的数据上使用。

Ø  重构手法:ExtractMethod、Move Method、Replace Conditional with Polymorphism、Self EncapsulateField、Replace Type Code with State/Strategy。

第二章 重构原则

Ø  何时重构

重构应该随时随地进行。

不是为了重构而重构,重构是为了帮助做好想做的事情。

事不过三,三则重构。(三次法则)

添加功能时重构。

修补错误时重构。

复审代码时重构。

Ø  你可以这么想:如果收到一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——没有清晰到让你能一眼看出bug。

Ø  程序有两面价值: “今天它可以为你做什么”和“明天它可以为你做什么”。不要为了完成今天的任务而不择手段,导致不能在明天完成明天的任务。

Ø  难以阅读的程序,难以修改;

逻辑重复的程序,难以修改;

添加新行为时需要修改已有代码的程序,难以修改;

带有复杂条件逻辑的程序,难以修改。

Ø  作者:我发现重构是理解软件的最快方式。

Ø  Dennis DeBruler:“计算机科学是这样一门科学:它相信所有问题都可以通过增加一个间接层来解决。”

Ø   “寄生式间接层”:你希望在不同的地点共享它,或者让它表现出多态性,最终却只在一处用到。那么请拿掉它。

Ø  “已发布接口”:需要修改的接口被那些“找不到,即使找到也不能修改”的代码使用。重构手法:尽量让旧接口调用新接口。或者同时维护新旧两个接口。

Ø  不要过早发布接口,请修改你的代码所有权政策,使重构更顺畅。

Ø  何时不该重构

现有代码根本不能正常运作。

项目接近最后期限,也应该避免重构。如果最后你没有足够时间,通常就表示你其实早该进行重构。

方法:重写(慎重)。折中办法:将“大块头软件”重构为封装良好的小型组件。

第三章 代码的坏味道

Ø  重复代码。DuplicatedCode。如果你在一个以上(两个及两个以上)的地点看到相同的程序结构,那么可以肯定:设法将它们合而为一,程序会变得更好。

Ø  过长函数。Long Method。早期的编程语言中,子程序调用需要额外开销,这使得人们不太乐意使用小函数。现代OO语言几乎已经完全免除了进程内的函数调用开销。不过阅读时需要京城转换上下文去看看子程序做了什么,故而一个好的函数名字显得更加重要。最终效果:你应该更积极地分解函数。原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。哪怕函数名比函数自身还长。

Ø  Replace Temp with Query,Introduce ParameterObject,Preserve Whole Object,Replace Method with Method Object,DecomposeConditional,

Ø  就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。

Ø  过大的类。Large Class

Ø  过长参数列。Long Parameter List

Ø  发散式变化。Divergent Change

Ø  霰弹式修改。Shotgun Surgery

Ø  依恋情结。Feature Envy

Ø  数据泥团。Data Clumps

Ø  基本类型偏执。Primitive Obsession。对象技术的新手通常不愿意在小任务上运用小对象。

Ø  switch惊悚现身。Switch Statements。面向对象程序的一个最明显的特征就是:少用switch语句。可以考虑用多态和继承来去除。

Ø  平行继承体系。Parallel Inheritance Hierarchies。为一个类添加子类,也必须为另外一个类添加子类的情况。策略:让一个继承体系的实例应用另一个继承体系的实例。

Ø  冗赘类。Lazy Class

Ø  夸夸其谈未来性。Speculative Generality

Ø  令人迷惑的暂时字段。Temporary Field

Ø  过度耦合的消息链。Message Chains

Ø  中间人。Middle Man

Ø  狎昵关系。Inappropriate Intimacy

Ø  异曲同工的类。Alternative Classes with Different Interfaces

Ø  不完美的类库。Incomplete Library Class

Ø  纯稚数据类。Data Class

Ø  被拒绝的遗赠。Refused Bequest

Ø  过多的注释。Comments。当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。

第四章 构筑测试体系

Ø  类应该包含它们自己的测试。(并且经常运行测试,可以提高生产)

Ø  Java则使用JUnit测试框架。

Ø  别人的代码,重构前构建测试体系。

第五章 重构列表

第六章 重新组织函数

Ø  提炼函数(Extract Method)。函数命名要尽量简短良好,细粒度:复用机会大,高层函数读起来就像注释,函数复写更容易。命名以“做什么”来命名。

Ø  尽量让一个函数只返回一个值,像C#那种支持“出参数”的语言可以,但还是不推崇。

Ø  内联函数(Inline Method)

Ø  内联临时变量(Inline Temp)。一个临时变量只被简单表达式赋值了一次,而妨碍了其他重构。

Ø  以查询取代临时变量(Replace Temp with Query)

Ø  引解释性变量(Introduce Explaining Variable)。复杂表达式中的结果放进一个临时变量,以此解释表达式用途。

Ø  分解临时变量(Split Temporary Variable)。某个临时变量被赋值超过一个,既不是循环变量也不用于收集计算结果,则针对每一次赋值,创造一个独立的对应的临时变量。

Ø  每个变量应该只承担一个责任,因此多了就应该分解。

Ø  移除对参数的赋值(Remove Assignments to Parameters)。代码对一个参数进行赋值,以一个临时变量取代该参数的位置。

Ø  我并不经常用final 来修饰参数,我会在较长的函数中使用它,让它帮我检查参数是否被做了修改。

Ø  以函数对象取代函数(Replace Method with Method Object)。大型函数中对局部变量的使用无法使用Extract Method。

Ø  替换算法(Substitute Algorithm)。把某个算法替换为一个更清晰的算法。

第七章 在对象之间搬移特性

Ø  搬移函数(Move Method)。有个函数与其所驻类之外的另一个类进行更多的交流:调用后者或者被后者调用。

Ø  搬移字段(Move Field)。某个字段被其所驻类之外的另一个类更多地用到。

Ø  提炼类(Extract Class)。某个类做了应该由两个类应该做的事。

Ø  将类内联化(Inline Class)。某个类没有做太多的事情。

Ø  隐藏“委托关系“(Hide Delegate)。客户通过一个委托类来调用另一个对象。在服务类上建立客户所需的所有函数,用以隐藏委托关系。

Ø  移除中间人(Remove Middle Man)。某个类做了过多的简单委托动作。

Ø  重构的意义在于:你永远不必说对不起——只要把出问题的地方修补好了就行了。

Ø  引入外加函数(Introduce Foreign Method)。你需要为提供服务的类增加一个函数,但你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。

Ø  引入本地扩展(Introduce Local Extension)。你需要为服务类提供一些额外函数,但你无法修改这个类。

第八章 重新组织数据

Ø  魔法数指代码中直接出现的数值,不要使用魔法数值,代之以有名字的Static final或者enum。

Ø  自封装字段(Self Encapsulate Field)。你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。为这个字段建立取值设值函数,并且只以这些函数来访问字段。

Ø  以对象取代数据值(Replace Data Value with Object)。你有一个数据项,需要与其他数据和行为一起使用才有意义。

Ø  将值对象改为引用对象(Change Value to Reference)。你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。

Ø  将引用对象改为值对象(Change Reference to Value)。你有一个引用对象,很小且不可改变,而且不易管理。

Ø  如果引用对象开始变得难以使用,也许就应该将它改为值对象。引用是对象必须被某种方式控制,你总是必须向其控制者请求适当地引用对象。它们可能造成内存区域之间错综复杂的关联。在分布系统和兵法系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。

Ø  以对象取代数组(Replace Array with Object)。数组中的原色各自代表不同的东西。以对象取代数组,对于数组中的每个元素,以一个字段来表示。

Ø  复制“被监视数据”(Duplicate Observed Data)。你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。

Ø  将单向关联改为双向关联(Change Unidirectional Association to Bidirectional)。两个类都需要使用对方特性,但其间只有一条单向连接。添加一个党项指针,并使修改函数能够同时更新两条连接。如果两者都是引用对象,关联是“一对多”关系,那么就由“拥有单一引用”的哪一方承担“控制者”角色;如果某个对象是组成另一个对象的部件,那么由后者负责控制关联关系;如果两者都是引用对象,关联是“多对多”关系,那么随便其中哪个对象来控制关联关系,都无所谓。

Ø  将双向关联该为单向关联(Change Bidirectional Associational to Unidirectional)。两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。双向关联很有用,但是需付出代价,那就是维护双向连接、确保对象被正确创建和删除而增加的复杂度。而且很多程序员并不习惯使用双向关联,它往往成为错误之源。大量的双向连接也很容易造成“僵尸对象”。

Ø  以字面常量来取代魔法数(Replace Magic Number with Symbolic Constant)

Ø  封装字段(Encapsulate Field)。你的类存在一个public字段。

Ø  封装集合(Encapsulate Collection)。有个函数返回一个集合。

Ø  以数据类取代记录(Replace Record with Data Class)。你需要面对传统编程环境中的记录结构。

Ø  以类取代类型码(Replace Type Code with Class)。类之中有一个数值类型码,但它不影响类的行为。

Ø  以子类取代类型码(Replace Type Code with Subclasses)。你有一个不可变的类型码,它会影响类的行为。

Ø  以State/Strategy取代类型码(ReplaceType with State/Strategy)。你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。

Ø  以字段取代子类(Replace Subclass with Fields)。你的各个子类的唯一差别只在“返回常量数据”的函数身上,修改这些函数,使他们返回超类中的某个(新增)字段,然后销毁子类。

第九章 简化条件表达式

Ø  分解条件表达式(Decompose Conditional)。你有一个负责的条件语句(if-else-then)。

Ø  合并条件表达式(Consolidate Conditional Expression)。你有一系列条件测试,都得到相同结果。

Ø  合并重复的条件片段(Consolidate Duplicate Conditional fragments)。在条件表达式我的每个分支上有着相同的一段代码。将这段重复代码搬移到条件表达式之外。

Ø  移除控制标记(Remove Control Flag)。在一系列布尔表达式中,某个变量带有“控制标记”的作用,以break语句或return语句取代控制标记。

Ø  以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)。函数中的条件逻辑使人难以看清正常的执行路径。使用卫语句表现所有特殊情况。

Ø  以多态取代条件表达式(Replace Conditional with Polymorphism)。你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。

Ø  引入Null对象(IntroduceNull Object)。你需要再三检查某对象是否为null,将null值替换为null对象。

Ø  引入断言(Introduce Assertion)。某一段代码需要对程序状态做出某种假设。以断言明确表现这种假设。这种假设是代码正确运行的必要条件。但是要谨慎使用断言。

第十章 简化函数调用

Ø  在对象技术中,最重要的概念莫过于“接口”,容易被理解和被使用的接口,是开发良好面向对象软件的关键。最简单也最重要的意见是就是修改函数名称。

Ø  多年来,我一直坚守一个很有价值的习惯:明确地将“修改对象状态”的函数(修改函数)和“查询对象状态”的函数(查询函数)分开设计。

Ø  良好的接口只向用户展现必须展现的东西。如果一个接口暴露了过多细节,你可以将不必要暴露的东西隐藏起来,从而改进接口的质量。含无异味,所有的数据都应该隐藏起来。

Ø  函数改名(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)。某个函数返回的对象,需要由函数调用者执行向下转型,则将向下转型动作移到函数中。

Ø  以异常取代错误码(Replace Error Code with Exception)。throw

Ø  以测试取代异常(Replace Exception with Test)

第十一章 处理概括关系(继承关系)

Ø  字段上移(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)。你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数,则让委托类继承受托类。

第十二章 大型重构

Ø  梳理并分解继承体系(Tease Apart Inheritance)。某个继承体系同时承担两项责任。建立两个继承体系,并通过委托关系让其中一个可以调用另一个。

Ø  将过程化设计转化为对象设计(Convert Procedural Design to Objects)。你手上有一些传统过程化风格的代码,将数据记录编程对象,将大块的行为分成小块,并将行为移入相关对象之中。

Ø  将领域和表述/显示分离(SeparateDomain from Presentation)。某些GUI类之中包含了领域逻辑,将领域逻辑分离出来,为它们建立独立的领域类。

Ø  提炼继承体系(Extract Hierarchy)。你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的,则应该建立继承体系,以一个子类表示一种特殊情况。

第十三章 重构,复用与现实

Ø  对设计模式的很多研究,都集中于良好编程风格以及程序各部分之间有用的交互模式,而这些都可以映射为结构特征和重构手法。

Ø  经验是无可替代的。

Ø  研究人员的典型尴尬处境——技术的发展超前于实践。(如果可以好好利用这一点现实或许可以开辟一番新天地)

Ø  自动化重构工具:http://st-www.cs.uiuc.edu

第十四章 重构工具

Ø  Refactoring Browser

第十五章 总结

Ø  前面列出的技术仅仅是一个七点,是你登堂入室之前的大门。如果没有这些技术,你根本无法对运行中的程序进行任何涉及上的改动。有了这些技术,你仍然做不到,但起码可以开始尝试了。

Ø  使重构能够成功的,不是前面各自独立的技术,而是这种节奏:知道何时应该使用它们、何时不应该使用,何时开始、何时停止,何时前进、何时等待。

Ø  大多时候,“得道”的标志是:你可以自信地停止重构。如果你再也无法前进一步,也许就在你停止时遇到了这种挫折,如果代码比重构之前好,就集成到系统中发布成果,如果代码没有变好就果断放弃这些无用的工作,回到起始点,然后为自己学到一课而高兴,不久的将来,灵感总会来的。这有点像在悬崖峭壁上的小径行走:只要有光,你就可以前进,虽然谨慎却仍然自信。但是一旦太阳下山,你就应该停止前进;夜晚你应该睡觉,并且相信明天早晨太阳仍然升起。

Ø  学习重构:

Ø  1、随时挑一个目标,某个地方的代码开始发愁了,你就应该将问题解决掉。

Ø  2、没把握就停下来,如果代码已经改善就发布成果,如果没有就撤销所有修改。

Ø  3、学习原路返回,回到最近一个没有出错的状态,然后逐一重复刚才做过的重构想,每一次重构之后一定要运行所有测试。重现做过的重构比调试平均时间短。

Ø  4、二重奏,和别人一起重构,可以收到更好的效果。

原创粉丝点击