重构改善既有代码的设计
序
重构就不能是一项靠着天份挥洒的艺术,必须是一项工程。
小步与缓迟前进,不过激,不躁进,再加上完整的测试配套(测试之于重构极其重要),才是「不带来破坏,不引入臭虫」的最伴保障。
自动化重构工具。
设计不再是一切动作的前提,而是在整个开发过程中逐渐浮现出来。
我比较担心,阅历不足的程序员在读过本书后可能发酵出「先动于再说,死活可重构,的心态,轻忽了事前优秀设计的重要性。
但是,Joshua Kerievsky在那篇著名的《模式与XP》(收录于《极限编程研究》一书)中明”地指出:在设计前期使用模式常常导致过度工程(over-engineering),这是一个残酷的现实,单凭对完美的追求无法写出实用的代码,而「实用」是软件压倒一切的要素。从一篇《停止过度工程》开始,Joshua撰写了”Refactoring to Patterns”系列文章。这位犹太人用他民族性的睿智头脑,敏锐地发现了软件的后结构主义道路。而让设计模式在飞速变化的Internet时代重新闪现光辉的,又是重构的力量。
首先,把你的敬畏到大西洋里去,对于即将变得像空气与水一样普通的技术,你无须对它敬畏;其次,找到合适的开发工具(如果你和我一样是Java人,那么这个「合适的工具」就是Eclipse),学会使用其中的自动测试和重构功能,然后再尝试使用本书介绍的任何技术。懒情是程序员的美德之一,绝不要因为这本书让你变得勤快。
最后,即使你完全掌握了这本书中的所有东西,也下方不要跟别人吹嘘。在我们的团队里,程序员常常会说:「如果没有单元测试和重构,我没办法写代码」。
重构(refactoring)这个概念来自Smalltalk圈子,没多久就进入了其他语言阵营之中。由于重构是framework(框架)开发中不可缺少的一部分,所以当framework开发人员讨论自已的工作时,这个术语就诞生了。当他们精炼自己的class hierarchies(类阶层体系)时,当他们叫喊自已可以拿掉多少多少行代码时,重构的概念慢慢浮出水面。framework设计者知道,这东西不可能一开始就完全正确,它将随着设计者的经验成长而进化;他们也知道,代码被阅读和被修改的次数远远多于它被编写的次数。保持代码易读、易修改的关键,就是重构一对framework而言如此,对一般软件也如此,
为了避免自掘坟墓,重构必须系统化进行。
本书的核心是一份完整的重构名录(catalog of refactoring),其中每一项都介绍一种经过实证的代码变换于法(code transformation)的动机和技术。
所谓重构是这样一个过程:“在不改变代码外在行为的前提下,对代码做出修改以改进程序的内部结构”。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法,可以将整理过程中不小心引入错误的机率降到最低。本质上说,重构就是「在代码写好之后改进它的设计」。
为了最大程度地帮助读者理解我的想法,我不想使用Jaya语中别复杂的部分。所以我避免使用innerclass(内嵌类)、reflection(反时机制),thread(线程)以及很多强人的Java特性,这是因为我希望尽可能清楚展现重构的核心。
我应该提醒你,这些重构准则并不对并发(concurrent)或分布式(distributed)编程。那些主题会出更多重要的事,超越了本书的关心范围。
坏味道及其重构方式
坏味道(smell) | 常用的重构手法(Common Refactoring) |
---|---|
Duplicated Code, p76 | Extract Method (110),Extract Class (149),Pull Up Method(322),Form Template Method(345) |
Comments, p87 | Extract Method(110),introduce Assertion,(267) |
Data Class, p86 | Move Method (142),Encapsulate Fieid(206),Encapsulate Collection(208) |
Large Class, p78 | Extract Ciass (149), Extract Subclass (330), Extract Interface(341),Replace Data Value with Object (175) |
lazy Class, p83 | Inline Class (154), Collapse Hierarchy(344) |
Long Method,p76 | Extract Method(110),Replace Temp With Query(120),Replace Method with Method Object (135),Decompose Conditional(238) |
Long Parameter List,p78 | Replace Parameter with Method(292).Introduce Parameter Object(295).Preserve Whole Obiject(288) |
Primitive Obsession, p81 | Replace Data Value with Object(175),Extract Class(149),introduce Parameter Object (295),Replace Array with Object (186),Replace Type Code with Class(218),Replace Type Code with Subclasses (223),Replace Type Code with State/Strategy(227) |
Switch Statements, p82 | Replace Conditional with Polymorphism(255),Replace Type Code with Subclasses(223),Replace Type Code with State/Strategy(227),Replace Parameter with Explicit Methods(285),introduce Null Object(260) |
Temporary Field, p84 | Extract Class (149), introduce Null Object (260) |
第一个案例
如果你发现自己需要为理序添加一个特性而代码结构使你法很便地那么做那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
测试的重要性
每当我要进行重构的时候,第一个步骤永远相同:我得为即将修改的代码建立一组可靠的测试环境
。这些测试是必要的,因为尽管避循重构准则可以使我避免绝大多数的臭虫引入机会,但我毕竟是人,毕竟有可能犯错。所以我需要可靠的测试。
重构之前,首先检查自已是否有一套可靠的测试机制。这些测试必须有自我检验(self-checking)能力。
本例一个明显的逻辑泥团就是switch语句,把它提炼(extract)到独立函数中似乎比较好。
和任何重构准则一样,当我提炼一个函数时,我必须知道可能出什么错。如果我提炼得不好,就可能给程序引入臭虫。所以重构之前我需要先想出安全作法。由于先前我已经进行过数次这类重构,所以我已经把安全步骤记录于书后的重构名录(refactoring catalog)中了。
首先我得在这段代码里头找出函数内的局部变量(local variables)和参数(parameters)。我找到了两个:each和thisAmount,前者并未被修改,后者会被修改。任何不会被修改的变量都可以被我当成参数传入新的函数,至于会被修改的变量就需格外小心。如果只有一个变量会被修改,我可以把它当作返回值。thisAmount是个临时变量,其值在每次循环起始处被设为0,并且在switch语句之前不会改变,所以我可以直接把新函数的返回值赋予它。
领域、子域和限界上下文
由于“领域模型”包含了“领域”这个词,我们可能会认为应该为整个业务系统创建一个单一的、内聚的、全功能式的模型。然而,这并不是我们使用DDD的目标。正好相反,在DDD中,一个领域被分为若于子域,领域模型在限界上下文中完成开发。事实上,在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。试图创建一个全功能的领域模型是非常困难的,并且很容易导致失败。就像本章中所讲到的一样,对领域的拆分将有助于我们成功。
在正确实施DDD的情况下,这种简单的子域可以以模块(Module,9)的形式从核心域中分离出来,而不需要包含在笨重的子系统组件中。
需要注意的是,一个限界上下文并不一是只包含在一个子域中,但这是可能的。
哪种类型的限界上下文在语言表达层面上设计得更好呢?换句话说,哪种限界上下文拥有非歧义的领域特定术语?
在图2.1中,我们看不出以上这些库存概念。在DDD中,我们不能靠猜测,而应该对每个概念都给出明确的定义,并将这些明确的定义用在交流和建模中。领域专家对这些概念的解释有助于在不同的限界上下文中分离这些概念。
集成的方式有很多种,我们将在上下文映射图(3)中学到不同的集成方案。
pp37