Clojure 的数据结构都是不可变的,通常我们也很少在 clojure 里使用 Java 的可变数据结构;其次,Clojure 的 FP 风格也提倡你的函数应该是无副作用的,同样的参数传递给某个函数,他应该每次都返回同样的结果,没有额外的状态改变等。这就造成一个后果,状态或者数据都需要通过参数来传递,那么往往造成参数列表很长,我们可以用 《Refactor Clojure(2)》 和 《Refactor Clojure(3)》 提到的手法来改善长参数列表的函数的接口。
不过,我们还是遇到这样一个问题:在函数之间参数的不匹配,我们无法保证每个函数的参数列表维持一个一致的风格,特别是涉及到二方或者三方库的调用的时候,你在 A 函数里调用 B,在 A 内部对 A 输入的参数做了一些处理,添加、移除或者转换参数列表后传入给 B 函数,这里就就有所谓阻抗不匹配的问题。如果 A 要在内部对 B 发起多次调用,并在 B 的参数列表已经长的话,无可避免代码显得非常累赘。
例如这么一个场景:
(defn query-objects [app table where opts] (let [conn (db/get-connection app)] (if (cache-table? app table) (db/with-table-cache (db/with-connection conn (db/query :table tabl :where where :offset (:skip opts) :limit (:limit opts)))) (db/with-connection conn (db/query :table tabl :where where :offset (:skip opts) :limit (:limit opts))))))
query-objects 会调用 db 库的函数来做查询,我们配置了某些应用启用查询缓存,通过 cache-table?
这个判断来决定是否启用查询缓存,如果启用,那么需要将 db/query
的执行放在 db/with-table-cache
的上下文里执行。其次, db 库使用 offset 选项来指代我们提供给外部用户的 skip,因此,我们不得不在这里做一次参数的转换:
:offset (:skip opts) :limit (:limit opts)
可以看到下面这个调用在代码里出现了两次:
(db/with-connection conn (db/query :table tabl :where where :offset (:skip opts) :limit (:limit opts)))
如果未来我们进一步支持其他功能,例如指定某个应用只允许查询某张表权限控制之类,需要引入更多的分支判断(if else 的消除是另一个重构话题),那么上面这段代码可能将出现在 query-objects
的更多地方。
我们可以先做一个事情,将 db/query
的参数提取出来成一个 local var,类似 conn
:
(defn query-objects [app table where opts] (let [conn (db/get-connection app) new-opts [:table tabl :where where :offset (:skip opts) :limit (:limit opts)]] (if (cache-table? app table) (db/with-table-cache (db/with-connection conn (apply db/query conn new-opts))) (db/with-connection conn (apply db/query conn new-opts)))))
因为 db/query
接收的是可选参数,我们不得不将 new-opts 变成一个 vector,并且使用 apply 来调用 db/query
。
不谈 apply 性能上的微小损耗,下面这样的代码出现两次仍然是累赘的:
(db/with-connection conn (apply db/query conn new-opts))
其实我们可以将这个调用抽取成一个闭包来使用,形如:
(defn query-objects [app table where opts] (let [do-query (fn [] (db/with-conn (db/get-connection app) (db/query conn :table tabl :where where :offset (:skip opts) :limit (:limit opts))))] (if (cache-table? app table) (db/with-table-cache (do-query)) (do-query))))
我们定义了一个局部函数 do-query,它是一个闭包,它在内部调用了 db/with-connection
和 db/query
做真正的查询工作,并且 close over 了 query-objects 传入的参数并做了转换,真正执行查询在的逻辑变得更清晰:
(if (cache-table? app table) (db/with-table-cache (do-query)) (do-query))
一方面是嵌套层次的减少,一方面我们也尽量将抽象层次保持在一个层级之上。
do-query
本身对 db 库的调用也消除了 apply 和重复代码,考察未来的扩展的几种情况:
这个手法的步骤如下:
这个手法,其实跟 Java 里的 Extract Class + Extract method + Move method 的重构类似,当你在使用 eclipse 的时候, refactor 菜单就提供了 Extract class 的功能,你可以选中一段代码,然后点击 Extract class
,eclipse 会找出这段代码里使用的变量,尝试帮你创建一个类,你还可以选择是作为内部类还是顶级类存在。接下来,你可以使用 extract method 将重复的调用提取成单独方法,然后使用 move method 将该方法移动到第一步提取出来的类,这样一来,我们就将重复的代码调用封装到一个单独的类里面,如果这是一个内部非静态类,你还可以在内部类获得外部类的实例变量,类似闭包,所以有人说内部类是 OO 的闭包。