### 重构与设计
「重构」肩负一项特别任务:它和设计彼此互补。初学编程的时候,我埋头就写程序,浑浑噩噩地进行开发。然而很快我便发现,「事先设计」(upfront design)可 以助我节省回头工的高昂成本。于是我很快加强这种「预先设计」风格。许多人都把设计看做软件开发的关键环节,而把编程(programming)看做只是机械式的低级劳动。他们认为设计就像画工程图而编码就像施工。但是你要知道,软件和真实器械有着很大的差异。软件的可塑性更强,而且完全是思想产品。正如Alistair Cockburn所说:『有了设计,我可以思考更快,但是其中充满小漏洞。』
有一种观点认为:重构可以成为「预先设计」的替代品。这意思是你根本不必做任何设计,只管按照最初想法开始编码,让代码有效运作,然后再将它重构成型。事实上这种办法真的可行。我的确看过有人这么做,最后获得设计良好的软件。极限编程〔Extreme Programming [Beck, XP] 的支持者极力提倡这种办法。
尽管如上所言,只运用重构也能收到效果,但这并不是最有效的途径。是的,即使极限编程(Extreme Programming)爱好者也会进行预先设计。他们会使用CRC卡或类似的东西来检验各种不同想法,然后才得到第一个可被接受的解决方案,然后才能开始编码,然后才能重构。关键在于:重构改变了「预先设计」的角色。如果没有重构,你就必须保证「预先设计」正确无误,这个压力太大了。这意味如果将来需耍对原始设计做任何修改,代价都将非常高昂。因此你需要把更多时间和精力放在预先设计上,以避免日后修改。
如果你选择重构,问题的重点就转变了。你仍然做预先设计,但是不必一定找出正确的解决方案。此刻的你只需要得到一个足够合理的解决方案就够了。你很肯定地知道,在实现这个初始解决方案的时候,你对问题的理解也会逐渐加深,你可能会察觉最佳解决方案和你当初设想的有些不同。只要有重构这项武器在手,就不成问题,因为重构让日后的修改成本不再高昂。
这种转变导致一个重要结果:软件设计朝向简化前进了一大步。过去未曾运用重构时,我总是力求得到灵活的解决方案。任何一个需求都让我提心吊胆地猜疑:在系统寿命期间,这个需求会导致怎样的变化?由于变更设计的代价非常高昂,所以我希望建造一个足够灵活、足够强固的解决方案,希望它能承受我所能预见的所有需求变化。问题在于:要建造一个灵活的解决方案,所需的成本难以估算。灵活的解决方案比简单的解决方案复杂许多,所以最终得到的软件通常也会更难维护——虽然它在我预先设想的方向上的确是更加灵活。就算幸运走在预先设想的方向上,你也必须理解如何修改设计。如果变化只出现在一两个地方,那不算大问题。然而变化其实可能出现在系统各处。如果在所有可能的变化出现地点都建立起灵活性,整个系统的复杂度和维护难度都会大大提高。当然,如果最后发现所有这些灵活性都毫无必要,这才是最大的失败。你知道,这其中肯定有些灵活性的确派不上用场,但你却无法预测到底是哪些派不上用场。为了获得自己想要的灵活性,你不得不加入比实际需要更多的灵活性。
有了重构,你就可以通过一条不同的途径来应付变化带来的风险。你仍旧需要思考潜在的变化,仍旧需要考虑灵活的解决方案。但是你不必再逐一实现这些解决方案,而是应该问问自己:『把一个简单的解决方案重构成这个灵活的方案有多大难度?』如果答案是「相当容易」(大多数时候都如此〉,那么你就只需实现目前的简单方案就行了。
重构可以带来更简单的设计,同时又不损失灵活性,这也降低了设计过程的难度,减轻了设计压力。一且对重构带来的简单性有更多感受,你甚至可以不必再预先思考前述所谓的灵活方案——一 旦需要它,你总有足够的信心去重构。是的,当下只管建造可运行的最简化系统,至于灵活而复杂的设计,唔,多数时候你都不会需要它。
**劳而无获**
—— Ron Jeffries
Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)的支付过程太慢了。虽然我们的开发还没结束,这个问题却已经幵始困扰我们,因为它已经拖累了测试速度。
Kent Beck, Martin Fowler和我决定解决这个问题。等待大伙儿会合的时间里,凭着我对这个系统的全盘了解,我开始推测:到底是什么让系统变慢了?我想到数种可能,然后和伙伴们谈了几种可能的修改方案。最后,关于「如何让这个系统运行更快」,我们提出了一些真正的好点子。
然后.我们拿Kent的量测工具度量了系统性能。我一开始所想的可能性竞然全都不是问题原因。我们发现:系统把一半时间用来创建「日期」实体(instance)。更有趣的是, 所有这些实体都有相同的值。
于是我们观察日期的创建逻辑,发现有机会将它优化。日期原本是由字符串转换而生,即使无外部输入也是如此。之所以使用字符串转换方式,完全是为了方便键盘输入。好, 也许我们可以将它优化。
于是我们观察日期怎样被这个程序运用。我们发现,很多日期对象都被用来产生「日期区间」实体(instance)。「曰期区间」是个对象,由一个起始日期和一个结束日期组成。仔细追踪下去,我们发现绝大多数日期区间是空的!
处理日期区间时我们遵循这样一个规则:如果结束日期在起始日期之前,这个日期区间就该是空的。这是一条很好的规则,完全符合这个class的需要。釆用此一规则后不久,我们意识到,创建一个「起始日期在结束日期之后」的日期区间,仍然不算是清晰的代码,于是我们把这个行为提炼到一个factory method(译注:一个著名的设计模式, 见《Design patterns》,由它专门创建「空的曰期区间I 。
我们做了上述修改,使代码更加清晰,却意外得到了一个惊喜。我们创建一个固定不变的「空日期区间」对象,并让上述调整后的fatory method每次都返回该对象,而不再每次都创建新对象。这一修改把系统速度提升了几乎一倍,足以让测试速度达到可接受程度。这只花了我们大约五分钟。
我和团队成员〔Kent和Martin谢绝参加)认真推测过:我们了若指掌的这个程序中可能有什么错误?我们甚至凭空做了些改进设计,却没有先对系统的真实情况进行量测。
我们完全错了。除了一场很有趣的交谈,我们什么好事都没做。
教训:哪怕你完全了解系统,也请实际量测它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。
- 译序 by 侯捷
- 译序 by 熊节
- 序言
- 前言
- 章节一 重构,第一个案例
- 起点
- 重构的第一步
- 分解并重组statement()
- 运用多态(Polymorphism)取代与价格相关的条件逻辑
- 结语
- 章节二 重构原则
- 何谓重构
- 为何重构
- 「重构」助你找到臭虫(bugs)
- 何时重构
- 怎么对经理说?
- 重构的难题
- 重构与设计
- 重构与性能(Performance)
- 重构起源何处?
- 章节三 代码的坏味道
- Duplicated Code(重复的代码)
- Long Method(过长函数)
- Large Class(过大类)
- Long Parameter List(过长参数列)
- Divergent Change(发散式变化)
- Shotgun Surgery(散弹式修改)
- Feature Envy(依恋情结)
- Data Clumps(数据泥团)
- Primitive Obsession(基本型别偏执)
- 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(过多的注释)
- 章节四 构筑测试体系
- 自我测试代码的价值
- JUnit测试框架
- 添加更多测试
- 章节五 重构名录
- 重构的记录格式
- 寻找引用点
- 这些重构准则有多成熟
- 章节六 重新组织你的函数
- Extract Method(提炼函数)
- Inline Method(将函数内联化)
- Inline Temp(将临时变量内联化)
- Replace Temp with Query(以查询取代临时变量)
- Introduce Explaining Variable(引入解释性变量)
- Split Temporary Variable(剖解临时变量)
- Remove Assignments to Parameters(移除对参数的赋值动作)
- Replace Method with Method Object(以函数对象取代函数)
- Substitute Algorithm(替换你的算法)
- 章节七 在对象之间搬移特性
- Move Method(搬移函数)
- Move Field(搬移值域)
- Extract Class(提炼类)
- Inline Class(将类内联化)
- Hide Delegate(隐藏「委托关系」)
- Remove Middle Man(移除中间人)
- Introduce Foreign Method(引入外加函数)
- Introduce Local Extension(引入本地扩展)
- 章节八 重新组织数据
- Self Encapsulate Field(自封装值域)
- Replace Data Value with Object(以对象取代数据值)
- Change Value to Reference(将实值对象改为引用对象)
- Replace Array with Object(以对象取代数组)
- Replace Array with Object(以对象取代数组)
- Duplicate Observed Data(复制「被监视数据」)
- Change Unidirectional Association to Bidirectional(将单向关联改为双向)
- Change Bidirectional Association to Unidirectional(将双向关联改为单向)
- Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)
- Encapsulate Field(封装值域)
- Encapsulate Collection(封装群集)
- Replace Record with Data Class(以数据类取代记录)
- Replace Type Code with Class(以类取代型别码)
- Replace Type Code with Subclasses(以子类取代型别码)
- Replace Type Code with State/Strategy(以State/strategy 取代型别码)
- Replace Subclass with Fields(以值域取代子类)
- 章节九 简化条件表达式
- Decompose Conditional(分解条件式)
- Consolidate Conditional Expression(合并条件式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)
- Replace Conditional with Polymorphism(以多态取代条件式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
- 章节十一 处理概括关系
- 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(将过程化设计转化为对象设计)
- Separate Domain from Presentation(将领域和表述/显示分离)
- Extract Hierarchy(提炼继承体系)
- 章节十三 重构,复用与现实
- 现实的检验
- 为什么开发者不愿意重构他们的程序?
- 现实的检验(再论)
- 重构的资源和参考资料
- 从重构联想到软件复用和技术传播
- 结语
- 参考文献
- 章节十四 重构工具
- 使用工具进行重构
- 重构工具的技术标准(Technical Criteria )
- 重构工具的实用标准(Practical Criteria )
- 小结
- 章节十五 集成
- 参考书目