Skip to content

Instantly share code, notes, and snippets.

@ordnungswidrig
Created June 22, 2011 22:27
Show Gist options
  • Save ordnungswidrig/1041419 to your computer and use it in GitHub Desktop.
Save ordnungswidrig/1041419 to your computer and use it in GitHub Desktop.
Define events a protocol methods
;; On my journey into event sourcing in clojure I'm trying different approaches
;; of modeling events and event handling in clojure.
;; A little ceremony to pay homage to the gods of clojure:
(ns handler-protocol
(:use clojure.contrib.trace)
(:use clojure.pprint)
(:use clojure.test))
;; My example problem domain for this will be a very simple sketch of twitter.
;; I'll define bunldes of events as protocols. Thus an event handler will have
;; to implement a method for each event it can handle.
;; This protocol defined the events "follows" and "unfollows" that are sent when
;; a user decides to follow or unfollow another user.
(defprotocol TwitterFollowEventHandler
(follows [handler user follower])
(unfollows [handler user follower]))
;; This event enable handlers to react on sent tweets
(defprotocol TwitterTweetEventHandler
(tweet [handler user message]))
;; Finally a single event for direct messages
(defprotocol TwitterMessageEventHandler
(message-sent [handler sender recipient message]))
;; What can we do with all this? We can react to the above events and build
;; a user's timeline. The handler is defined as a record and returns an updated
;; new instance after handling each event.
;; First we define a little data container for tweets.
(defrecord Tweet [user message])
;; Now let's build the timeline for timeline-user. We need to remember which
;; user's timeline this is, which other users it follows and which entries in
;; the timeline we recorded so far:
(defrecord TimeLineForUser [timeline-user following entries]
TwitterFollowEventHandler
(follows [this user follower]
(if (= timeline-user follower) ; if the timeline-user is the following-user
; add the user to the list of users followed
; and build a new, upated handler
(TimeLineForUser. timeline-user (conj following user) entries)
; else return the handler unchanged
this))
(unfollows [this user follower] ; the same as above but remove the user
(if (= timeline-user follower)
(TimeLineForUser. timeline-user (disj following user) entries)
this))
TwitterTweetEventHandler
(tweet [this user message] ; record the twee if
(if (or (= timeline-user user) ; it's from the timeline-user
(following user) ; or the set of followed users contains it
; or timeline-user is mentioned.
(re-find (re-pattern (str "@" (name timeline-user) "\\b")) message))
(TimeLineForUser. timeline-user following (conj entries (Tweet. user message)))
this)))
;; The following handler will just print a message for the same events as above. Additionally
;; it implements TwitterMessageHandler and logs direct messages as well.
(defrecord TwitterPrintHandler []
TwitterTweetEventHandler
(tweet [this user message]
(doseq [mentioned (map second (re-seq #"@\b(\w+)\b" message))]
(println (str "Hello " mentioned ": " user " mentioned you: " message)))
this) ;; note that we return the unmodified handler instance because we only need the side
;; effect of println
TwitterFollowEventHandler
(follows [this user follower]
(println (str "Hello " user ": " follower " now follows you."))
this)
(unfollows [this user follower]
(println (str "Hello " user ": " follower " no longer follows you."))
this)
TwitterMessageEventHandler
(message-sent [this sender recipient message]
(println (str "Hello " recipient ": " sender " sent you a message: " message))
this))
;; The last thing we need is a way to combine several event handlers into a single
;; instance that implements the above event protocols.
;; Due to clojures one-pass compiler the following definitions are "bottom up".
;; This is not the must convinient order but leads to a nice grand final show case.
;; Apply a method on only if the instance implements (satisfies) the protocol. Else
;; return the unchanged instance.
(defn- apply-if-satisfies? [protocol method instance & args]
(if (satisfies? protocol instance)
(apply method instance args)
instance))
;; Apply a methods on all instances that implement (satisfy) the protocol. Those
;; instance that do not satisfy the protocol are returned unchanged.
(defn- apply-all-if-satisfies? [protocol method instances & args]
(map #(apply apply-if-satisfies? protocol method % args) instances))
;; We define a delegator as a record that holds all handles we want to "combine".
;; It implements all of the defined event protocols and delegates the method
;; call to the handlers using the two helper functions we have just defined.
(defrecord Delegator [delegates]
TwitterTweetEventHandler
(tweet [_ user message]
(Delegator.
(apply-all-if-satisfies? TwitterTweetEventHandler tweet delegates user message)))
TwitterFollowEventHandler
(follows [_ user follower]
(Delegator.
(apply-all-if-satisfies? TwitterFollowEventHandler follows delegates user follower)))
(unfollows [_ user follower]
(Delegator.
(apply-all-if-satisfies? TwitterFollowEventHandler unfollows delegates user follower)))
TwitterMessageEventHandler
(message-sent [_ sender recipient message]
(Delegator.
(apply-all-if-satisfies? TwitterMessageEventHandler message-sent sender recipient message))))
;; Grand Final.
;;
;; We defined three handlers a delegator that combines these and thread some method
;; invocations through it.
(defn test-delegate []
(let [tlfu1 (TimeLineForUser. :alice #{} [])
tlfu2 (TimeLineForUser. :bob #{} [])
ph (TwitterPrintHandler.)
delegator (Delegator. [ph tlfu1 tlfu2])]
(-> delegator
(follows :alice :bob)
(tweet :alice "Hey bob is in da house!")
(tweet :bob "This is bob but nobody notices!")
(tweet :bob "Hey @alice are you online?")
(follows :bob :alice)
(tweet :bob "I'm so bored...")
pprint)))
;; =>
;; {:delegates
;; ({} ;; TwitterPrintHandler
;; {:timeline-user :alice, ;; TimeLineForUser alice
;; :following #{:bob}, ;; Alice finally follows bob
;; :entries ;; ...and collected three entires in the timeline
;; [{:user :alice, :message "Hey bob is in da house!"}
;; {:user :bob, :message "Hey @alice are you online?"}
;; {:user :bob, :message "I'm so bored..."}]}
;; {:timeline-user :bob,
;; :following #{:alice}, ;; In the end bob follows alice
;; :entries ;; ...and has a timeline, too!
;; [{:user :alice, :message "Hey bob is in da house!"}
;; {:user :bob, :message "This is bob but nobody notices!"}
;; {:user :bob, :message "Hey @alice are you online?"}
;; {:user :bob, :message "I'm so bored..."}]})}
;; What can be improved? Remove the boilerplate from the delegator definition
;; and create macro that can be invoked as follows
;;
;; (defdelegator protocols delegates)
;;
;; I included the list of protocols because I didn't found a way to determine all
;; protocols that are implemented by an arbitrary value. I'm not sure if it
;; even would be desirable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment