1、如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构哪个程序,使特性的添加比较容易的进行,然后再添加特性
2、重构前,先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力
3、重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可发现它
4、任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员
5、重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
6、是不过三,三则重构
7、不要过早发布接口,请修改你的代码所有权政策,使重构更顺畅
8、当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余
9、确保所有的测试都自动化,让他们检查自己的测试结果。
10、一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间。
11、频繁地运行测试,每次编译请把测试也考虑进去,每天至少执行每个测试一次
12、每当你收到bug报告,请先写一个单元测试暴露这个bug
13、编写为臻完善的测试并实际运行,好过对完美测试的无尽等待。
14、考虑可能出错的边界条件,把测试火力集中在那里
15、不要因为测试无法捕捉所有bug就不写测试,因为测试的确定可以捕捉到大多数bug。
几乎任何情况下我都反对专门拨出时间进行重构。在我看来,重构本身就不是一键应该特别拔出时间做的事件,重构应该随时随地的进行。你不应该为了重构而重构,你之所以重构,只是因为你想做别的什么事情,而重构可以帮助你把哪些事情做好。
事不过三,三则重构
我们希望程序(1)容易阅读 (2)所有逻辑都只在唯一地点指定 (3)新的改动不会危及现有行为(4)尽可能简单表达条件逻辑
重构就是这样一个过程:它在一个目前可运行的程序上进行,在不改变程序行为的前提下使其具备上述美好性质,使我们能够继续保持高速开发,从而增加程序的价值。
每个重构手法都有如下5个部分
你应该检查每一个引用点,确定它的确指向你想要替换的东西
概要:将一段代码放进独立的函数中,并让函数名称解释该函数的用途。
简短而命名良好的函数。
函数多长才合适?关键在于函数名称和函数本体之间的语义距离,如果提炼可以强化代码的清晰度,那么就去做,就算函数名称比提炼出来的代码还长也无所谓。
以它“做什么”来命名,而不是以它“怎么做”命名
一个函数的本体与名称同样清楚易懂,在函数调用点插入函数本体,然后移除该函数。
如果你手上有一大群组织不合理的函数,你可以将它们都内联到一个大函数中,再从中提炼组织合理的小函数。
你有一个临时变量,只被一个简单表达式赋值过一次,而它妨碍了其他重构手法,将所有对该变量的引用动作,替换为对它赋值的那个表达式。
如果这个临时变量并未被申明为final,那就将它申明为final,然后编译,这个可以检查该临时变量是否真的只被赋值一次。
你的程序以一个临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立的函数中,将这个临时变量的所有引用点替换为对新函数的调用,此后,新函数就可以被其他函数使用。
临时变量的问题在于:它们是暂时的,而且只能在所属的函数内使用。
确保提炼出的函数无任何副作用,也就是说该函数并不修改任何对象内容
代码组织良好,你往往能够发现更有效的优化方案,如果没有进行重构,好的优化方案就可能与你失之交臂。
你有一个复杂的表达式,将该表达式(或者其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
表达式有可能非常复杂而难以理解。这种情况下,临时变量可以帮助你讲表达式分解为比较容易管理的形式。
你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果,针对每次赋值,创建一个独立、对应的临时变量。
代码对一个参数进行赋值,以一个临时变量取代该参数的位置。
不对参数赋值
有一个大函数,其中局部变量的使用使你无法差用extract method ,将这个函数放进一个单独的对象中,如此以来,局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小函数。
把某个算法替换为另一个更清晰的算法
程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或者被后者调用。在该函数最常引用的类中建立一个有着类似行为的新函数,将旧函数变成一个单纯的委托函数,或者是将旧函数完全移除。
检查源类中被源函数所使用的一切特性(包括字段和函数),考虑它们是否也该被搬迁。
程序中,某个字段被其所驻类之外的另一类更多的用到,在目标类中新建一个字段,修改源字段的所有用户,令它们改用新字段。
如果源字段不是private 就必须在源类的所有子类中查找源字段的引用点,并进行相应的替换。
小步前进
某个类做了应该由两个类做的事。建立一个新类,将相关的字段和函数从旧类搬移到新类。
每次搬迁后,编译、测试
某个类没有做太多事情,将这个类的所有特性搬移到另一个类中,然后移除原类。
如果一个类不再承担足够责任、不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任)。
客户通过一个委托类来调用另一个对象,在服务类上建立客户所需的所有函数,用以隐藏委托关系。
封装意味每个对象都应该尽可能减少了解系统的其他部分,
某个类做了过多的简单委托动作,让客户直接调用委托类
重构的意义就在于:你永远不必说对不起----只要把出问题的地方修补好就行了。
需要为提供服务的类增加一个函数,但你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类示例。
不要忘记,外加函数总归是权宜之计。如果有可能,你依然应该将这些函数搬移到它们的理想家园。
你需要为服务类提供一些额外函数,但你无法修改这个类,建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类或者包装类。
你直接访问一个字段,但与字段间的耦合关系逐渐变得笨拙,为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
你有一个数据项,需要与其他数据和行为一起使用才有意义,将数据项变成对象
为待替换数值新建一个类,在其中申明一个final字段,其类型和源类中的待替换数值类型一样。然后再新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数。
值对象应该是不可修改的内容。如果你希望给这个对象加入一下可以修改的数据,你需要将这个对象变成一个引用对象。
你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。将这个值对象变成引用对象。
你有一个引用对象,很小且不可变,而且不易管理,将它变成一个值对象
如果该对象目前还不是不可变的,就使用remove setting Method 直到它变成不可变为止
如果无法将该对象修改为不可变的,就放弃使用本项重构。
你有一个数组,其中的元素各自代表不同的东西,以对象替换数组,对于数组中的每一个元素,以一个字段表示。
你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据,将该数据复制到一个领域对象中,建立一个observe模式。已同步领域对象和GUI对象内的重复数据。
分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。
两个类都需要使用对方特性,但其间只有一条单向链接,添加一个反向指针,并使修改函数能够同时更新两条链接。
两个类之间有双向关联,但其中一个如今不再需要另一个类的特性,去除不必要的关联。
你有一个字面数值,带有特别含义,创建一个常量,根据其意义为他命名,并将上述的字面数值替换为这个常量。
类中存在一个public 字段 ,将它声明为private ,并提供相应的访问函数。
面向对象的首要原则之一就是封装,或者称为“数据隐藏”
有个函数返回一个集合。让这个函数返回该集合的一个只读副本,并在这个类中提供添加移除集合元素的函数。
取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者一无所知,这也会对用户暴露过多对象内部数据结构的信息。不应该为这整个集合提供一个设置函数,但应该提供用以为集合添加/移除元素的函数。
你需要面对传统编程环境中的记录结构,为该记录创建一个“哑”数据对象。
类之中有个数值类型码,但它并不影响类的行为,以一个新的类替换该数值类型码
你有一个不可变的类型码,他会影响类的行为,以子类取代这个类型码。
为了能够顺利进行重构,首先应该将类型码替换为可拥有多态行为的继承体系,这样的一个继承体系应该以类型码的宿主类为基类,并针对每一种类型码各建立一个子类。
你有一个类型码,他会影响类的行为,但你无法通过继承手法消除它,以状态对象取代类型码
个股子类唯一的差别只在“返回常量”的函数身上,修改这些函数,是他们返回超类中的某一个(新增)字段,然后销毁子类。
建立子类的目的是为了增加新特性或者变化其行为。
尽管常量函数有其用途,但若子类中只有常量函数,实在没有足够的存在价值
较之过程化程序而言,面向对象程序的条件表达式通常比较少,这是因为很多条件行为都被多态机制处理掉了,多态之所以更好,是因为调用者无需了解条件行为的细节,因此条件的扩展更为容易。
你有一个复杂的条件(if-then-else)语句。从if、then、else 三个段落中分别提炼出独立函数。
对于条件逻辑,将每个分支条件分解成新函数还可以 突出条件逻辑,更清楚的表明每个分支的作用,并且突出每个分支的原因。
一系列条件测试,都得到相同的结果,将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立函数。
将检查条件提炼成一个独立函数,对于厘清代码意义非常有用,因为他把描述“做什么”的语句换成了“为什么这样做”
如果你认为这些检查的确彼此独立,的确不应该被视为同一次检查,那么就不用使用本项重构。因为在这种情况下,你的代码已经清除表达出自己的意义。
在条件表达式的每一个分支上有着相同的一段代码。将这段复杂代码搬移到条件表达式之外。
在一系列布尔表达式中,某个变量带有“控制标记(control flag)”的作用,用break语句或者return语句取代控制标记
函数中的条件逻辑使人难以看清正常的执行路径,使用卫语句表现所有特殊情况。
如果两条分支都是正常行为,就应该使用形如if..else..的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在改条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。
给某一条分支以特别的重视。
卫语句要不就从函数中返回,要不就抛出一个异常。
有一个条件表达式,他根据对象类型的不同而选择不同的行为。将这个条件表达式的每一个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
你需要再三检查某个对象是否为null,将null值替换为null对象
某段代码需要对程序状态做出某种假设,以断言明确表现这种假设
注意,不要滥用断言。请不要使用它来检查“你认为应该为真”的条件,请只使用它来检查“一定必须为真”的条件。
明确的将“修改对象状态”的函数(修改函数)和“查询对象状态”的函数(查询函数)分开设计。
良好的接口只向用户展示必选展示的东西。如果一个接口暴露过多的细节,你可以将不必要暴露的东西隐藏起来,从而改进接口的质量。
函数的名称未能揭示函数的用途,修改函数名称。
给函数命名有个好方法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名。
想成为一个真正的编程高手,起名的水平是至关重要的。
某个函数需要从调用端得到更多的信息。为此函数添加一个对象参数,让改对象参数带进所需要的信息。
除了添加参数之外,你常常还有其他选择,只要可能,其他选择都比添加参数要好,因为它们不会增加参数列的长度。
函数主体不再需要某个参数,将改参数去除。
参数代表着函数所需要的信息,不同的参数值有不同的意义、。
某个函数即返回对象状态值,又修改对象状态,建立两个不同的函数,其中一个负责查询,另一个负责修改。
任何有返回值得函数,都不应该有看得见的副作用。
如果调用者将返回值赋值给了一个临时变量,你应该去掉这个临时变量
若干函数做了类似的工作,但在函数本体中却包含了不同的值。建立单一函数,以参数表达那些不同的值。
以“可将少量数值视为参数”为依据,找出带有重复性的代码
你有一个函数,其中完全取决于参数值而采用不同的行为。针对该参数的每一种可能值,建立一个独立函数
如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数值做出不同的行为,那么久应该使用本项重构、但是如果参数值不会对函数行为有太多影响,你就不应该使用本项重构
你从某个对象中取出若干值,将它们作为某一次函数调用时的参数,改为传递整个对象。
如果这会使你的依赖结构恶化,那么久不该使用本项重构
对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数。让参数接受者去掉该项参数,并直接调用前一个函数。
如果有必要将参数的计算过程提炼到一个独立函数中。
有些参数总是很自然的同事出现,以一个对象取代这些参数、
参数对象中的值可以把其中所有的字段都设置为final,只能由构造函数来赋值。这样可以避免很多困扰。
类中的某个字段应该在对象创建时被设值,然后就不再改变。去掉该字段的所有设值函数
如果你为某个字段提供了设值函数,这就暗示这个字段可以被改变。
有一个函数,从来没有被其他任何类用到,将这个函数改为private
你希望在创建对象时不仅仅是做简单的建构动作。将构造函数替换为工厂函数。
某个函数返回的对象,需要有函数调用者执行向下转型,将向下转型动作移动到函数中
某个函数返回一个特定的代码,用以表示某个错误情况,改用异常
面对一个调用者可预先检查的条件,你抛出了一个异常。修改调用者,是它在调用函数之前先做检查。
两个子类拥有相同的字段,将该字段移至超类
本项重构从两方面减少重复:首先它去除了重复的数据申明,其次它使你可以将使用该字段的行为从子类移至超类,从而去除重复的行为。
有些函数,在各个子类中产生完全相同的结果,将该函数移至超类
只要系统内出现重复,你就会面临“修改其中一个缺未能修改另一个”的风险
如果这些函数看上去做了相同的事情,但是并不完全一致,可以使用 substitute 爱老公人 让它们变得完全一致。
在各个子类中拥有一些构造函数,它们的本体几乎完全一致。在超类中新建一个构造函数,并在子类构造函数中调用它。
超类中的某个函数只与一部分(而非全部)子类有关。将这个函数移到相关的那些子类中去。
超类中某个字段只被部分(而非全部)子类用到,将这个字段移到需要它的那些子类中去。
类中的某些特性只能被某些(而非全部)实例用到,新建一个子类,将上面所说的那一部分特性移到子类中。
两个类有相似特性,为这两个类建立一个超类,将相同特性移至超类
若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。将相同的子集提炼到一个独立接口中。
超类和子类之间无太大区别。将它们和为一体。
有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了,然后将原函数上移至超类。
某个子类只使用了超类接口中的一部分,或是根本不需要继承而来的数据。在子类中新建一个字段用以保存超类,调整子类函数,令他该为委托超类,然后去掉两者之间的继承关系
你在两个类之间使用委托关系,并经常为整个接口编写许多简单至极的委托函数,让委托类继承受托类。
某个继承体系同时承担两项责任。建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
你手上有一些传统过程化风格的代码。将数据记录变成对象,将大块的行为分为小块,并将行为移入相关对象中。
某些GUI类之中包含了领域逻辑。将领域逻辑分离出来,为它们建立独立的领域类。
你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的。建立继承体系,以一个子类表示一种特殊情况。
将一个极度混乱的设计方案梳理出来,可能需要数周甚至数月的时间。你可以先进行本重构中的一些简易步骤,稍微休息一下,再花几天时间编写一些能体现产出的代码。
如何把重构和软件业者(特别是那些开发大型项目的软件业者)的日常事务结合起来
通过重新组织软件结构,重构使设计思路更详尽明确。重构被用于开发框架、抽取可复用组件、使软件框架更清晰、使新功能的增加更容易。重构可以帮助你充分利用以前的投资,减少重复劳动,使程序更简洁有力。
应对并处理变化,使软件开发的根本复杂性之一
重构也可以带来短期利益,让软件更容易修改、更易维护。重构只是一种手段,不是目的。它是“”程序员或者程序开发团队如何开发并维护自己软件”这一更宽广场景的一部分。
对员工进行培训、尽量获取短期利益、减少开销、安全引入新技术
重构的最大障碍之一就是:几乎没有工具对它提供支持。
refactoring browser 工具
这些技术如此精彩,可它们却仅仅只是个开始:因为你还不知道何时应该使用它们、合适不应该使用;何时开始、何时停止;何时前进、何时等待。使重构能够成功的,不是前面各种独立的技术,而是这种节奏
1、随时挑一个目标:某个地方的代码开始发臭,你就应该将问题解决掉。你应该朝目标前进,达成目标后即停止。
2、没把握就停下来:朝目标前进的过程中,可能会有这样的时候:你无法证明你所做的一切能够保成程序原本的语义。此时你就应该停下来,如果代码已经改善了一些,就发布你的成果;如果没有,就撤销所有的修改。
3:学习原路返回。重构的原则不好学,而且很容易遗矢准头。如果重构已经失控,你应该回到最近一个没有出错的状态,然后逐一重复刚才做过的重构,每次重构之后一定要运行所有测试。
4、二重奏:和别人一起重构、可以收到更好的效果
重构时你的目标之一就是保持代码的功能完全不变,即不多也不少。对于那些需要修改的东西,列个清单把它们记录下来,需要添加或者修改的测试用例,需要进行的其他重构、需要撰写的文档、需要画的图……都暂时记在卡上,这样就不会忘掉这些需要完成的工作。千万别让这些工作打乱你手上的工作。重构完成之后,再去做这些事情也不迟。