Fix your Clojure code: Clojure comes with design patterns (Part 2)
Patterns
Our story continues
Edmund, a bright, awkward young man with a masters degree from a prestigious University, loves functional programming, but his views are too pure for Suzi's taste. He always drones on about how we don't need to think about any of those design patterns things because "that's only for object-oriented programming".
However, Edmund's latest project with an intern, a load testing application, was taking far too long, like most projects at StartupAI. Despite the mythical (wo)man-month, Suzi's manager asks her to join the project, and she agrees.
"We see the heart of this project as state machine" Edmund says, during a code walk through.
To Suzi surprise, she sees a dense case
form. "Uh oh" thinking to herself.
Next Edmund shows her the dozens of functions created to manufacture types to kick start the state machine. Of course, whenever they have to change these functions, they have to change the state machine and vise-versa. To top it all off, they're using Clojure futures to generate the load tests, attempting to coordinate all the futures and their requests. Lucky for Suzi, she knows why they're not progressing on the project, and why it doesn't work at all.
"I have an idea, why don't you replace this case
form with a state pattern, and these functions with a builder?" Suzi says gently presenting the idea.
"Oh, I just prefer not to do those kind of things in functional programming." He replies.
"Right, but it would decouple the state machine from the this namespace, and you can decouple your functions here from this code down here." She says while pointing with her pen at the functions on the screen from earlier, and gesturing downward at the state machine. Continuing, "And, you can ditch this whole futures thing. Just use core.async".
"What's core.async?" Edmund asks no one in particular while typing 'clojure core async' into the browser search bar.
"Communicating Sequential Processes in Clojure. It handles thread pooling and everything, all we have to do is dispatch work to the 'go loops'." Suzi answers.
Oh.
Builder
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
A lot of patterns become thinking about, and structuring of, the flow of higher-order functions and their composition. While the functions may seem vague, we need to keep in mind our function's intent. With the builder pattern we want to delegate object (or function) construction (composition) .
When to use it
When you want to decouple the construction of a complex object from the context it's used in.
Clojure analogue
partial
orfn
andcomp
- Most design patterns are a response to missing features from functional programming. The command pattern address the lack of higher order functions in Object Oriented programming languages. So in this respect, the builder pattern addresses the lack of currying. Unfortunately, Clojure also doesn't support currying, but we can get a close approximation with partial function composition usingpartial
orfn
andcomp
.cond->
andassoc
and related map functions - In practice most "builders" are really just methods to set some kind of member values rather than delegating object construction. We see this a lot in something like Go where a struct has receivers that return a new struct, allowing for chaining of call likeMyCoolStruct.WithFun("stuff").WithHydration("Water").WithCode("Clojure")
. If you're idea of a builder looks like this, you can probably get pretty far with threading macros and map functions likeassoc
,update
,merge
since they all take a hash map as an argument and return a hash map, or just make up your own functions!
Keep in mind
- Partial function composition can get hairy since you can't name the function produced by either
partial
orcomp
. - When using
partial
andcomp
, take care to remember that transducers exist. Depending on your use case composing a transform from transducers (map
,filter
, etc) will be much more performant than creating partial functions over them.
Sample code
;; builder
;; now we can delegate our map construction to this
;; function.
(defn build-s3-client
[mock-creds ssl default-bucket sts]
(let [if-fn (fn [cond f] (if cond f identity))]
(comp
(if-fn (not (empty? creds))
#(let [secret (s/gen :specs/client-secret)
access-key (s/gen :specs/access-key)]
(merge % {:client-secret (g/generate secret)
:access-key (g/generate access-key)})))
(if-fn ssl #(assoc % :ssl true))
(if-fn (and (string? default-bucket)
(not (empty? default-bucket)))
#(assoc % :default-bucket default-bucket))
(if-fn (and (string? sts)
(not (empty? sts))
(some? (re-matches #"." sts)))
#(assoc % :sts-grant sts)))))
(def s3-client
(build-s3-client {} false "my-bucket" "real-token"))
;; Chaining technique
(defn ->s3-client
[]
(let [{:keys [aws-secret-key
aws-access-key]} env]
{:client-secret client-secret
:access-key access-key}))
(defn with-ssl
[m]
(assoc m :ssl true))
(defn with-default-bucket
[m bucket]
(assoc m :default-bucket bucket))
(defn with-sts
[m token]
(assoc m :sts-grant token))
(defn with-mock-credentials
[m]
(let [secret (s/gen :specs/client-secret)
access-key (s/gen :specs/access-key)]
(merge m {:client-secret (g/generate secret)
:access-key (g/generate access-key)})))
(defn build-s3-client
[bucket]
(-> (->s3-client)
(with-ssl)
(with-default-bucket bucket)))
(defn mock-s3-client
[bucket]
(-> (->s3-client)
(with-mock-credentials)
(with-bucket bucket)))
Chain of Responsibility
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
The structure of this pattern seems similar to the builder, but our intent for our functions is different. And, If I'm being honest, the only Chain of Responsibility I've ever seen, even outside of Clojure, is the middleware chain in ring applications.
When to use it
When you want to decouple a request from its handler.
Clojure analogue
->
or->>
andfn
- Using the thread macro through lambdas that have the same function signature, creating closures over closures until its final form.
Keep in mind
- Chain of Responsibility in Clojure has the same caveats as the command pattern from the last post. Be sure to name your
fn
forms because the stack traces from the Chain of Responsibility are challenging to debug as the chain can sometimes be quite long. - Like all threading macros, If the chain order matters, you will have to structure your handlers in reverse order. For example, in the case of ring middleware you want the function returned by
wrap-authentication
to evaluate before the function returned bywrap-authorization
.
Sample code
Ring middleware gives us the best and most prevalent example of a Chain of Responsibility in Clojure. In this example wrap-authentication
and wrap-authorization
are adapted from buddy.auth.
(defn wrap-authentication
"Ring middleware that enables authentication for your ring
handler. When multiple `backends` are given each of them gets a
chance to authenticate the request."
[handler & backends]
(fn authentication-handler
([request]
(handler (apply authentication-request request backends)))
([request respond raise]
(handler (apply authentication-request request backends)
respond
raise))))
;; lots of code in between. Just pretend it's here for
;; the sake of this example.
(defn wrap-authorization
"Ring middleware that enables authorization
workflow for your ring handler.
The `backend` parameter should be a plain function
that accepts two parameters: request and errordata
hashmap, or an instance that satisfies IAuthorization
protocol."
[handler backend]
(fn authorization-handler
([request]
(try (handler request)
(catch Exception e
(authorization-error request e backend))))
([request respond raise]
(try (handler request respond raise)
(catch Exception e
(respond (authorization-error request e backend)))))))
;; In another namespace:
;; Middleware usage
(def app
(-> compojure-routes
(wrap-authorization jwe)
(wrap-authentication jwe)
(wrap-cors :access-control-allow-origin [#".*"]
:access-control-allow-methods [:get :post])
(wrap-json-response {:pretty false})
(wrap-json-body {:keywords? true})
(wrap-accept {:mime ["application/json" :as :json
"text/html" :as :html]})
(wrap-cookies)
(wrap-params)))
Proxy
Provide a surrogate or placeholder for another object to control access to it.
Unlike the only Chain of Responsibility I've seen, I have seen a proxy once or twice. Usually developers don't use the proxy
form, rather opting to wrap a function in another function to restrict access, lazy typing, or virtualization.
When to use it
The Gang of Four quote is pretty legible on this one. Use a proxy as a 'middleman' for (controlling) access to another object. You can also use it for creating a lazily loaded object if a LazySeq
doesn't do it for you.
Clojure analogue
proxy
- Clojure ships with a whole set of proxy-related functions. They are seldom used as developers typically prefer to usereify
(and related forms). Unlikereify
,proxy
can be used to instantiate Java classes, not just interfaces and protocols.- You could use higher-order
fn
to have a proxy too, but at this point the idea is getting repetitive, so I'm just going to keep it to the quirky, interesting stuff.
Keep in mind
- The proxy functions
proxy
,proxy-super
,init-proxy
,update-proxy
,construct-proxy
,get-proxy-class
, andproxy-mapping
are old (Clojure 1.0) and reminiscent of Object-oriented programming. If you didn't know what a proxy was, you might end up doing double the work of plain old interop when usingproxy
. - Some of the proxy functions like
proxy-super
expand to use reflection on theirthis
argument unless type hinted. - Like all extensibility on the JVM,
proxy
can't inherit from afinal class
which also means no proxies to Clojure records. - You can't proxy a proxy, meaning you can't use
proxy
on something that already implements Clojure'sIProxy
interface. If you could, that'd be OOP. This isn't Common Lisp.
Sample code
(defn access-controlled-input-stream
"Returns an access controlled InputStream"
[user]
(proxy [java.io.InputStream] []
(read
([]
(if (allowed? user)
(let [^java.io.InputStream this this]
(proxy-super read))
(throw (ex-info "Unauthorized"
{:user user}))))
([^bytes b]
(if (allowed? user)
(let [^java.io.InputStream this this]
(proxy-super read b))
(throw (ex-info "Unauthorized"
{:user user}))))
([^bytes b off len]
(if (allowed? user)
(let [^java.io.InputStream this this]
(proxy-super read b off len))
(throw (ex-info "Unauthorized"
{:user user})))))))
Adapter
Convert the interface of a class into a another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
You don't need to always use a function to wrap something, you can also use an adapter. Adapters are the ultimate glue code and de facto polymorphism (protocols) in Clojure. If we consider a type an interface (or bag of functions™), any library or API could be considered a interface, so we can wrap it in an adapter for core business logic for when we want to swap it out1. Or not, if you're an "aren't going to need it" person.
When to use it
When you want to use an implementation with a different interface. Also called a wrapper.
Clojure analogue
- protocols - Any Clojure type can be extended to support a particular protocol using
extend-type
.
Keep in mind
- Using an adapter can be a symptom of a leaky abstraction.
Sample code
;; Shamelessly using protocols example from last post
;; our expect interface
(defprotocol MoneySafe
"A type to convert other money type to bigdec"
(make-safe [this] "Coerce a type to be safe for money arithmetic"))
;; our conversions, could also extend Java objects here.
(extend-protocol MoneySafe
java.lang.Number
(make-safe [this] (bigdec this))
java.lang.String
(make-safe [this]
(try
(-> this
(Double/parseDouble)
(bigdec))
(catch NumberFormatException nfe
(println "String must be string of number characters"))
(catch Exception e
(println "Unknown error converts from string to money")
(throw e))))
clojure.lang.PersistentVector
(make-safe [this]
(try
(let [num-bytes (->> this (filter int?) (count))]
(if (= (count this) num-bytes)
(->> this
(map char)
(clojure.string/join)
(Double/parseDouble)
(bigdec))
(throw (ex-info "Can only convert from vector of bytes"
{:input this}))))
(catch NumberFormatException nfe
(println "Vector must be bytes representing ASCII number chars")
(throw nfe))
(catch Exception e
(println "Error converting from vector of bytes to money")
(throw e)))))
;; our client
(defn ->money
[x]
(make-safe x))
Template Method
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine steps of an algorithm without changing the algorithm's structure.
Yet lots of people need template methods (functions?) since I've seen plenty of template functions in Clojure codebases over the years. Template functions get pretty hairy when the delegate functions get spread out over the codebase, doubly so if unamed lambdas are used. If you need to write functions across the vast expanse of your namespaces, use a strategy instead.
When to use it
Use a template method when you want to decouple primitives or subroutines of an algorithm from the overaching structure.
Clojure analogue
- Variodic function arguments with function composition - We could call this a template function rather than a template method. Variodic functions will give us the ability to define defaults as well as mandatory implementations.
do-template
-do-template
evaluates an expression, grouping and feeding it's arguments into the template. It's good for a "template method" where the overrides are primitives or all "subtypes" of the template method/function are evaluated in the same client.
Keep in mind
- Template methods are very similar to the strategy pattern, but tightly couple the subtypes to the template, implying function definition locality is your friend.
- Depending on how important isolation of your variodic arguments is, You can swap out traditional variodic arguments for variodic keyword arguments in Clojure 1.11. For example, in our sample code below if we want to change the implementation of build-windows without changing build-foundation:
Sample code
;; our template defaults
(defn- build-foundation-default
[]
(println "Building foundation with cement iron rods and sand"))
(defn- build-windows-default
[]
(println "Building glass windows"))
;; The "template"
(defn build-house-template
([build-pillars build-walls]
(build-house-template build-pillars
build-walls
build-foundation-default
build-windows-default))
([build-pillars build-walls build-foundation]
(build-house-template build-pillars
build-walls
build-foundation
build-windows-default))
([build-pillars build-walls build-foundation build-windows]
(build-foundation)
(build-pillars)
(build-walls)
(build-windows)
(println "House is built.")))
(defn wooden-house
[]
(build-house-template #(println "Building pillars with wood coating")
#(println "Building wooden walls")))
(defn glass-house
[]
(build-house-template #(println "Building pillars with glass coating")
#(println "Building glass walls")))
;; our client
(defn house-builder
[build-house]
(build-house))
Flyweight
Use sharing to support large numbers of fine-grained objects efficiently.
Naturally, the Strategy pattern would follow from the template function, but I decided to do flyweight next. Flyweight is basically a cache. If you want to get fancy, you can use the Clojure core.cache library where you get a cache protocol, conveinence macros, and eviction algorithms. Real caching. If you need to share some good ol' intrinsic state, here is a couple flyweight ideas for you.
When to use it
Use a Flyweight when you have overlapping object usage between clients. Flyweights typically destinguish between extrinsic state and intrinsic state where the extrinsic state is context dependend and intrinsic is context independent. So, the intrinsic state gets shared between client contexts.
Clojure analogue
memoize
-memoize
will store the return value of a function mapped to it's arguments, conflating the two parts of the flyweight, the flyweight factory and operation, into one singlememoize
call.fn
over anatom
- We might not necessarily want to use our extrinsic state as our factory function key. In normal dev speak, that means we might not necessarily want our function arguments to determine what state gets shared between clients.
Keep in mind
- Introducing state (not the pattern) into your application means you have the potential to introduce certain bugs you wouldn't otherwise have. Use a watch to keep track of state changes if you're concerned about it.
- Using the
atom
version of the ->flyweight can be use in a local binding, but it will create a new atom, so be mindful of that.
Sample code
(defprotocol Sprite
"Describes a totally real sprite type"
(draw! [this x y height width]
"draw something to the screen."))
(deftype PlayerSprite [sprite-sheet x y height width]
Sprite
(draw! [this x y height width]
"Drew the player on the screen."))
(deftype EnemySprite [sprite-sheet x y height width]
Sprite
(draw! [this x y height width]
"Drew the enemy on the screen."))
;; Our "flyweights"
;; could store anything in memoize.
(def player-sprite
(memoize #(->PlayerSprite (slurp "./spritesheet.png")
{:x 0 :y 0 :height 32 :width 32})))
(def enemy-sprite
(memoize #(->EnemySprite (slurp "./spritesheet.png")
{:x 33 :y 33 :height 32 :width 32})))
;; Atom example
;; our flyweight function is similar to the body of memoize
;; except we want to use a keyword to get value instead
(defn ->flyweight
[]
(let [mem (atom {})
player-coords {:x 0 :y 0 :height 32 :width 32}
enemy-coords {:x 33 :y 33 :height 32 :width 32}
sprites {:player #(->PlayerSprite (slurp "./spritesheet.png")
player-coords)
:enemy #(->EnemySprite (slurp "./spritesheet.png")
enemy-coords)}]
(fn [k & args]
(if-let [e (find @mem k)]
(val e)
(when-let [f (get sprites k)]
(let [ret (apply f args)]
(swap! mem assoc k ret)
ret))))))
(def flyweight (->flyweight))
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
There's no official Clojure library for Strategy though. This is kind of a boring pattern in Clojure because it's just a lambda, so I'm including it completeness, but what blog post doesn't have fluff.
When to use it
Use a Strategy when you need to decouple algorithms from where they're being used.
Clojure analogue
fn
(again) - This time you'll have think real hard about the input and output of the higher-order functions you're passing around.sort-by
allows you to pass in a key-fn as a strategy for getting the values to pass to the compare operation (or would that be a template method? Hmm....).
Keep in mind
- The usual stuff with
fn
. Name yourfn
for debugging and recursion. I suppose you could name themblah-blah-strategy
, but you might get flak for saying the pattern name in the code.
Sample code
;; I'm not going to type out the sorts because I haven't
;; been in a CS classroom for years.
(defn quick-sort
[coll])
(defn merge-sort
[coll])
(defn radix-sort
[coll])
(defn find-key
"Performs a binary search on coll after calling
sort-fn on coll."
[key coll sort-fn]
(find (sort-fn coll) key))
;; client somewhere
(find-key :fun-key (get-list) radix-sort)
1. Some people call this ports and adapters architecture.
2.The template method example was inspired by this Digital Ocean blog post.