《重构:改善既有代码的设计》
作者:(美)马丁·福勒(Martin Fowler)
重构的重新认识(再版序)
重构是在不改变软件可观察行为的前提下改善其内部结构。
重构的生活方式(译序)
在设计前期使用模式常常导致过度工程(over-engineering)。
想法:所以说在重构的时候再考虑该用什么模式去优化 !?
1.2 重构的第一步
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
想法:这是其中一种考虑是否要重构的方式
好的测试是重构的根本
想法:比如回归测试,在自动化测试完善的时候重构
1.3 分解并重组statement()
绝大多数情况下,函数应该放在它所使用的数据的所属对象内
2.2 为何重构
性能优化往往使代码较难理解,但为了得到所需的性能你不得不那么做。
想法:所以这就很尴尬,往往需要寻找两者的平衡点,或者其实二者并不是水火不容的?
添加新功能时,你不应该修改既有代码,只管添加新功能
想法:这算是重构的一个目标了吧
2.3 何时重构
重构应该随时随地进行。你不应该为重构而重构,你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。
想法:什么时候重构?
所以在安排开发计划时应该针对功能调整留出些许buffer
否则迫于deadline的压力,程序员只能毫无感情的往里面压入新的代码
事不过三,三则重构。
为了让过程正常运转,你的复审团队必须保持精练。就我的经验,最好是一个复审者搭配一个原作者,共同处理这些代码。复审者提出修改建议,然后两人共同判断这些修改是否能够通过重构轻松实现。果真能够如此,就一起着手修改。
如果是比较大的设计复审工作,那么在一个较大团队内保留多种观点通常会更好一些。此时直接展示代码往往不是最佳办法。我喜欢运用UML示意图展现设计,并以CRC卡展示软件情节。换句话说,我会和某个团队进行设计复审,而和单个复审者进行代码复审。
极限编程[Beck,XP]中的“结对编程”形式,把代码复审的积极性发挥到了极致。
想法:这是不是可以作为review制度的一种优化?
以后可以研究一下
或许可以从《极限编程》一书中窥见一二
2.4 怎么对经理说
2.4 怎么对经理说
想法:有点意思,作者可真是面面俱到
2.5 重构的难题
请将这种方法与“小心翼翼的事前设计”做个比较。推测性设计总是试图在任何一行代码诞生之前就先让系统拥有所有优秀质量,然后程序员将代码塞进这个强健的骨架中就行了。这个过程的问题在于:太容易猜错。如果运用重构,你就永远不会面临全盘错误的危险。程序自始至终都能保持一致的行为,而你又有机会为程序添加更多价值不菲的质量。
想法:事前设计总没办法尽善尽美
如果重构手法改变了已发布接口,你必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应。幸运的是,这不太困难。你通常都有办法把事情组织好,让旧接口继续工作。请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要复制函数实现,那会让你陷入重复代码的泥淖中难以自拔。你还应该使用Java提供的deprecation(不建议使用)设施,将旧接口标记为deprecated。这么一来你的调用者就会注意到它了。
2.6 重构与设计
重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运作。你可能只是试着做点测试,然后就发现代码中满是错误,根本无法稳定运作。记住,重构之前,代码必须起码能够在大部分情况下正常运作。
想法:以此为判断,什么时候就不重构直接重写算了
如果你选择重构,问题的重点就转变了。你仍然做预先设计,但是不必一定找出正确的解决方案。此刻的你只需要得到一个足够合理的解决方案就够了。你很肯定地知道,在实现这个初始解决方案的时候,你对问题的理解也会逐渐加深,你可能会察觉最佳解决方案和你当初设想的有些不同。只要有重构这把利器在手,就不成问题,因为重构让日后的修改成本不再高昂。
想法:再次说明了人的思维是有限的,不可能有完美的方案,重构应该贯穿产品开发的整个过程。也不必纠结于完美的事前设计,过度设计
3.2 Long Method(过长函数)
我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
想法:对于英语不好的人还是得看注释呀
3.3 Large Class(过大的类)
就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去
想法:有点狠
3.5 Divergent Change(发散式变化)
一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了
想法:发散的去想,如果修改不同问题的两个人在同一个地方产生冲突了,是否也是一种需要重构的信号呢?
3.14 Temporary Field(令人迷惑的暂时字段)
其内某个实例变量仅为某种特定情况而设
想法:经常搞这种事情
3.22 Comments(过多的注释)
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
4.1 自测试代码的价值
频繁进行测试是极限编程[Beck,XP]的重要一环
5.3 这些重构手法有多成熟
重构的基本技巧——小步前进、频繁测试
设计模式……为重构行为提供了目标
想法:所以设计模式也要学滴
6.1 Extract Method(提炼函数)
关键在于函数名称和函数本体之间的语义距离
就算函数名称比提炼出来的代码还长也无所谓。
第7章 在对象之间搬移特性
在对象的设计过程中,“决定把责任放在哪儿”即使不是最重要的事,也是最重要的事之一。我使用对象技术已经十多年了,但还是不能一开始就保证做对
想法:人家十多年了都没发完全搞清楚,咱也不用说事前设计时就一定要定下来
类往往会因为承担过多责任而变得臃肿不堪。
想法:这不正就是我们项目的现状嘛
7.1 Move Method(搬移函数)
搬移函数”是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就会搬移函数。通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务。
我常常会浏览类的所有函数,从中寻找这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多
想法:出轨的函数
。所以,我会凭本能去做,反正以后总是可以修改的。
想法:重构过还能再重构。这本来就是代码工作的一部分
7.2 Move Field(搬移字段)
随着系统的发展
想法:随着系统的发展。随着程序员能力的提升。这些都是重构的契机
在这个星期看似合理而正确的设计决策,到了下个星期可能不再正确。这没问题。如果你从来没遇到这种情况,那才有问题。
7.3 Extract Class(提炼类)
一个类应该是一个清楚的抽象,处理一些明确的责任。
先搬移较低层函数(也就是“被其他函数调用”多于“调用其他函数”者),再搬移较高层函数。
7.5 Hide Delegate(隐藏“委托关系”)
封装”即使不是对象的最关键特征,也是最关键特征之一。“封装”意味每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的对象就会比较少——这会使变化比较容易进行。
7.6 Remove Middle Man(移除中间人)
随着系统的变化,“合适的隐藏程度”这个尺度也相应改变
重构的意义就在于:你永远不必说对不起——只要把出问题的地方修补好就行了。
7.7 Introduce Foreign Method(引入外加函数)
重复代码是软件万恶之源
7.8 Introduce Local Extension(引入本地扩展)
使用包装类
想法:这种方式应该是我们用的比较多的
8.4 Change Reference to Value(将引用对象改为值对象)
Change Reference to Value
想法:看不懂
值对象有一个非常重要的特性:它们应该是不可变的
8.6 Duplicate Observed Data(复制“被监视数据”)
Duplicate Observed Data(复制“被监视数据
想法:没理解。先去看下观察者模式。后续再来看吧
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。
8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
在计算科学中,魔法数(magic number)是历史最悠久的不良现象之一。所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字
8.11 Encapsulate Collection(封装集合)
修改完所有此类情况之后,我可以让取值函数返回一个只读副本,用以确保没有任何一个用户能够通过取值函数修改集合:
public Set getCourses(){
return Collections.unmodifiableSet(_courses);
}
_skills = arg;
}
String[] _skills;
同样地,首先要提供一个修改函数。由于用户有可能修改数组中某一特定位置上的值,所以我提供的setSkill()必须能对任何特定位置上的元素赋值:
void setSkill(int index,String newSkill){
_skills[index] = newSkill;
}
如果我需要对整个数组赋值,可以使用下列函数:
void setSkills(String[] arg){
_skills = new String[arg.length];
for (int i = 0; i < arg.length; i++)
setSkill(i,arg[i]);
8.13 Replace Type Code with Class(以类取代类型码)
只有当类型码是纯粹数据时(也就是类型码不会在switch语句中引起行为变化时),你才能以类来取代它
我把Person中的类型码改为使用BloodGroup类:
想法:所以每次都要让他们翻译多语言是因为这里没搞好
8.14 Replace Type Code with Subclasses(以子类取代类型码)
一般来说,这种情况的标志就是像switch这样的条件表达式
如果宿主类中并没有出现条件表达式,那么Replace Type Code with Class (218)更合适,风险也比较低。
如果需要再加入新的行为变化,只需添加一个子类就行了
如果未来还有可能加入新行为,这项重构将特别有价值
8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码)
对象的类型码是可变的,所以我不能使用继承方式来处理类型码
第9章 简化条件表达式
较之于过程化程序而言,面向对象程序的条件表达式通常比较少,这是因为很多条件行为都被多态机制处理掉了。多态之所以更好,是因为调用者无需了解条件行为的细节,因此条件的扩展更为容易。所以面向对象程序中很少出现switch语句。一旦出现,就应该考虑运用Replace Conditional with Polymorphism (255)将它替换为多态。
9.1 Decompose Conditional(分解条件表达式)
像这样的情况下,许多程序员都不会去提炼分支条件。因为这些分支条件往往非常短,看上去似乎没有提炼的必要。但是,尽管这些条件往往很短,在代码意图和代码自身之间往往存在不小的差距
9.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”(guard clauses)[Beck]。
Replace Nested Conditional with Guard Clauses (250)的精髓就是:给某一条分支以特别的重视
如果对函数剩余部分不再有兴趣,当然应该立刻退出。引导阅读者去看一个没有用的else区段,只会妨碍他们的理解。
9.6 Replace Conditional with Polymorphism(以多态取代条件表达式)
Replace Conditional with Polymorphism(以多态取代条件表达式)
想法:尴尬。没怎么看懂这个例子
如果你需要在对象创建好之后修改类型码,就不能使用继承手法,只能使用State/Strategy模式
9.7 Introduce Null Object(引入Null对象)
只有当大多数客户代码都要求空对象做出相同响应时,这样的行为搬移才有意义
特例类的价值是:它们可以降低你的“错误处理”开销
9.8 Introduce Assertion(引入断言)
Introduce Assertion(引入断言)
想法:Java断言存在的意义是什么。和抛出错误有什么区别
实际上,程序最后的成品往往将断言统统删除
10.4 Separate Query from Modifier(将查询函数和修改函数分离)
任何有返回值的函数,都不应该有看得到的副作用
10.6 Replace Parameter with Explicit Methods(以明确函数取代参数)
Replace Parameter with Explicit Methods(以明确函数取代参数)
想法:就是说不要用万能函数
10.7 Preserve Whole Object(保持对象完整)
如果被调用函数使用了来自另一个对象的很多项数据,这可能意味该函数实际上应该被定义在那些数据所属的对象中。所以,考虑Preserve Whole Object (288)的同时,你也应该考虑Move Method (142)
10.9 Introduce Parameter Object(引入参数对象)
自从学到Range模式[Fowler,AP]之后,我就尽量以“范围对象”取而代之
11.11 Replace Inheritance with Delegation(以委托取代继承)
一开始继承了一个类,随后发现超类中的许多操作并不真正适用于子类
来自京东读书 for iOS
导出于 2021-02-19 20:34:43