public
Created

Define events a protocol methods

  • Download Gist
handler-protocol.clj
Clojure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
;; 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 if found no way to determine all
;; protocols that are implemented by an arbitrary value. I'm not sure if it
;; even would be desirable.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.