Below you will find pages that utilize the taxonomy term “Refactor Clojure”
Posts
Refactor Clojure(4) -- 使用闭包避免重复参数传递
问题 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?
Posts
Refactor Clojure(3) -- builder function 构建有效选项 map
问题 在《Refactor Clojure(2)》我们介绍了如何使用 optional map 解决参数列表过长的问题,通过引入有意义的命令选项,一定程度上让用户使用起来更方便,不过它也有缺陷,比如 :or 没有自动加入 :as 结果的陷阱,以及用户可能将参数名称可能不小心拼写错误,特别是后者,在 Clojure 里是很容易发生的,这种错误也通常只能运行时才能发现。
本质上,我们的目的是生成一个有效的查询选项:
{:skip skip :limit limit :query-keys query-keys :include include} 为了避免用户拼写错误,也许我们可以类似使用设计模式里的 Builder 模式,提供一系列更为明确的的 setter 函数来让用户构建一个有效的选项 map。
解决 不过我们不准备引入一个可变的 Java 对象,而是思考构建选项的过程是什么样?
我们会从一个默认值 map 开始 {:skip 0 :limit 100},然后用户加入 skip 的时候,往这个 map 添加一项:
(assoc {:skip 0 :limit 100} :skip 100) 用户添加 limit 的时候,再加入一个选项:
(assoc (assoc {:skip 0 :limit 100} :skip 100) :limit 10) 如果用户设定 query-keys,我们也一样,再次 assoc:
(assoc (assoc (assoc {:skip 0 :limit 100} :skip 100) :limit 10) :query-keys ["a" "b" "c"]) 不过这个嵌套层次很难看了,按照《Refactor Clojure(1)》,我们可以用 thread 宏来简化:
Posts
Refactor Clojure (2) -- 使用 optional map 解决参数过多问题
这个系列准备提到的方式可能对大多数 clojure 老手来说都不新鲜,我的目的主要是自己归纳总结,并且同时重构自己的代码,其次是针对解决方式,引出一些讨论。因此每篇博客都分成三部分:
问题,说明这次要解决什么问题。 解决,提出初步的解决方案和最终本文要讨论的解决方案。 讨论,针对这个解决方案做进一步的扩展讨论。 权且抛砖引玉,欢迎有兴趣的朋友参与讨论。
问题 通常,我们开始写一个函数来完成某个功能,可能一开始只需要一两个参数就够了,接下来用户提出更多的定制化需求,不可避免,你可能需要传递更多参数进来。当一个方法的参数膨胀到 5 个以上,哪怕 clojure 有内置的 metadata 文档系统,这个函数也不可避免的难用起来。如果这是一个内部方法,也许还能承受,但是如果开放给用户,它的易用性就很成问题。
除了参数过多之外,很可能大多数参数用户是不需要的,内部应该提供一些默认值给这些参数,而不是每次都让用户传递。
以一个问题为例子,我们有这么一个查询接口,一开始他是这样的,你需要传递查询的表名和 where 条件:
(defn query-objects [table where]) 接下来,发现用户需要分页功能,我们增加了 skip 和 limit:
(defn query-objects [table where skip limit]) 过了没几分钟,用户提出希望能指定返回的对象的字段列表,ok,我们继续增加一个 keys 参数:
(defn query-objects [table where query-keys skip limit]) 又过了几天,用户提出需要能排序和关联查询,好吧,我们需要加入排序字段和 include 字段:
(defn query-objects [table where query-keys skip limit order include]) 这个过程如果再持续下去,这个方法的参数将无可避免地膨胀,它的问题包括:
用户需要知道参数名,也需要知道参数的顺序。 另外一个就是刚才提到的默认参数,可能大多数用户只是想查询第一个页,每页 100 条,那么 skip 和 limit 默认就是可以是 0 和 100等。这样的用户目前也不得不传递很多参数进来。 万一哪天你想修改这两个默认值,你需要去修改所有调用这个方法的地方,这完全违背了开闭原则(是的,哪怕是 FP,我们也希望遵循开闭原则。) 解决 一个简单的解决办法可能是使用重载参数,请注意** clojure 仅支持参数个数的重载,不支持参数类型重载**:
Posts
Refactor Clojure (1) -- 使用 thread 宏替代嵌套调用
我一直想写这么一个系列文章,讲讲 clojure 怎么重构代码。想了很久,没有行动,那么就从今天开始吧。这个系列的目的是介绍一些 clojure 重构的常用手法,有 clojure 特有,也有一些跟《重构:改善既有代码的设计》类似的方法,权且抛砖引玉。我会尽量以真实的项目代码为例子来介绍,Let’s begin。
问题 Clojure 是函数式编程,而函数式编程一个特点就是我一直喜欢强调的数据流抽象,在实际编程中我们经常做的一个事情是将一个数据使用 map,filter,reduce 等高阶函数做转换,增加一点信息,减少一点信息,过滤一些信息,累积一些信息等等,来得到我们最终想要的数据。例如我最近有这么一个任务,从一个 map 里收集所有的 key,包括内部的嵌套 map,例如这么一个 map:
(def x {:a 1 :b { :b1 "hello" :b2 2} :c { :c1 {:c21 "world" :c22 5}}}) 我要写一个函数 all-keys,想得到的结果是
user=> (all-keys x) [:c :b :a :c1 :c22 :c21 :b2 :b1] 顺序我不关心,但是要求能找出所有的 key,包括嵌套。
初步解决 解决这个问题不难,我们本质上是要遍历一个树形结构,找出所有 map,然后使用 keys 函数获取他们的关键字列表,然后加入一个结果集合。使用 clojure cheatsheet 我们根据关键字 tree 找到函数 tree-seq:
user=> (doc tree-seq) ------------------------- clojure.core/tree-seq ([branch? children root]) Returns a lazy sequence of the nodes in a tree, via a depth-first walk.