Patterns - Builder
The last blog post was about the Abstract-Factory pattern. We have seen that in Common Lisp there is hardly a pattern visible.
One could say patterns are code constructs that are repetetive. Almost like a language in a language. Paul Graham once asked: "Are Patterns a language smell?".
Builder
Today we look at the Builder pattern. Similarly as the Abstract-Factory pattern is the Builder a creator pattern. It can help creating instances of objects. The difference to Abstract-Factory is that the Builder is tightly coupled to the class it creates. Yet, it allows to hide details of the class that only the Builder has access to while being in the same package. There can be different Builders that create instances of the same class but with a different configuration. If we wanted to do this with the classes directly we'd have to open them up. A Builder can also hide complexities when creating objects while providing a more simple interface to the user.
Example in Scala
First, we will look at some Scala code.
We want to create an object (a dungeon) like this:
val dungeon = new CastleDungeonBuilder()
.setDifficulty(VeryDifficult)
.addMonsters(15)
.addSpecialItems(5)
.get()
First we create a Builder. It is a special kind of Builder that builds a castle dungeon. We set a difficulty, add monsters and some special items that the dungeon object should place somewhere.
The CastleDungeonBuilder looks like this:
class CastleDungeonBuilder extends IDungeonBuilder {
override protected val theDungeon = new Dungeon(CastleDungeonKind)
def addMonsters(n: Int): IDungeonBuilder = {
// add nice monsters
val filteredMonsters = Monsters.filter(m => m.creepyFactor < 5)
theDungeon.monsters = (0 until n)
.map(filteredMonsters(new Random().nextInt(filteredMonsters.size)))
this
}
}
As part of creating the Builder instance it creates a Dungeon
instance. This CastleDungeonBuilder
has a speciality, the monsters it adds are nice monsters that have a low 'creepy factor'. There is also a CellarDungeonBuilder
that adds monsters with a 'creepy factor' >= 5 (on a scale from 0 to 10). The right monsters for a cellar.
The method addMonsters
also hides some complexity from the user. It just allows to say how many monsters to add, but to the dungeon instance the Builder sets a collection of pre-configured monsters instances.
The abstract Builder (where CastleDungeonBuilder
and CellarDungeonBilder
inherit from) actually only does some generic configuration. It looks like this:
trait IDungeonBuilder {
protected val theDungeon: Dungeon
def setDifficulty(difficulty: Difficulty): IDungeonBuilder = {
theDungeon.difficulty = difficulty
this
}
def addMonsters(n: Int): IDungeonBuilder = {
theDungeon.monsters =
for(i <- 0 until n)
yield Monster(new Random().nextInt(3), new Random().nextInt(10))
this
}
def addSpecialItems(n: Int): IDungeonBuilder = {
theDungeon.specialItems =
for(i <- 0 until n)
yield SpecialItem(new Random().nextInt(7))
this
}
def get: Dungeon = theDungeon
}
This is the Dungeon
class itself:
class Dungeon(private _kind: DungeonKind) {
private var _difficulty: Difficulty = Difficulty.NotDifficultAtAll
private var _monsters: List[Monster] = Nil
private var _specialItems: List[SpecialItem] = Nil
def difficulty: Difficulty = _difficulty
private[dungeon]
def difficulty_=(d: Difficulty): Unit = _difficulty = d
def monsters: List[Monster] = _monsters.copy
private[dungeon]
def monsters_=(list: List[Monster]): Unit = _monsters = list.copy
def specialItems: List[SpecialItem] = _specialItems.copy
private[dungeon]
def specialItems_=(list: List[SpecialItems]): Unit = _specialItems = list.copy
}
While it allows to query the properties. It doesn't allow to set them except from within the same package. So the Builder must be defined in the same package as the dungeon class is.
The poor-man's Builder
Scala allows named and optional parameters in functions and constructors. A poor-man's Builder pattern in Scala could simply be to use those features on object creation together with auxiliary constructors. Though this doesn't allow the abstraction of a Builder and the encapsulation of the object properties but could be sufficient in some cases.
Example in Common Lisp
In Common Lisp we could certainly build a similar structure for Builders with separate classes and so on. But that's not needed. It is possible to allow the same features, the same level of abstraction and encapsulation by using multi-methods.
Let's also start with how we want the object to be created. I'd like to use the 'threading' (->
) operator known from Clojure. I find it quite nice, but it is just some syntactic sugar around a let
:
(let ((dungeon (-> (make-dungeon :type 'cellar)
(set-difficulty 'very-difficult)
(add-monsters 15)
(add-special-items 5))))
;; do something with dungeon
)
This first creates a dungeon object of 'cellar
type, then sets difficulty, adds monsters and special-items. Here are two different things at play. make-dungeon
is a simple factory function. set-*
and add-*
functions are generic functions that we use to form a builder protocol. Each returns the dungeon object so that the 'threading' (or piping) can be done:
;; builder protocol
(defgeneric set-difficulty (dungeon difficulty))
(defgeneric add-monsters (dungeon amount))
(defgeneric add-special-items (dungeon amount))
Similarly as the Builders we created in Scala those generic function definitions should be in the same package as the dungeon class and the factory function is. If we want to apply a different set of monsters for different dungeon types we have to do two things. First we need to define sub-classes for those dungeon types. And second, we have to provide different implementation of the add-monsters
builder protocol. Let's have a look at the classes and the factory function:
(defclass dungeon ()
((difficulty :initform 'not-difficult-at-all)
(monsters :initform nil :reader monsters)
(special-items :initform nil :reader special-items)))
(defclass castle-dungeon (dungeon) ())
(defclass cellar-dungeon (dungeon) ())
(defun make-dungeon (&key type)
(make-instance (ecase type
(castle 'castle-dungeon)
(cellar 'cellar-dungeon))))
The specialization of the add-monsters
generic function on the class type does the trick:
;; specialized for 'castle-dungeon
(defmethod add-monsters ((obj castle-dungeon) amount)
(with-slots (monsters) obj
;; set a bunch of nice looking monsters
(setf monsters
(filter-monsters-by-creepy-factor 5 #'< amount *monsters*)))
obj)
;; specialized for 'cellar-dungeon
(defmethod add-monsters ((obj cellar-dungeon) amount)
(with-slots (monsters) obj
;; set a bunch of creepy monsters
(setf monsters
(filter-monsters-by-creepy-factor 5 #'>= amount *monsters*)))
obj)
Common Lisp automatically does a match on the first function parameter for the class type. This is called multi-dispatch or multi-methods. So a different add-monsters
implementation is called depending on whether the dungeon is created with type 'castle
or 'cellar
.
There is otherwise not really a lot more to it. All we did here is use the language features.
Summary
The Builder pattern in many object-oriented languages requires separate builder classes around a class they should create. This is used for abstraction and data encapsulation which would not be easily possible without the Builder.
In Common Lisp dedicated Builder classes are not needed. But dedicated classes are required to allow the multi-methods to do their work. This structure of this can also be recognized as a pattern, but it is simpler.
-
[Polymorphism and Multimethods]
02-03-2023 -
[Global Day of CodeRetreat - recap]
07-11-2022 -
[House automation tooling - Part 4 - Finalized]
01-11-2022 -
[House automation tooling - Part 3 - London-School and Double-Loop]
02-07-2022 -
[Modern Programming]
14-05-2022 -
[House automation tooling - Part 2 - Getting Serial]
21-03-2022 -
[House automation tooling - Part 1 - CL on MacOSX Tiger]
07-03-2022 -
[Common Lisp - Oldie but goldie]
18-12-2021 -
[Functional Programming in (Common) Lisp]
29-05-2021 -
[Patterns - Builder-make our own]
13-03-2021 -
[Patterns - Builder]
24-02-2021 -
[Patterns - Abstract-Factory]
07-02-2021 -
[Lazy-sequences - part 2]
13-01-2021 -
[Lazy-sequences]
07-01-2021 -
[Thoughts about agile software development]
17-11-2020 -
[Test-driven Web application development with Common Lisp]
04-10-2020 -
[Wicket UI in the cluster - the alternative]
09-07-2020 -
[TDD - Mars Rover Kata Outside-in in Common Lisp]
03-05-2020 -
[MVC Web Application with Elixir]
16-02-2020 -
[Creating a HTML domain language in Elixir with macros]
15-02-2020 -
[TDD - Game of Life in Common Lisp]
01-07-2019 -
[TDD - classicist vs. London Style]
27-06-2019 -
[Wicket UI in the cluster - reflection]
10-05-2019 -
[Wicket UI in the Cluster - know how and lessons learned]
29-04-2019 -
[TDD - Mars Rover Kata classicist in Scala]
23-04-2019 -
[Burning your own Amiga ROMs (EPROMs)]
26-01-2019 -
[TDD - Game of Life in Clojure and Emacs]
05-01-2019 -
[TDD - Outside-in with Wicket and Scala-part 2]
24-12-2018 -
[TDD - Outside-in with Wicket and Scala-part 1]
04-12-2018 -
[Floating Point library in m68k Assembler on Amiga]
09-08-2018 -
[Cloning Compact Flash (CF) card for Amiga]
25-12-2017 -
[Writing tests is not the same as writing tests]
08-12-2017 -
[Dependency Injection in Objective-C... sort of]
20-01-2011