###Goal
The GOF books states that the builder pattern is to:
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
However we get much more information out of what the GOF book states as consequences of using the Builder Pattern:
It lets you vary a product's internal representation by hiding the internal representation from the client. To produce a new product define simply definea new builder
It isolates code for construction and representation. A client doesn't know anything about the object that built the product or the product itself.
It gives you finer control over the construction process. You can take actions between steps to build up the result.
Lets make sure we understand the value of each of these:
###Implementation in Clojure:
Lets give ourselves some concrete tasks so we know were done. The third one is just an example as its hard to nail down concretely:
So the implementation needs to:
First thing is Hide the products internal representation from the client Lets consider what a client is first. It's just the caller for a class right? We don't need to model that here, its just the top level name space. A plain function hides its internals well enough to satisfy this condition as the client doesn't really know what function did or what it returned.
(defn make-pizza [order] order)
We update our todo list:
Next on the list is, Hide how the builder works from the client. We might be tempted to say we already did this, but our goal here is to hide the type of builder the client is using. Clojure doesn't have classes, but it can dispatch to different functions based on data using a multimethod. This way we can order multiple things, like a pizza or a salad.
(defmulti order (fn [{:keys [type]}] type))
(defmethod order "salad" [order] (str "i cant believe you ordered a salad from a pizza place"))
(defmethod order "pizza" [order] (merge order {:order-id 5}))
We update our list:
Ok now we need to design a construction process that can be shared. What were constructing is the result of the function. That's just some data, so we we want to modify it, we can, well just call another function on the return value. Sense the functions are pure and the data is immutable this is safe.
(merge {:coupon "1 dollar off"} (order {:size "medium" :type "pizza"}))
update date the list:
And were Done.
###Summery
If you ignore the How of the Builder pattern in the GOF book and look at the Why it comes down to 2 things:
In clojure our data isn't mutable and we have so few types its simpler to transform our data in the open using core library functions that create a ritual around it.
There are a lot of other explanations online that seem to focus on the how something is built up. The ritual of:
builder
.addSomething("thing")
.addAnotherThing("thing")
They say this is helpful because the alternative is having to many constructors for an object and it becoming hard to tell from the method signature should go where. I have come to favor simple function signatures that just take a map, so to address the above issue would look like this:
(defmethod order :pizza [order]
[{:keys [crust sauce cheese size] :as order}]
(merge order {:order-id 5}))
As a reader i know that keys are being looked for, and as a user of the function i pass my arguments as a map so i there is no confusion over what relates to what. After all, did the order of those arguments matter? If not, why are they ordered!
(order {:crust "regular" :sauce "red" :cheese "mozzarella" :size "large" :type "pizza"})
Finally someone might quibble that we haven't addressed some of the control over data that we see in other functions such as having defaults or checking the arguments range. By passing in the defaults, and adding a pre check, we show we can satisfy both of those.
(def pizza-defaults {:crust "regular" :sauce "marinara" :cheese "mozzaralla" :size "medium"})
(defmethod order :pizza [order defaults]
[{:keys [crust sauce cheese size] :as order} defaults]
{:pre [(#{"medium" "small" "large"} size)]}
(merge defaults order {:order-id 5}))
Showing default:
(order {:crust "regular" :size "large" :type "pizza"})
Showing pre-check:
(order {:crust "regular" :type "pizza"})
That should throw and error. If its not its because of some mistake i have made and not a lack of language feature.