This is the 4th post in the Learning Clojure focus series.Other posts include:
- Decoding Clojure code, getting your feet wet
- Learning Clojure: coping with dynamic typing
- Learning Clojure: the arrow and doto macros
- Learning Clojure: dynamic dispatch (this post)
- Learning Clojure: dependent types and contract-based programming
- Learning Clojure: comparing with Java streams
- Feedback on Learning Clojure: comparing with Java streams
- Learning Clojure: transducers
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:
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:
- An HTTP request is sent with a specific
Accept
header - The registry searches for a registered handler able to handle the MIME type
- The handler is invoked
- It returns an HTTP response with the relevant
Content-Type
header
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:- a label, referencing the registry it applies to
- the value that will be matched with the one returned by the registry’s routing logic.
- the list of parameters made available to the handler’s logic
- 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"
1 | Map-like request parameter |
2 | Header’s name e.g. :accept |
3 | Usage 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)
1 | As stated above, this is the defmulti name |
2 | Return 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 |
2 | Define a formatter using the clj-time library |
3 | Format the request’s date header using the above formatter |
4 | Put the request’s accept header in the response’s content-type header |
5 | Store 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.