9

我很可能以错误的方式处理这个问题,所以请原谅我的天真:

为了学习 Clojure,我已经开始将我的 Python OAuth 客户端库移植到 Clojure。我通过包装 clj-http 来做到这一点,就像我在 Python 库中包装 Python 请求一样。到目前为止,这似乎工作得很好,我真的很高兴看到实现在 Clojure 中实现。

但是我遇到了一个问题:我计划同时支持 OAuth 1.0 和 2.0,并将各自的功能分成两个文件:oauth1.clj 和 oauth2.clj。现在,理想情况下,每个文件都应该公开一组与 HTTP 动词对应的函数。

(ns accord.oauth2)

...

(defn get
  [serv uri & [req]]
  ((:request serv) serv (merge req {:method :get :url uri})))

这些函数本质上是相同的,实际上现在在 oauth1.clj 和 oauth2.clj 之间是完全相同的。我的第一反应是将这些函数移动到 core.clj 中,然后在各自的 OAuth 命名空间(oauth1、oauth2)中要求它们,以避免重复编写相同的代码。

这很好,只要我使用文件中的引用函数,即 oauth1.clj 或 oauth2.clj。但是假设我们想按照我的意图使用这个库(在 REPL 中,或者在你的程序中),如下所示:

=> (require '[accord.oauth2 :as oauth2])  ;; require the library's oauth2 namespace

...

=> (oauth2/get my-service "http://example.com/endpoint")  ;; use the HTTP functions

找不到varoauth2/get是因为仅将其拉入 oauth2.clj 中的命名空间似乎并没有像它实际上在该命名空间中那样暴露它。我不想用更多的功能来包装它们,因为这基本上违背了目的;这些函数非常简单(它们只是包装了一个request函数),基本上,如果我要这样做的话,我会在三个地方编写它们。

我确信我没有正确地在 Clojure 中理解命名空间,而且可能是惯用地思考抽象问题和代码共享的一般方式。

所以我想知道这个的惯用解决方案是什么?我会以完全错误的方式解决这个问题吗?

编辑:

这是问题的简化:https ://gist.github.com/maxcountryman/5228259

请注意,目标是一次性编写 HTTP 动词函数。他们不需要特殊的调度类型或类似的东西。他们已经很好了。问题是它们没有暴露于accord.oauth1or accord.oauth2,例如,当您的程序需要时accord.oauth2

如果这是 Python,我们可以像这样导入函数:from accord.core import get, post, put, ...进入accord.oauth1accord.oauth2然后当我们使用accord.oauth1模块时,我们可以访问所有这些导入的函数,例如import accord.oauth2 as oauth2... oauth2.get(...)

我们如何在 Clojure 中做到这一点,或者我们应该如何惯用地提供这种 DRY 抽象?

4

3 回答 3

5

考虑看看 Zach Tellman 的图书馆Potemkin。Zach 将其描述为“用于重组命名空间和代码结构的函数集合”。

波将金并非没有争议。这是Clojure 邮件列表上一个线程的开头,其中 Stuart Sierra 明确表示他不是这个想法的粉丝。

于 2013-03-23T18:48:59.893 回答
2

我将回答我的问题,尽管感谢所有评论的人:安德鲁的回答内容丰富,虽然它不能完全回答问题,但它肯定会导致答案。我确实认为 Potemkin 会这样做,但我继续并根据这个线程编写了自己的解决方案。我会说我不觉得这种方法通常是惯用的,基于这里的一些回应和 IRC 中的进一步讨论,但是它可能对有限的用例有意义,比如我的。

但要回答这个问题,这个函数应该做我原本打算的:

(defn immigrate
  [from-ns]
  (require from-ns)
  (doseq [[sym v] (ns-publics (find-ns from-ns))]
    (let [target (if (bound? v)
                  (intern *ns* sym (var-get v))
                  (intern *ns* sym))]
      (->>
        (select-keys (meta target) [:name :ns])
        (merge (meta v))
        (with-meta '~target)))))

然后你可以像这样调用它,假设我们把它放在 foo.clj 中(如果你看到我在编辑中添加的要点):

(ns testing.foo)

(immigrate `testing.baz)

现在,如果我们在 REPL 中需要 testing.foo:

=> (require '[testing.foo :as foo])
=> (foo/qux "hi!")
;; "hi!"

在 IRC 上与 Stuart Sierra 交谈并阅读 Andrew 链接的电子邮件线程后,我得出结论,这不一定是使用命名空间的预期方式。

相反,实现我的库的更好方法可能如下所示:

=> (require '[accord.oauth2 :as oauth2])
=> (def my-serv (oauth2/service 123 456 ...))
=> (require '[accord.http :as http])
=> (http/get my-serv "http://example.com/endpoint")

但是,考虑到我想向最终用户提供尽可能干净的 API,我可能会继续immigrate在“导入”HTTP 方法函数的这个非常有限的范围内使用该函数。

编辑:

经过进一步讨论,我认为通常不应该使用上述解决方案,正如我已经说过的那样。对于我的用例,我可能会使用我的最后一个解决方案,即使用两个单独的命名空间。

于 2013-03-25T14:37:16.047 回答
0

设计解决方案的一种选择是使用提供默认实现的多方法。

;The multi methods which dispatch on type param
(defmulti get (fn [serv uri & [req]] serv))
(defmulti post (fn [serv uri & [req]] serv))

;get default implementation for any type if the type doesn't provide its own implementation
(defmethod get :default [serv uri & [req]]
  "This is general get")

;post doesn't have default implementation and provided specific implementation.
(defmethod post :oauth1 [serv uri & [req]]
  "This is post for oauth1")

(defmethod post :oauth2 [serv uri & [req]]
  "This is post for oauth2")


;Usage
(get :oauth1 uri req) ;will call the default implementation
(get :oauth2 uri req) ;will call the default implementation
(post :oauth1 uri req) ;specific implementation call
(post :oauth2 uri req) ;specific call 
于 2013-03-23T08:28:31.490 回答