使用跨多个命名空间定义的 Clojure 多重方法

Using Clojure multimethods defined across multiple namespaces

虽然下面的例子看起来有点奇怪,但这是因为我试图将我目前遇到的一个相当大的问题简化为一个最小的例子。当 multimethods 位于几个抽象层后面并且 defmulti 和相应的 defmethods 在多个名称空间中定义时,我正在努力研究如何调用它们。我真的觉得我在这里遗漏了一些明显的东西......

假设我有以下场景:

使用 Clojure,实现通用接口的推荐方法是通过协议或多方法。在这种情况下,由于我根据供应商的价值进行切换,我认为处理我在下面描述的情况的最佳方法是通过多种方法(但我可能是错的)。

我的多方法定义看起来像这样,它定义了一个我想用来与每个供应商的 APIs:

对话的通用接口
(ns myapp.suppliers.interface)
(defmulti purchase-item :supplier)
(defmulti get-item-price :supplier)

对于每个供应商,我可能想要这样的东西:

(ns myapp.suppliers.supplier1
  (:require [myapp.suppliers.interface :as supplier-api]))
(defmethod purchase-item :supplier1 [item quantity] ...)
(defmethod get-item-price :supplier1 [item] ...)

(ns myapp.suppliers.supplier2
  (:require [myapp.suppliers.interface :as supplier-api]))
(defmethod purchase-item :supplier2 [item quantity] ...)
(defmethod get-item-price :supplier2 [item] ...)

到目前为止,没问题

现在我的代码调用这些抽象的方法,我假设它看起来像:

(ns myapp.suppliers.api
  (:require [myapp.suppliers.supplier1 :as supplier1]
            [myapp.suppliers.supplier2 :as supplier2])
(defn buy-something
  [supplier item quantity]
  (purchase-item [supplier item quantity])
(defn price-something
  [supplier item]
  (get-item-price [supplier item])

这看起来有点……难看。每次我实施新供应商的 API 时,我都需要将 myapp.suppliers.api 更改为:要求新供应商的方法并重新编译。

现在我的工作更上一层楼,我想从 supplier2 购买一个小部件。

(ns myapp.core
  (:require [myapp.suppliers.api :as supplier])
(def buy-widget-from-supplier2    
  (buy-something :supplier2 widget 1)

这行不通,因为 :supplier2 尚未在此命名空间中的任何位置定义。

有没有更优雅的方式来编写这段代码?特别是,在 myapp.core,我如何从 :supplier2 购买东西?

初始笔记

很难判断您是否在简化示例的过程中混淆了一些东西,或者它们是否完全不正确。对于我所指的示例,请考虑 purchase-item,尽管 get-item-price:

的问题类似
  • defmulti 调用是一个单参数函数
  • 每个 defmethod 调用都有两个参数
  • buy-something中的调用将向量传递给purchase-item,但在向量中查找:supplier关键字总是returnnil

您的顾虑

  • Every time I implement a new supplier's API, I'll need to change myapp.suppliers.api to :require that new supplier's methods and recompile.

    • 如果你需要myapp.suppliers.interface命名空间myapp.suppliers.api,这个问题就可以避免
  • This can't work, because :supplier2 hasn't been defined anywhere in this namespace.

    • 简而言之,这会起作用。 :)
  • Is there a more elegant way to write this code? In particular, in myapp.core, how can I buy-something from :supplier2?

    • 当然可以,但是这个解决方案将根据初始注释.
    • 中的歧义做出一些假设

在不偏离您的原始设计的情况下,这里有一个完整的示例,说明我如何解释您试图实现的目标:

  • myapp.suppliers.interface

    (ns myapp.suppliers.interface)
    
    (defmulti purchase-item (fn [supplier item quantity] supplier))
    
  • myapp.suppliers.supplier1

    (ns myapp.suppliers.supplier1
      (:require [myapp.suppliers.interface :as supplier-api]))
    
    (defmethod supplier-api/purchase-item :supplier1 [supplier item quantity]
      (format "Purchasing %dx %s from %s" quantity (str item) (str supplier)))
    
  • myapp.suppliers.supplier2

    (ns myapp.suppliers.supplier2
      (:require [myapp.suppliers.interface :as supplier-api]))
    
    (defmethod supplier-api/purchase-item :supplier2 [supplier item quantity]
      (format "Purchasing %dx %s from %s" quantity (str item) (str supplier)))
    
  • myapp.suppliers.api

    (ns myapp.suppliers.api
      (:require [myapp.suppliers.interface :as interface]))
    
    (defn buy-something [supplier item quantity]
      (interface/purchase-item supplier item quantity))
    
  • myapp.core

    (ns myapp.core
      (:require [myapp.suppliers.api :as supplier]))
    
    (def widget {:id 1234 :name "Monchkin"})
    
    (supplier/buy-something :supplier1 widget 15)
    ;;=> "Purchasing 15x {:id 1234, :name \"Monchkin\"} from :supplier1"
    
    (supplier/buy-something :supplier2 widget 3)
    ;;=> "Purchasing 3x {:id 1234, :name \"Monchkin\"} from :supplier2"
    

如您所见,supplier/buy-something 调用传播到适当的多方法实现。希望这可以帮助您到达您想要去的地方。