Clojure:覆盖库中的一个函数

Clojure: overriding one function in a library

这个问题是几天前 previous question I asked 的问题。其中一条评论是我应该放弃用于提取查询参数的 Ring 中间件并编写我自己的中间件。我认为我会尝试的一种替代方案是利用现有的替代方案来获得我想要的东西,并且我一直在深入研究 Ring 源代码。它几乎完全符合我的要求。如果我写出我理解它是如何工作的:

  1. 中间件有函数wrap-params调用params-request
  2. params-request 添加一个 params 映射到 request 映射,调用 assoc-query-params
  3. assoc-query-params 最终在传入的查询字符串上调用 ring.util.codec/form-decode 以将其转换为映射
  4. form-decode 使用 assoc-conj 通过 reduce
  5. 将值合并到现有映射中
  6. assoc-conj 的文档字符串说

Associate a key with a value in a map. If the key already exists in the map, a vector of values is associated with the key.

最后一个函数是我上一个问题中有问题的函数(TL;DR:我希望地图的值在字符串或向量的 class 中保持一致)。戴上面向对象的帽子后,我可以通过 subclassing 和覆盖我需要改变行为的方法轻松解决这个问题。但是,对于 Clojure,我看不出如何只替换一个函数而不必更改堆栈中的所有内容。这可能并且容易吗,或者我应该以另一种方式进行吗?如果涉及到它,我可以复制整个中间件库和编解码器库,但对我来说它似乎有点重量级。

我不同意不要使用 Ring 的参数中间件的建议。它为您提供有关传入参数的完美信息,因此如果您不喜欢 string-or-list 的默认行为,您可以根据需要更改参数。

有很多方法可以做到这一点,但一种明显的方法是编写您自己的中间件,并将其插入 Ring 的参数中间件和您的处理程序之间。

(defn wrap-take-last-param []
  (fn [handler]
    (fn [req]
      (handler 
        (update req :params 
                (fn [params]
                  (into {} 
                     (for [[k v] params]
                       [k (if (string? v) v, (last v)]))))))))

您可以写一些更高级的东西,比如向函数添加一些参数,让您指定哪些参数您只想接收最后指定的参数,哪些参数您希望始终作为列表接收。在那种情况下,您可能不想将它包裹在整个处理程序周围,而是分别包裹在您的每个路由周围以指定它们的预期参数。

虽然自定义中间件可能是解决此问题的最明确方法,但请不要忘记您始终可以覆盖 any使用 with-redefs 的函数。例如:

(ns tst.demo.core
  (:use tupelo.core tupelo.test))

(dotest
  (with-redefs [clojure.core/range (constantly "Bogus!")]
    (is= "Bogus!" (range 9))))

虽然这主要在单元测试期间使用,但它是一个敞开的逃生口,可用于覆盖任何功能。

对于 Clojure,源代码中的 Var 与库中的 Var(甚至 clojure.core 本身,如示例所示)之间没有区别。