/ CLOJURE, DISPATCH, DEFMULTI, DEFMETHOD

Learning Clojure: dynamic dispatch

This post is part of a serie on learning the Clojure language. Previous posts include:

  1. Coping with dynamic typing
  2. The arrow and doto macros

Introduction

A common pattern in software development is the dynamic dispatching (or routing) table. The table is responsible for returning the relevant output for a specific input.

In the Object-Oriented programming world, the object model would look something like that:

dynamic dispatch

When an input is sent to the registry, it internally finds the handler that can process the input, and delegates to the selected handler.

A common use-case for such a pattern is a (very simplified) HTTP request-response routing table. For example, the routing could be decided upon the Accept HTTP header:

  1. An HTTP request is sent with a specific Accept header
  2. The registry searches for a registered handler able to handle the MIME type
  3. The handler is invoked
  4. It returns an HTTP response with the relevant Content-Type header
request response dispatch

It’s up to the implementor to create handlers for HTML, XML, JSON, etc. and register them in the registry. Alternatively, some default implementations could already be available out-of-the-box, and could be registered as well.

Clojure’s approach

From Clojure’s point of view, the registry is quite boiler-plate’ish: if offers a generic mechanism to achieve the same. This mechanism is built upon two macros:

  • defmulti plays the role of the registry. It accepts two arguments: a label and the routing logic. The value it returns will be matched with those provided by handlers.
  • defmethod plays the role of a handler. It accepts some arguments:
    1. a label, referencing the registry it applies to
    2. the value that will be matched with the one returned by the registry’s routing logic.
    3. the list of parameters made available to the handler’s logic
    4. the handler’s logic

Show me the code

Given that, let’s implement the above model in Clojure.

Irrelevant to the rest of the code, the first step is to create an utility function to extract the accept header from a map-like request.

(defn extract-header [request                               (1)
                      header]                               (2)
  (-> request                                               (3)
      (get :headers)
      (get header)))

(extract-header {:headers {:accept "foo/bar"}} :accept)     ; "foo/bar"
(extract-header {:headers {:accept "text/html"}} :accept)   ; "text/html"
1Map-like request parameter
2Header’s name e.g. :accept
3Usage of the thread-first macro - it was explained in detail last week

The next step is to define the defmulti. Remember that it defines the main routing logic.

(defmulti dispatch-accept                          (1)
          (fn [req] (extract-header req :accept))) (2)
1As stated above, this is the defmulti name
2Return the header value from the request

The last step is to define an implementation:

(defmethod dispatch-accept "text/html" [req]                              (1)
  (let [formatter (f/formatters :date)                                    (2)
        date (f/unparse-local-date formatter (extract-header req :date))] (3)
    {:status  200
     :headers {:content-type (extract-header req :accept)}                (4)
     :body    (str "<html><body>Date is " date)}))                        (5)
1"Attach" the defmethod to the defmulti with the same name. This method will be called when the former returns text/html i.e. when it’s the value of the :accept header
2Define a formatter using the clj-time library
3Format the request’s date header using the above formatter
4Put the request’s accept header in the response’s content-type header
5Store the formatted date in the response :body

Now is time to call the defined method with a map-like request:

(dispatch-accept {:headers {:accept "text/html"
                            :date   (t/today)}})

As expected, this yields:

{:status 200,
 :headers {:content-type "text/html"},
 :body "<html><body>Date is 2018-09-29"}

It’s straightforward to add more handlers when necessary:

(defmethod dispatch-accept "application/xml" [req]
  (let [year (f/formatters :year)
        month (f/formatter "MM")
        day (f/formatter "dd")
        date (extract-header req :date)]
    {:status  200
     :headers {:content-type (extract-header req :accept)}
     :body    (str "<?xml version=\"1.0\"?><date><year>"
                   (f/unparse-local-date year date)
                   "</year><month>"
                   (f/unparse-local-date month date)
                   "</month><day>"
                   (f/unparse-local-date day date)
                   "</day></date>")}))

(defmethod dispatch-accept "application/json" [req]
  (let [year (f/formatters :year)
        month (f/formatter "M")
        day (f/formatter "dd")
        date (extract-header req :date)]
    {:status  200
     :headers {:content-type (extract-header req :accept)}
     :body    (str "{ \"date\" : { \"year\" : "
                   (f/unparse-local-date year date)
                   ", \"month\" : "
                   (f/unparse-local-date month date)
                   ", \"day\" : "
                   (f/unparse-local-date day date)
                   "}}")}))

dispatch-accept {:headers {:accept "application/xml"
                            :date   (t/today)}})
(dispatch-accept {:headers {:accept "application/json"
                            :date   (t/today)}})

The above code respectively returns:

{:status  200,
 :headers {:content-type "application/xml"},
 :body    "<?xml version=\"1.0\"?><date><year>2018</year><month>09</month><day>29</day></date>"}

{:status   200,
 :headers {:content-type "application/json"},
 :body    "{ \"date\" : { \"year\" : 2018, \"month\" : 9, \"day\" : 29}}"}

Missing cases

At this point, calling dispatch-accept returns values for a set of accept headers: text/html, application/xml and application/json. What if a request is sent with an un-managed header?

(dispatch-accept {:headers {:accept "image/jpeg"
                            :date   (t/today)}})
java.lang.IllegalArgumentException:
  No method in multimethod 'dispatch-accept' for dispatch value: image/jpeg

In order to manage unmanaged cases, Clojure allows to define a default defmethod, which will be called when no others match. This is similar to the default case of switch statements. Let’s add that:

(defmethod dispatch-accept :default [req]
  {:status 400
   :body   (str "Unable to process content of type " (extract-header req :accept))})

With the above defined:

(dispatch-accept {:headers {:accept "image/jpeg"
                            :date   (t/today)}})

{:status 400,
 :body   "Unable to process content of type image/jpeg"}

Conclusion

While OOP offers polymorphism through inheritance, Clojure provides it also but without the former. This feature is implemented with the defmulti/defmethod macro pairs.

Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with narrower interests like Software Quality, Build Processes and Rich Internet Applications. Currently working for Exoscale. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More