大话重构 之 消除过长方法
在面向对象程序中活的最好最长久的是短方法。对于新手而言,很困恼面向对象的程序中完全找不到计算逻辑,反而是无穷无尽的方法调用,但是当你习惯面向对象后就会了解到短方法的价值所在。
短方法的价值
从较早的时候,程序员们就发现方法越长就越难以理解,但由于老的编程语言在方法调用上的开销使得很多人放弃了用短方法。在现代的面向对象语言中,这一开销已经非常小,这不应该再是我们使用长方法的理由。
短方法可以极大地增强代码的可读性。
- 一个好的方法命名可以使你不看方法实现就能知道它的功能,重点就在于一个好的方法命名;
- 使用短方法,可以给业务逻辑提供合适的抽象层级。
方法的抽象层级可能不太容易理解,举些例子。比如部门的组织架构,主管下面有多个项目经理,项目经理下面有多个项目组长,项目组长下面有多个小兵,一个好的主管会把目光聚焦在几个项目经理身上,而不是给每个小兵派具体的任务。再如网络通讯的协议栈,从应用层到物理链路层,每层都构建在下一层之上,都有自己清晰的界限。方法的抽象层级也一样,每个方法做一个级别的事情,而不是把所有细节堆到一个方法里面,跟一锅粥一样。
什么场景下用短方法
很简单的一点就是看方法体内的注释。方法体内的注释通常意味着语义上的背离,即看代码块实现很难理解它的意图。下面是几种常见需要短方法的场景:
- 如果一个方法里面,有多个代码块,每个代码快都有注释,这是使用短方法的绝佳时机;
- 如果想给方法里面的某个代码块写注释,也需要提取一个短方法;
- 如果发现不同的地方有重复,提取短方法就是第一步。请参考消除重复代码
比较极端的情况下,甚至只有一行代码,也值得提取一个短方法。
再次提醒下各位看官,问题的关键点在于语义上的差别:方法名可以说明意图,而代码块自身只能说明是如何实现的。
提示:代码是给人读的,要说明意图,而不仅仅是如何实现。
如何消除过长方法
如下面的代码:
def process {
try {
// 初始化
openA()
openB()
// 执行业务逻辑
...
...
} finally {
// 清理
closeB()
closeA()
}
}
一个方法体内有注释说明的多个代码快,是典型特征。解决的办法简单明了,多次 提取方法。
def process {
try {
initialize()
execute()
} finally {
clean()
}
}
def initialize {
openA()
openB()
}
def execute = { ... }
def clean {
closeB()
closeA()
}
重构后的四个方法都只做了一个抽象级别的事情,赏心悦目。
上面的例子非常简单,但是只要你开始实施 消除过长方法,不久后一定会出现一个绊脚石,方法内有大量的局部变量。如果任由这些局部变量存在,而实施 提取方法 进行重构,那重构出来的多个短方法会有很多参数,MLGB,还不如不重构。
咋办呢?莫急。
如何应对诸多局部变量
应对局部变量根据场景不同,可分为如下四种。
▶ 用查询替代变量
val basePrice = this.quality * this.itemPrice
if (basePrice > 1000)
basePrice * 0.95
else
basePrice * 0.98
把局部变量换成方法,随时可以调用。
if (basePrice > 1000)
basePrice * 0.95
else
basePrice * 0.98
def basePrice = this.quality * this.itemPrice
有人会说了,重构后的代码性能会降低。嗯,确实降低了,有些情况下,可读性和性能是冲突的,但是如果小小的性能牺牲可以换来更高的可读性,我会毫不犹豫地选择可读性。当然在性能消耗特别大的情况下。。。依然可以用这种方式,先 提取方法,然后在提取出来的方法里面使用缓存,两全其美。
▶ 引入参数对象
val x1 = ...
val y1 = ...
val x2 = ...
val y2 = ...
calcDistance(x1, y1, x2, y2)
引入一个代表点的参数类Point,然后重载一个calcDistance方法。
val point1 = new Point(..., ...)
val point2 = new Point(..., ...)
calcDistance(point1, point2)
case class(x: Double, y: Double)
case class是scala的一种特性,相当于C++里面的struct,java里面的数据类,有兴趣的同学可以google下。哦,google不能用了,用www.gfsoso.net吧。当然case class的魔法不是这个,是和构造函数对应的解构函数,专业点叫 模式匹配。额,继续gfsoso下吧。
▶ 保持对象完整
val begin = dateRange.begin
val end = dateRange.end
withinPlan = plan.withinRange(begin, end)
重载个withinRange方法,支持直接传入完整的DateRange对象啊。
withinPlan = plan.withinRange(dateRange)
▶ 用方法对象替代方法
class Account {
def gamma(value: Int, quantity: Int, date: Int) = {
val value1 = value * quantity + detla()
var value2 = value * date + 100
if ((date - value1) > 100)
value2 -= 20
val value3 = value2 * 7
value3 - 2 * value1
}
}
我要把if语句那段提取出来,因为它太重要(不是真的太重要,是为了说明 用方法对象替代方法)。
class Account {
def gamma(value: Int, quantity: Int, date: Int) = {
new Gamma(value, quantity, date).compute()
}
}
class Gamma(value: Int, quantity: Int, date: Int) {
var value1 = _
var value2 = _
var value3 = _
def compute() {
value1 = value * quantity + detla()
value2 = value * date + 100
importantThing()
value3 = value2 * 7
value3 - 2 * value1
}
def importantThing() {
if ((date - value1) > 100)
value2 -= 20
}
}
有点累哇?好好休息下,准备迎接下一篇《消除坏味道》系列的文章吧。
提示汇总
- 代码是给人读的,要说明意图,而不仅仅是如何实现。
推荐
查看《大话重构》系列文章,请进入YoyaProgrammer公众号,点击 核心技术,点击 大话重构。
分类 大话重构
优雅程序员 原创 转载请注明出处