Handling cross-cutting concerns in Clojure
10.04.2013
Permalink
What I like most about Clojure is that it allows me to implement most
things without going extra miles. In Java, which I use for almost 15
years now, there are several different solutions for a recurring
problem: how can we handle cross-cutting concerns like authorisation,
transaction demarcation or exception logging in a central well-defined
place without littering business code? And how can we declaratively
specify which handlers we like to have in which order?
The fast answer nowadays is: AOP, either by using Spring dynamic
proxies or by using the more complete approach of
AspectJ. Additionally there are several implementations like Servlet
filters, EJB interceptors, the Tapestry request handler pipeline or
the CXF handler chain that essentially address the same problem. (And
I'm sure there are dozens of other implementations that I've never
heard of.)
Clojure (and other languages that treat functions as first class
citizens) allow me to employ higher-order functions that act as
wrappers. So, every concern is handled by one handler function, and
all I need then is a means to compose these handler functions together
with the business function to a new one. The do-it-yourself-way in
Clojure requires me to write 6(!) lines of sparse code. Take a look at
the function 'augment':
(ns snippets.handlers)
(defn augment
[f & handlers]
(reduce (fn [augmented h]
(partial h augmented))
f
(reverse handlers)))
;; Sample handlers for cross-cutting concerns
(defn handle-tx
[f & args]
(println "BEGIN TX")
(try
(let [result (apply f args)]
(println "COMMIT TX")
result)
(catch Exception ex (do
(println "ABORT TX")
(throw ex)))))
(defn handle-exceptions
[f & args]
(try (apply f args)
(catch Exception ex (println "Caught Exception" ex))))
;; Sample business functions
(defn say-hello [x]
(println "Hello" x))
(defn throw-something [b]
(if b
(throw (IllegalArgumentException. "Oops"))
(println "Success")))
;; Create augmented functions that deal with cross-cutting concerns
(def say-hello-augmented (augment say-hello
handle-exceptions
handle-tx))
(def throw-something-augmented (augment throw-something
handle-exceptions
handle-tx))
The REPL output shows to us what is happening.
Without augmentation:
(say-hello "Falko")
; Hello Falko
; nil
With augmentation:
(say-hello-augmented "Falko")
; BEGIN TX
; Hello Falko
; COMMIT TX
; nil
Without augmentation:
(throw-something true)
; IllegalArgumentException Oops snippets.handlers/throw-something (handlers.clj:36)
(throw-something false)
; Success
; nil
With augmentation:
(throw-something-augmented true)
; BEGIN TX
; ABORT TX
; Caught Exception #<IllegalArgumentException java.lang.IllegalArgumentException: Oops>
; nil
(throw-something-augmented false)
; BEGIN TX
; Success
; COMMIT TX
; nil
Of course you don't have to invent that wheel, a more complete Clojure solution is
Robert Hooke.
So, the important take-away here is: Clojure does not force us to write amounts
of 'technical infrastructure code' or XML configuration to solve this problem.
Even Robert Hooke is a tiny piece of code compared to what you usually find in
the Java world.
And this is what I found true for almost every other typical enterprise software
problem that I came across in the last couple of years.