clojureprotocolsabstractionmultimethod

Using Clojure multimethods defined across multiple namespaces


Although the below example seems a bit strange, it's because I'm trying to reduce a fairly large problem I've got at present to a minimal example. I'm struggling to work out how to call into multimethods when they're sitting behind a couple of abstraction layers and the defmulti and corresponding defmethods are defined in multiple namespaces. I really feel like I'm missing something obvious here...

Suppose I've got the following scenario:

  • I purchase stuff from a variety of suppliers, via their own proprietary interfaces
  • I want to implement a common interface to talk to each of those suppliers
  • I want to be able to purchase different items from different suppliers

Using Clojure, the recommended ways of implementing a common interface would be via protocols or multimethods. In this case, as I'm switching based on the value of the supplier, I think the best way to handle the situation I'm describing below is via multimethods (but I could be wrong).

My multimethod definitions would look something like this, which defines a common interface I want to use to talk to every supplier's APIs:

(ns myapp.suppliers.interface)
(defmulti purchase-item :supplier)
(defmulti get-item-price :supplier)

For each supplier, I probably want something like:

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

and

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

So far, no problem

Now to my code which calls these abstracted methods, which I assume looks something like:

(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])

This is starting to look a bit ... ugly. 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.

Now I'm working at the next level up, and I want to buy a widget from supplier2.

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

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?


Solution

  • Initial notes

    It's hard to tell if you mixed up some things in the process of simplifying the example, or if they weren't quite right out of the gate. For an example of what I'm referring to, consider purchase-item, though the issues are similar for get-item-price:

    • The defmulti call is a single-argument function
    • The defmethod calls each take two arguments
    • The call in buy-something passes a vector to purchase-item, but looking up the :supplier keyword in a vector will always return nil

    Your concerns

    • 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.

      • If you require the myapp.suppliers.interface namespace myapp.suppliers.api, the problem can be avoided
    • This can't work, because :supplier2 hasn't been defined anywhere in this namespace.

      • Simply put, this will work. :)
    • Is there a more elegant way to write this code? In particular, in myapp.core, how can I buy-something from :supplier2?

      • Certainly, but this solution is going to make some assumption based on the ambiguities in the Initial notes.

    Without straying too far from your original design, here's a fully-working example of how I interpret what you were trying to achieve:

    • 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"
      

    As you can see, the supplier/buy-something calls propagate to the appropriate multimethod implementations. Hopefully this helps you get where you were trying to go.