重构 - Simplifie Function Call

本章是重构的第五章,主要讲解了函数调用的优化。注意,这和 Extract Method 很像,但是它们侧重于两个不同的方面,Extract 着重于对函数实现,也就是函数体的重构,而这里的 Function Call 则更侧重于对于调用函数的优化,包括对于函数名称、参数长度、责任和角色的处理。

说明自身扮演的作用

Rename Method

对于函数而言,重构的一个重要手段就是把一个大函数拆分成很多个小函数,因为每个函数都有名称,它们可以解释自己做了什么(而不是怎么做),因此,函数名称的一个重要目的就是用来替代注释:当你想要为一个函数写点注释时,大胆的重命名函数吧,一个好的函数名能够自解释它的功能。

优化函数的参数长度

Add Parameter

对于中间变量的去除,在之前文章中介绍过,如果只用来读,那么可以放入参数处,减少中间变量。

但是其实,应该尽可能不要得到一个过长的参数列,如果有其它选择,比如在函数体中调用外部字段或者查询函数,不要去插入参数。添加参数是很容易做的,但不是不能做的,只是说,当你在做的时候,要时刻记住自己应该还有别的选择。

Remove Parameter

当参数列过长的时候,应该精简它们,如果一个参数可以通过另一个参数对象的方法、字段获得,那么就在方法体中这样做,从而去除多余参数。

如果是 public 的函数,将其声明为 deprecated。

Preserve Whole Object

如果函数的参数来自一个对象的多个字段、方法,那么将这些参数合并,传入对象。这往往会较少函数参数列,提高其可读性。但是,这样的问题是,如果你传入对象,可能造成依赖结构复杂,这时候,就不要使用此方法。

注意,如果你真的依赖某个对象,需要从中读取很多值,可能你需要使用 Move Method 将这个函数放到那个对象中。

Introduce Parameter Object

和 Preserve Whole Object 类似,如果多个参数相似,可以属于同一个范畴,但是还不存在一个对象,那么就创建一个对象,让这个对象提供这些参数。比如在很多时候都伴随出现的 start 和 end Date 类,完全可以替换为一个 DateRange 类,这样更直观,减少了参数列,并且你还可以为这个 Range 类提供更多实用的方法,这样就不用在调用这个函数中做这些样板代码了 —— 先进行 Extract Methods,然后进行 Move Methods 即可。

Replace Parameter with Method

还有一种情况,如果对象调用某个函数,并且将其结果作为参数,传递给另一个函数,那么接受该参数的函数本身其实也可以直接调用那个原本的函数的。

这种方法也可以用来缩短参数列。可能造成的一个问题是,这个函数和原本函数产生了依赖,但是这总比过长的参数列好。问题是,如果后一个函数是一个纯函数,而前一个函数是带有副作用的函数,那么不要使用这个方法。

函数的责任划分:拆分、合并、隐藏、去除

Parameterize Method

如果多个函数在做相似的工作,考虑将其合并为一个函数,并且通过入参来表达不同的值。注意,这仅限于少量参数就可以区分多个算法的情况,如果合并两个函数的代价太大,需要大量的入参,或者对于一个入参需要大量的 switch 来区分“条件”,那么宁可不做,或者... 采用多态的方法。

def get5DoubleWith(a:Int) = 5 * a
def get6DoubleWith(a:Int) = 6 * a
//更改为
def getDoubleWith(a:Int, with_:Int) = a * with_

Replace Parameter with Explicit Methods

和上一个相反,如果说在一个函数中,根据入参做了不同的逻辑,那么应该考虑拆分函数为不同的责任,这样逻辑更加清晰。

var height = 0
var width = 0
def setValue(name:String, value:Int) = {
    if (name == "height") height = value
    if (name == "width") width = value
}

def setHeight(value:Int) = this.height = value

Hide Method

函数尽可能的要隐藏,除非必要打开其访问权限。因为这样,当一个 API 发布后,你能够更好的控制自己的代码,可以大胆的删除 protect 和 private 的函数,或者移动它们。

对于 Scala 的 val 和 var 也是如此,最好是 private[package] 的权限,如果实在不想让别人看到的一个临时变量,使用 private[this]。

Remove Setting Method

在 Java 中,如果一个对象只读取,但是不设置(除了构造),那么不要写设值函数。对于 Scala,还是那句话,统一访问原则自动为我们处理了这些事情。

函数扮演的特定角色

Separate Query from Modifier 【纯函数和副作用函数】

将查询和带有副作用的修改函数区分开是一种良好的编程实践。实际上,对于 Scala,默认不带括号的方法都是查询方法,带有空括号、带有括号和参数但是返回 Unit 的方法都是带有副作用的方法。这种区分对于 OOP 和 FP 语言下实现并发编程很有帮助,同时可以避免 Bug。

《Scala 函数式编程》 中介绍过如何得到一个纯函数,即在任何内部外部环境下,给定输入都得到相同输出的函数,如果你有一个逻辑,试图将它分为做出修改的带有副作用的方法和纯粹提供算法的纯函数,即 Query Method 和 Side Effect Method。这样的话,纯函数更容易测试、模块化、集成,并且大量应用于并发并且不会造成 Bug。注意,一个好的结构应该有大量的 Query Method 和较少的 Side Effect Method,即不可变的核心和薄的外围。

Replace Constructor with Factory Method 【工厂函数】

这个之前讲过很多次了,其一,构造函数使用上并不方便,因为可能需要在构造好之后进行一些操作,典型的比如两个互相依赖对方的类,除非要在一个中构造另一个,这样就过度耦合了,因此,最好的方式是,在某个工厂的地方处理这些设置的步骤。

对于工厂而言,这些函数能够以接口类型返回对象,这样还可以隐藏内部的类型实现,进一步减少了外部对于实现的依赖。

工厂类一般需要经常修改和重构,如果一个工厂过分复杂,那么使用抽象工厂和工厂方法替代简单工厂设计模式。


2019-05-07 撰写本文。