Handling cross-cutting concerns in Clojure10.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':
The REPL output shows to us what is happening. Without augmentation:
(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))
(say-hello "Falko") ; Hello Falko ; nilWith augmentation:
(say-hello-augmented "Falko") ; BEGIN TX ; Hello Falko ; COMMIT TX ; nilWithout augmentation:
(throw-something true) ; IllegalArgumentException Oops snippets.handlers/throw-something (handlers.clj:36) (throw-something false) ; Success ; nilWith 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 ; nilOf 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.