views:

103

answers:

2

I'm looking for an idiomatic way(s) to define an interface in Clojure that can be implemented by an external "service provider". My application would locate and instantiate the service provider module at runtime and delegate certain responsibilities to it.

Let's say, for example, that I'm implementing a RPC mechanism and I want to allow a custom middleware to be injected at configuration time. This middleware could pre-process the message, discard messages, wrap the message handler with logging, etc.

I know several ways to do this if I fall back to Java reflection, but feel that implementing it in Clojure would help my understanding.

(Note, I'm using SPI in a general sense here, not specifically referring to the way it's defined in the JAR file specification)

Thanks

+4  A: 

Compojure uses "middleware" to handle HTTP requests, you might look at its implementation. A "handler" in Compojure is a function that takes a request and returns a response. (Request and response are both Clojure hash-maps.) "Middleware" is a function that takes a handler function, and returns a different handler function. Middleware can alter the request, the response, or both; it can call the handler it's passed (repeatedly if it wants) or short-circuit and ignore the handler, etc. You can wrap handlers in other handlers this way in any combination.

Thanks to functions being first-class objects, this is very lightweight and easy to implement and use. However it doesn't enforce anything at compile time as you would get from a Java interface; it's all a matter of following conventions and duck-typing. Protocols might be good for this task eventually, but they are not going to be available for a while (probably in Clojure 2.0?)

Not sure if this is what you want, but here is a very rudimentary version:

;; Handler
(defn default [msg]
  {:from "Server"
   :to (:from msg)
   :response "Hi there."})

;; Middleware
(defn logger [handler]
  (fn [msg]
    (println "LOGGING MESSAGE:" (pr-str msg))
    (handler msg)))

(defn datestamper [handler]
  (fn [msg]
    (assoc (handler msg)
      :datestamp (.getTime (java.util.Calendar/getInstance)))))

(defn short-circuit [handler]
  (fn [msg]
    {:from "Ninja"
     :to (:from msg)
     :response "I intercepted your message."}))

;; This would do something with a response (send it to a remote server etc.)
(defn do-something [response]
  (println ">>>> Response:" (pr-str response)))

;; Given a message and maybe a handler, handle the message
(defn process-message
  ([msg] (process-message msg identity))
  ([msg handler]
     (do-something ((-> default handler) msg))))

Then:

user> (def msg {:from "Chester" :to "Server" :message "Hello?"})
#'user/msg
user> (process-message msg)
>>>> Response: {:from "Server", :to "Chester", :response "Hi there."}
nil
user> (process-message msg logger)
LOGGING MESSAGE: {:from "Chester", :to "Server", :message "Hello?"}
>>>> Response: {:from "Server", :to "Chester", :response "Hi there."}
nil
user> (process-message msg (comp logger datestamper))
LOGGING MESSAGE: {:from "Chester", :to "Server", :message "Hello?"}
>>>> Response: {:datestamp #<Date Fri Nov 27 17:50:29 PST 2009>, :from "Server", :to "Chester", :response "Hi there."}
nil
user> (process-message msg (comp short-circuit logger datestamper))
>>>> Response: {:from "Ninja", :to "Chester", :response "I intercepted your message."}
nil
Brian Carper
Thanks for the detailed answer. Compojure middleware is definitely close to the design pattern I'm after. Seems the only thing missing is the ability to indirectly wire up a middleware and attach it to a handler from outside the application, say, at deployment time as opposed to design time.
Joe Holloway
+3  A: 

Clojure is a very dynamic language: almost anything that can be done at compile time can be done at runtime. Your "deployment configuration" could simply be a clojure source file that gets loaded into the application at runtime. Just call (load "my-config.clj"). Note that you can even override functions in a particular dynamic scope if you really want to, so you can wrap any function (including core functions) with another one that say logs their arguments, return value and how long they took to run. Have a look at clojure.contrib.trace for an example of how to do this.

Alex Osborne