函数
提炼函数
对于“何时应该把代码放进独立的函数”这个问题;可以采取“将意图与实现分开”:如果需要花时间浏览一段代码才能清楚它的功能,此时就可以提炼到一个函数中,并根据它所做的功能分开。
内联函数
当内部代码和函数名称同样清晰易读;可以去掉个函数,直接使用其中的代码。
改变函数声明
一个好名字能让我一眼看出函数的用途,而不必查看其实现代码。(先写一句注释描
述这个函数的用途,再把这句注释变成函数的名字)
// 简单
function circum(radius) {
return 2 * Math.PI * radius;
}
// 改成
function circumference(radius) {
return 2 * Math.PI * radius;
}
// 采用适配器
function circum(radius) {
return 2 * Math.PI * radius;
}
// 改成
function circum(radius) {
return circumference(radius);
}
function circumference(radius) {
return 2 * Math.PI * radius;
}
引入函数对象
一组数据项总是结伴同行,出没于一个又一个函数;替换成一个数据结构。
真正的意义在于,它会催生代码中更深层次的改变。一旦识别出新的数据结构,可以重组程序的行为来使用这些结构;
组合成类
如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),可以建一个类,类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。
组合变换
经常需要把数据传递给一个程序,让它再计算出各种派生信息。
这些派生数值可能会在几个不同地方用到,因此这些计算逻辑也常会在用到派生数据的地方重复。可以把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。
变量
提炼变量
面对一个复杂的表达式,变量能给其中的一部分命名,这样能更好地理解这部分逻辑。
使用提炼变量,就意味着要给代码中的一个表达式命名。一旦决定要这样做,就得考虑这个名字所处的上下文。但如果这个变量名在更宽的上下文中也有意义,就会考虑将其暴露出来,通常以函数的形式。
封装变量
如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。这样,把“重新组织数据”的困难任务转化为“重新组织函数”这个相对简单的任务。
封装能提供一个清晰的观测点,可以由此监控数据的变化和使用情况;可以轻松地添加数据被修改时的验证或后续逻辑。
变量改名
在另一个代码库中使用了该变量,这就是一个“已发布变量”(published variable),此时不能进行这个重构。如果变量值从不修改,可以将其复制到一个新名字之下,然后逐一修改使用代码,每次修改后执行测试。
内联变量
有时候,这个名字并不比表达式本身更具表现力,会妨碍重构附近的代码。应该通过内联的手法消除变量。
其他
拆分阶段
每当看见一段代码在同时处理两件不同的事,把它拆分成各自独立的模块,因为这样到了需要修改的时候,就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。
如果一块代码中出现了上下几封装段,各自使用不同的一组数据和函数;将这些代码片段拆
分成各自独立的模块,能更明确地标示出它们之间的差异。
封装
- 数据:封装数据对象、封装集合、以对象取代基本类型、以查询取代临时变量
- 函数、类:提炼类、内联类
- 交互:隐藏委托关系、移除中间人
-
封装数据对象
封装集合
只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。为避免此种情况,我会在类上提供一些修改集合的方法。不会在模块以外修改集合,仅仅提供这些修改方法。
以对象取代基本类型
往往决定以简单的数据项表示简单的情况,比如使用数字或字符串等。但随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。
一旦发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新类,只要类有了,日后添加的业务逻辑就有地可去了。以查询取代临时变量
将变量的计算逻辑放到函数中,也有助于在提炼得到的函数与原函数之间设立清晰的
边界,这能帮发现并避免难缠的依赖及副作用。
改用函数还避免了在多个函数中重复编写计算逻辑。提炼类
如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示应该将它们分离出去。
内联类
隐藏委托关系
如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口,变化会波及通过服务对象使用它的所有客户端。可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。
替换算法
发现做一件事可以有更清晰的方式,就会用比较清晰的方式取代复杂的方式。
随着对问题有了更多理解,往往会发现,在原先的做法之外,有更简单的解决方案,移除中间人
每当客户端要使用受托类的新特性时,就必须在服务端添加一个简单委托函数。随着受托类的特性(功能)越来越多,更多的转发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户直接调用受托类。
重新组织数据
拆分变量
变量有各种不同的用途,其中某些用途会很自然地导致临时变量被多次赋值。
变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。字段别名
记录结构中的字段可能需要改名,类的字段也一样。在类的使用者看来,取值和设值函数就等于是字段。对这些函数的改名,跟裸记录结构的字段改名一样重要。
以查询取代派生变量
将引用对象取代值对象
在把一个对象(或数据结构)嵌入另一个对象时,位于内部的这个对象可以被视为引用对象,也可以被视为值对象。两者最明显的差异在于如何更新内部对象的属性:
如果将内部对象视为引用对象,在更新其属性时,保留原对象不动,更新内部对象的属性;
- 如果将其视为值对象,会替换整个内部对象,新换上的对象会有我想要的属性值。
将值对象转化成引用对象
当出现共享数据时,使用值对象共享的数据需要更新,将其复制多份的做法就会遇到巨大的困难。此时我找到所有的副本,更新所有对象。只要漏掉一个副本没有更新,就会遭遇麻烦的数据不一致。
可以考虑将多份数据副本变成单一的引用,这样对顾客数据的修改就会立即反映在该顾客的所有订单中。
把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有这些实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。(保持单个实例共享)
简化条件逻辑
分解表达式
程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一;常常让弄不清楚为什么会发生;
和任何大块头代码一样,可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
合并条件表达式
有时会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。
以卫语句取代嵌套表达式
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”(guard clauses)。
function getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount();
if (isRetired) return retiredAmount();
return normalPayAmount();
}
以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如果使用if-then-else结构,你对if分支和else分支的重视是同等的。
这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。
卫语句就不同了,它告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”
引入特例
一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。
如果发现代码库中有多处以同样方式应对同一个特殊值,把这个处理逻辑收拢到一处。
统一判断null\undefined
;
重构API、函数
将查询函数与修改函数分开
任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离(Command-Query Separation)[mf-cqs]
如果遇到一个“既有返回值又有副作用”的函数,将查询动作从修改动作中分离出来。
函数参数化
如果发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。
移除标记参数
不喜欢标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该怎么调用。标记
参数却隐藏了函数调用中存在的差异性。
deliveryDate(aCustomer, true);
deliveryDate(aCustomer, false);
如果调用者传入的是程序中流动的数据,这样的参数不算标记参数;只有调用者直接传入字面量值,并且只有参数值影响了函数内部的控制流,这才是标记参数。
rushDeliveryDate(aCustomer);
regularDeliveryDate(aCustomer);
如果拆分逻辑比较复杂可以
function rushDeliveryDate (anOrder) {return deliveryDate(anOrder, true);}
function regularDeliveryDate(anOrder) {return deliveryDate(anOrder, false);}
保持对象完整性
如果看见代码从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数,更愿意把整个记录传给这个函数,在函数体内部导出所需的值。
“传递整个对象”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,就不用为此修改参数列表。
并且传递整个记录也能缩短参数列表,让函数调用更容易看懂。
如果有很多函数都在使用记录中的同一组数据,处理这部分数据的逻辑常会重复,此时可以把这些处理逻辑搬移到完整对象中去。
以查询取代对象
和任何代码中的语句一样,参数列表应该尽量避免重复,并且参数列表越短就越容易理解。
如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。这个本不必要的参数会增加调用者的难度,因为它不得不找出正确的参数值,其实原本调用者是不需要费这个力气的。
不使用以查询取代参数最常见的原因是,移除参数可能会给函数体增加不必要的依赖关系——迫使函数访问某个程序元素,原本不想让函数了解这个元素的存在。
如果想要去除的参数值只需要向另一个参数查询就能得到,这是使用以查询取代参数最安全的场景。
在处理的函数具有引用透明性(referentialtransparency,即,不论任何时候,只要传入相同的参数值,该函数的行为永远一致),这样的函数既容易理解又容易测试。
以参数取代查询
想要改变代码的依赖关系——为了让目标函数不再依赖于某个元素,把这个元素的值以参数形式传递给该函数。
注意权衡:如果把所有依赖关系都变成参数,会导致参数列表冗长重复;如果作用域之间的共享太多,又会导致函数间依赖过度。
判断标准是否具有“透明性”
移除设值函数
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段声明为不可变)。这样一来,该字段就只能在构造函数中赋值,
以工厂函数取代构造函数
构造函数的名字是固定的,因此无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的操作符来调用(在很多语言中是new关键字),所以在要求普通函数的场合就难
以使用。
工厂函数就不受这些限制。工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。
以命令取代函数
函数,不管是独立函数,还是以方法(method)形式附着在对象上的函数,是程序设计的基本构造块。不过,将函数封装成自己的对象,有时也是一种有用的办法。这样的对象我称之为“命令对象”(command object),或者简称“命令”(command)。这种对象大多只服务于单一函数,获得对该函数的请求,执行该函数,就是这种对象存在的意义。
与普通的函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。
有当特别需要命令对象提供的某种能力而普通的函数无法提供这种能力时,才会考虑使用命令对象。
以函数取代命令
命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;
以分别调用;开始调用之前的数据状态也可以逐步构建。但这种强大是有代价
的。大多数时候,是想调用一个函数,让它完成自己的工作就好。如果这个函数不是太复杂,那么命令对象可能显得费而不惠,就应该考虑将其变回普通
的函数
处理继承关系
函数上移
无论何时,只要系统内出现重复,就会面临“修改其中一个却未能修改另一个”的风险。
通常,找出重复也有一定的难度。如果某个函数在各个子类中的函数体都相同(它们很可能是通过复制粘贴得到的),这就是最显而易见的函数上移适用场合。
如果它们被使用的方式很相似,可以将它们提升到超类中去。
字段上移
函数下移
如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从
超类中挪走,放到真正关心它的子类中去。
字段下移
如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要
该字段的子类中。
构造函数本体上移
构造函数是很奇妙的东西。它们不是普通函数,使用它们比使用普通函数受
到更多的限制。看见各个子类中的函数有共同行为,第一个念头就是使用提炼函
数(106)将它们提炼到一个独立函数中,然后使用函数上移(350)将这个函数
提升至超类。
以子类取代类型码
软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类(工程师、经理、销售),订单可以按优先级分类(加急、常规)。表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符号、字符串或者数字。
如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用
移除子类
随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了.
提炼超类
如果看见两个类在做相似的事,可以利用基本的继承机制把它们的相似之处提炼到超类。可以用字段上移把相同的数据搬到超类,用函数上移搬移相同的行为。
折叠继承体系
随着继承体系的演化,有时会发现一个类与其超类已经没多大差别,不值得再作为独立的类存在。就会把超类和子类合并起来。
以委托取代子类
对于不同的变化原因,可以委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。因此,继承关系遇到问题时运用以委托取代子类是常见的情况。有一条流行的原则:“对象组合优于类继承”(“组合”跟“委托”是同一事)
以委托取代超类
使用委托
关系能更清晰地表达“这是另一个东西,只是需要用到其中携带的一些功能”这层意思。即便在子类继承是合理的建模方式的情况下,如果子类与超类之间的耦合过强,超类的变化很容易破坏子类的功能,还是会使用以委托取代超类。
总结
针对代码重构需要保持一定的原则:
- 高内聚、低耦合
- 易扩展
- 可读性(语义化)
- 理解难易
- 项目优先
- 保持对代码持续重构的热情
变量、数据
变量是数据的来源;大部分代码都是操作变量;好的变量的处理;能对代码有质的提升
- 提炼函数、内联函数、提炼类、内联类 - 函数体
- 改变函数声明(适配器模式、装饰者模式)、组合变换、组合类
-
交互、函数调用
函数、数据之间的交互
隐藏委托、移除中介、替换算法
- 以命令取代函数或者反之
- 提炼超类、以委托取代子类|超类
其他
警惕变化
发散变化、霰弹修改、
最后以上是从变量、函数、条件、交互、其他五个方面,来说明了重构各个方面;
可以看到重构并不是什么黑科技,需要多么高深的技巧;都是从点滴做起;小到变量的别名,大到函数的交互;而且相互之间在适当的条件下可以转化的如:提炼与内联之间、合并与分解;都是体现出抽象、封装、提炼等特点;
在编写代码时候;需要时刻记得这些特点;这样就可写出好的代码了。