Fix your Clojure code: Clojure comes with design patterns (Part 1)
Clojure either directly supports, or easily supports the use of common design patterns like singleton, command, observer, visitor
Patterns
A motivational story
Suzi, after busting her ass at her first Clojure job, lands her second Clojure job at StartupAI. This company moved her half-way across the country to the Big City™, filling her with naive excitement. When she finishes company on-boarding, Suzi submits her first pull request after several days of staggering through the code. While the code is the same Clojure she has always known and loved, the repository gives her stress headaches.
The persistence namespace, the one with all the database queries, has eight different functions to get the same data using same query. Next, a minuscule change in the application layer causes dozens unit tests to fail, and to top it off, it takes three weeks to a pull request to get approved for the change.
"I moved across the country for this?!".
Here we go again
Although I'm sure some people will say it is, Suzi's dilemma isn't unique. Large Clojure codebases sometimes suffer from a lack proper care and abstraction. I suspect a lot of people rely too heavily on the pure function. Functions are great, but over time they create a codebase laden with high coupling and low cohesion. So, how do we help Suzi create a codebase satisfying our desired "ilities"? Lucky for Suzi, Clojure either directly supports, or easily supports design patterns like singleton, command, observer, visitor, and iterator. Before we get into those patterns though, we need to get little more abstract than the pure function.
We're speaking the same language
Let's start with the abstraction just above the function: Protocols. Protocols are Clojure's analogue to the interface. At a higher level, we can call a bag of function inputs and their outputs (or function signatures) an interface. Without diving into full-blown type theory, when something satisfies the interface, we can also say that something is a type of that interface. I'll even argue that we don't need a formal programming construct to achieve this "typehood". On a basic level, by definition, every Clojure sequence supports the functions cons
,first
,rest
, and count
. Does that mean we should go out and create our own collection types? Not likely, the built-in types are quite good, but we do have few handy constructs for creating or extending types if we want.
While we could define types with functions like sequence, we would lose the biggest benefit of the protocol construct, Polymorphism.
Null Object
Create an empty implementation for an interface.
By my "interface is a bag of functions" reasoning, nil
makes a good Null Object for sequences, collections, and maps (or maybenil
is a sequence, collection, and map, but I'll leave that to the Philosophers of Computer Science ;).
When to use it
When simulating the behaviour of a type without affecting the process.
Clojure analogue
nil
- Clojure sequence, collection, and map operations all support nil
as their input sequence, collection, and map, respectively, either returning an empty sequence, collection, map, or nil
.
Keep in mind
Not everything supports nil
, and nil
is different than Java's null pointer reference, yet some gotchas with nil
can throw NullPointerExecption
, so some defensive programming with if
or some->
/some->>
might be needed to avoid pitfalls.
Sample Code
Repl output:
user> (:keyword nil)
;; nil
user> (get nil :hi)
;; nil
user> (assoc nil :hi "bye")
;; {:hi "bye"}
user> (first nil)
;; nil
user> (rest nil)
;; ()
user> (next nil)
;; nil
user> (count nil)
;; 0
user> (nth nil 10)
;; nil
user> (pop nil)
;; nil
user> (peek nil)
;; nil
user> (println "You get the idea.")
You get the idea.
;; nil
Singleton
Ensure a class has one instance, and provide a global point of access to it.
A less philosophical pattern, the singleton appears a lot in Clojure codebases. Thanks to software transactional memory, we don't have to worry about non-atomic operations, but it's still an anti-pattern because one piece of client code may update the atom with something another piece of client code doesn't expect, causing a hard-to-debug error.
When to use it
Just don't.
Clojure analogue
defonce
macro and an atom/agent
Keep in mind
Global state is best to be avoided unless absolutely necessary. Global configuration, database connection, and web servers are arguably important enough objects to use this, since they're the stateful sole instance and must stick around for the process runtime, but most Clojurians go in for a state management library or framework like Component, Mount, or Integrant.
Sample Code
(defonce web-server (atom nil))
(defn start-server
[]
(if @web-server
(log/warn "Server is already running!")
(reset! webserver (run-jetty routes {:port (parse-int (:port config))
:join? false}))))
Command
Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations.
The errors from functions are a lot easier to debug though because of stack traces. Our command pattern, the object-oriented version of callbacks, can be implemented in Clojure via higher-order functions (obviously).
When to use it
When you need to decouple the execution of a block of code from when it needs to be called.
Clojure analogue
Higher-order functions. All functions in Clojure can be passed around as arguments, and support closures.
Keep in mind
If you're passing around lambdas a lot, use the fn
form and give it a name to help with debugging. The road to callback hell is paved with #(...)
lambdas.
State with Callbacks are typically considered an anti-pattern, but JavaScript promises are a thing, so pick your poison.
Sample Code
;; Trivial example
(defn fire
"fire!!!"
[]
(println "Fire the gun"))
(defn jump
"jump!!!"
[]
(println "Jump"))
(defn run
"run!#!#"
[]
(println "Run enabled"))
(defn diagonal-aim
"Shoot things on the walls"
[]
(println "Aiming diagionally"))
(defn remap-button
"Remap a player's button to a function.
Can be used on the remap menu screen, or
for temporary game mechanics."
[player button f]
(swap! player assoc-in [:controls button] f))
;; (remap-button player :a-button fire)
(defn do-buttons!
"Execute the button actions assigned to the
players controller map."
[button-pressed controllor-map]
(let [{:keys [a-button
b-button
y-button
x-button]} controller-map]
(case button-pressed
"A" (a-button) ;; "Execute" the command!
"B" (b-button)
"X" (x-button)
"Y" (y-button))))
Doing, redoing, and undoing
Because I know people will say it, here is an example of the command pattern supporting undo with all the previously made edits. Supporting redo will be left as an exercise for the reader.
(defn do-undo
"Create a command and undo over a pure function"
[f]
(let [state (atom nil)]
{:exec (fn execute!
[& args]
(let [v (apply f args)]
(swap! state conj v)
v))
:undo (fn undo!
[]
(swap! state pop)
(peek @state))}))
(defn change-cell
"A supposedly pure function"
[s coord]
(assoc sheet coord s))
(let [{:keys [exec undo]} (do-undo change-cell)]
(def change-cell-cmd exec)
(def change-cell-undo undo))
Observer
Define a one-to-many dependency between objects so that when one object changes state, all the dependents are notified and updated automatically.
I see a lot of functions written to watch for something to happen. Typically, developers will use a loop
over a function until some state changes or "event" transpires. This works, but couples the event to the event reaction. Object-oriented programming addresses this with the Observer pattern, but we can address it with a Clojure watch.
When to use it
When you want to decouple changes in state from what the state affects, also known as a publish/subscribe pattern.
Clojure analogue
There are two ways to do this in Clojure:
- Clojure watches. You can add a watch to any atom, agent, var, or ref with
add-watch
and remove it withremove-watch
. - core.async's
pub
andsub
.pub
creates a channel from a existing channel with optional topic, and then other channels cansub
to the pub channel.
Keep in mind
When the watch target (the var/atom/agent/ref) changes, all the watch functions will be called syncronously even if multiple threads are manipulating the watch target. So, it's best to use the new-state
argument in all your watch functions.
Sample Code
(defn unlock
"Unlocks the achiement given"
[player k]
(when-not (unlocked? player k)
(do-unlock! player k)))
(defn on-bridge?
"predicate to see if player is on the bridge"
[player]
(is-colliding? (get @entities :bridge) @player))
(defn achievement-toaster
[k]
(let [a (get achievements :player-fell-achievement)]
(when a
(swap! entities :current-toaster a))))
(defn unlock-achievement
[key player old-state new-state]
(case (:last-event new-state)
:player-fell (when (on-bridge? new-state)
(unlock player :player-fell-achievement)
(achievement-toaster :player-fell-achievement))))
(defn watch-actions
[player]
(add-watch player :achievements unlock-achievement))
State
Allow an object to alter its behavior when its internal state changes. The object will appear to change it's class.
We might not care about reacting to the state change though. Heck, we might not even care how the state happens, we just care about the results. A state pattern can give use a nice function to conceal our messy state transitions, so we don't have to write giant cond
statements.
When to use it
The real thing to remember here is that we want to model a finite state machine (FSM) with our functions.
Clojure analogue
(atom f)
- An atom over a function is the simplist version of the state pattern.(loop [state (init-state data)] (recur (state data)))
- Alternatively, the the current state could be re-bound a loop binding since the current state function always returns the next state.
This take uses a multimethod over an atom. This certainly models an FSM, but we kind of lose our ability to decouple the state from the context.
Keep in mind
-
The current state will always return the next state in the machine. Modeling state this way can be tricky, but can be super helpful if you find yourself with many nested
case
orcond
forms. You'll have to decide of the cyclomatic complexity is worth the trade-off. -
When your state transition returns one state, this method is fine. If you need more functions in a single 'state', I'd consider using protocols to create states. I intentionally left this out of the sample code because it was huge.
-
If you need a history for your state machine, like a push-down automata, you can
conj
each state function to a vector andpeek
at the top. -
The atom-free state pattern with loop bindings can be problematic if the current state needs to modify the binding itself. Use the atom version instead.
Sample Code
(defn play-game
[game]
(let [{:keys [components player]} game]
(do
(doseq [component components]
(update! component))
(draw-game game)
(if (= :paused (:action @player))
pause-menu
play-menu))))
(defn start-menu
[game]
(let [{:keys [menu player]} game]
(do
(draw-game game)
(if (= :affirm (:action @player))
(when-let [menu-item (collision player)]
(cond ;; our state transition
(= menu-item :start-game) play-game
(= menu-item :options) options-menu
(= menu-item :quit-game) quit-game
:else start-menu))))))
(let [game-state (atom start-menu)]
(defn start-game
[game]
(reset! game-state (@game-state game))))
(defn main
[]
(let [game-engine (game-engine)]
(loop [quit? (:quit game-engine)]
(when-not quit?
(start-game game-engine)
(recur (:quit game-engine))))))
;; or stateless 'state'
(defn stateless-start-game
[game]
(start-menu game))
(defn stateless-main
[]
(let [game-engine (game-engine)]
(loop [game-state (start-game game-engine)]
(when-not (:quit game-engine)
(recur (game-state game-engine))))))
Visitor
Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
Giant case
statements are sometimes addressed with multimethods. That's a valid strategy, but multimethods give us multiple dispatch for free.
When to use it
When you want to create new functionality without changing the existing code structure.
Clojure analogue
multimethods. Clojure's multimethods offer dynamic dispatch functionality right out of the box, so we can put a multimethod inside of a multimethod to get the double dispatch functionality.
Keep in mind
Multimethods have a performance penalty. You're unlikely to experience it, but it is there, so just be mindful of it in performance sensitive applications. If you need fast dispatching, consider dispatch on protocols instead.
Sample Code
(defrecord Equipment [elements])
(defrecord Sword [name])
(defrecord QuestItem [name])
(defrecord Armor [name])
(defmulti visit (fn [f x y] [(class x) (class y)]))
(defmethod visit :default [f x y])
(defmethod visit [Equipment Object] [f equipment other]
(let [{:keys [elements]} equipment]
(doseq [element elements]
(f element other))))
(defmulti do-stuff (fn [x y] [(class x) (class y)]))
(defmethod do-stuff :default [x y]
(println (format "You sit there bewildered, unable to understand what to do with %s and %s" x y)))
(defmethod do-stuff [Sword Long] [x y]
(println (format "Swing your sword %s does %d damage" (:name x) y)))
(defmethod do-stuff [Sword String] [x y]
(println (format "Using your sword %s, you make a noble gesture %s" (:name x) y)))
(defmethod do-stuff [Armor Long] [x y]
(println (format "Your armor %s has prevented %d damage" (:name x) y)))
(defmethod do-stuff [Armor String] [x y]
(println (format "You remove armor %s as a noble gesture %s" (:name x) y)))
(defmulti store-stuff (fn [x _] (class x)))
(defmethod store-stuff :default [x y]
(println "You stored: " x))
Repl output:
user> (def equipment (->Equipment [(->Sword "Excalibur") (->Sword "Gramr") (->Sword "Zulfiqar") (->Sword "Durendal") (->QuestItem "dungeon key") (->Armor "Achilles")]))
;; #user/equipment
user> (visit store-stuff equipment 10)
You stored: #user.Sword{:name Excalibur}
You stored: #user.Sword{:name Gramr}
You stored: #user.Sword{:name Zulfiqar}
You stored: #user.Sword{:name Durendal}
You stored: #user.QuestItem{:name dungeon key}
You stored: #user.Armor{:name Achilles}
;; nil
user> (visit store-stuff equipment nil)
;; nil
user> (visit do-stuff equipment 50)
Swing your sword Excalibur does 50 damage
Swing your sword Gramr does 50 damage
Swing your sword Zulfiqar does 50 damage
Swing your sword Durendal does 50 damage
You sit there bewildered, unable to understand what to do with user.QuestItem@bb0c2cb0 and 50
Your armor Achilles has prevented 50 damage
;; nil
Iterator
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Like our Visitor pattern, the Iterator doesn't really reveal anything special. We can use it without any extra effort which makes this section fluff. Enjoy!
When to use it
When you want to have an ordered, sequential container for other objects which sounds an awful lot like a sequence or collection.
Clojure analogue
Lists and Vectors - Clojure list and vector types are sequence and collection, respectively, and support arbitrary typed elements. They have a rich set of functions for operating on them like doseq
for
map
filter
etc.
Keep in mind
Make sure you use doseq
for side-effects. Some sequences (and transforms from collections) are Chunked, and can create strange behavior if using side-effects with map
or filter
.
A Java Iterator (supporting the java.util.Iterator interface) is not a sequence in Clojure. If you are working with a Java Iterators, be sure to call the iterator-seq
function on your Java Iterator to get a (chunked) sequence. Some Java types do return collections though, and those should be fine in map
or filter
since Clojure collections inherit from the Java collections.
Sample code
(->> (range 1000) ;; Range gives us a list of numbers
(filter even?) ;; Plugs into
(map inc))
1. Because it's my favorite book on this subject, the inspiration for this post by and large came from Game Programming Patterns (print). The sample code for Command and Observer, as well as each pattern's section structure, was adapted for this post's theme.
2. This post is also largely a rebuttle to mishadoff's Clojure design patterns.
3. Except for the Null Object, all quotes underneath pattern names come directly from Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. ISBN 0-201-63361-2.