The operations play a vital role in the domain model, and until now, we’ve considered them in isolation. But we rarely do one operation by itself. Instead, we perform sequences of operations all operating on the same data. These operations relate to each other. We can extract quite a lot of information from those relationships to make better design decisions.
Chapter objectives
103
Chapter 4
Composition Lens
Important terms
you’ll learn these terms in this chapter
103
The most interesting interactions involve multiple operations
Do you remember the touchscreen interface the barista used to take an order? Let’s look at a typical interaction the barista will have with a customer.
104
let coffee = newCoffee();
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
coffee = setRoast(coffee, "burnt");
coffee = addAddIn(coffee, "soy");
coffee = addAddIn(coffee, "almond");
coffee = removeAddIn(coffee, "almond");
coffee = removeAddIn(coffee, "almond");
One coffee please!
Sure thing! What size?
Galactic.
No, just mega.
Great. Is charcoal okay?
Burnt, please.
Anything in that coffee?
Can I add soy? And almond?
Yes!
Sorry, no almond.
You got it!
Chapter 4
let coffee = newCoffee();
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
coffee = setRoast(coffee, "burnt");
coffee = addAddIn(coffee, "soy");
coffee = addAddIn(coffee, "almond");
presses “new coffee”
sets size
sets roast
adds soy and almond
removes almond
accidentally touches “remove almond” a second time
sets size again
coffee = removeAddIn(coffee, "almond");
coffee = removeAddIn(coffee, "almond");
Customer
Barista
Touchscreen
Here’s the whole sequence written out:
Operations are rarely used alone. Instead, we use them in sequence and in parallel. A composition is two or more operations performed where the return value of one operations is used as the argument of another.
return value from one is used as an argument for the next
Finding relationships between operations in composition
The prior sequence of operations has some interesting structure baked into it. For instance:
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
The second call to setSize() overrides the first call to setSize(). We can call this last-write-wins.
Here’s another example:
coffee = addAddIn(coffee, "almond");
coffee = removeAddIn(coffee, "almond");
In this case, removeAddIn() reverses the effect of addAddIn(). We would say that removeAddIn() is the inverse of addAddIn().
Here’s a final example:
coffee = removeAddIn(coffee, "almond");
coffee = removeAddIn(coffee, "almond");
In this case, the second call to removeAddIn() has no effect because there is no almond to remove.
These properties of the operations might seem very obvious. But notice that we haven’t even implemented the operations, yet we can talk about their behavior when they work together. These are called algebraic properties and they let us formalize the behavior of operations in composition. By formalizing them, we can easily reason about how the compositions work as the compositions get more complex.
105
Composition Lens
What is an algebraic property?
Let’s take a look at the pair of setSize() calls:
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
The setSize() operation has a common (and obvious) property called “last write wins”. It’s kind of the default, kind of boring, and the only reason I’m putting it in the book is because it’s a good example to introduce the idea of properties.
A property is a statement about an operation that always holds true. An algebraic property is a statement about compositions that always holds true.
One of the first questions I get after I say the term is, “why algebra?”, including groans about boring high-school math class. Let me answer that with an example.
To make sure we get a “last write wins” property, we could write a test:
let coffee = newCoffee();
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
assert(getSize(coffee) === "mega");
Now, this test is very specific. It only tests one unique scenario: Starting from a fresh coffee, setting the size to galactic then to mega will mean the size is mega. We want to test all scenarios, if possible, to show that this property always holds. So let’s generalize it.
First, we can generalize the creation of the coffee:
let coffee = anyCoffee();
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
assert(getSize(coffee) === "mega");
Now we are testing not just fresh coffee values, but any valid coffee value. But we’re not done. We can generalize more.
generalize from “a new coffee” to “any coffee”
anyCoffee()
Return a random, valid coffee.
106
Chapter 4
This is a very general test. It says that for any random, valid coffee, we can set the size to any valid value any number of times, then set it one last time, and no matter what, it will be that last size we set. We can wrap it in a for loop so that it doesn’t test just one scenario. Let’s see that on the next page.
Now let’s generalize the final size we set. Remember, this is the one that the size will be set to in the final coffee (last write wins):
While we’re at it, let’s generalize it even more. Instead of setting just one size, let’s set a sequence of them:
Here’s the code we had on the last page:
let coffee = anyCoffee();
coffee = setSize(coffee, "galactic");
coffee = setSize(coffee, "mega");
assert(getSize(coffee) === "mega");
Now we want to generalize the first size we set. Let’s define a variable to hold that and initialize it to a random size:
let coffee = anyCoffee();
const firstSize = anySize();
coffee = setSize(coffee, firstSize);
coffee = setSize(coffee, "mega");
assert(getSize(coffee) === "mega");
let coffee = anyCoffee();
const firstSize = anySize();
coffee = setSize(coffee, firstSize);
const lastSize = anySize();
coffee = setSize(coffee, lastSize);
assert(getSize(coffee) === lastSize);
let coffee = anyCoffee();const sizes = anyArrayOf(anySize);
coffee = sizes.reduce(setSize, coffee);
const lastSize = anySize();
coffee = setSize(coffee, lastSize);
assert(getSize(coffee) === lastSize);
generalize from “galactic” to “any size”
generalize from “mega” to “any size”
anySize()
Return a random, valid size.
anyArrayOf(f)
Return an array of random length with elements returned from calls to f.
107
Composition Lens
Let’s execute our test one hundred times to test one hundred different scenarios:
for(let i = 0; i < 100; i++) {
let coffee = anyCoffee();
const sizes = anyArrayOf(anySize);
coffee = sizes.reduce(setSize, coffee);
const lastSize = anySize();
coffee = setSize(coffee, lastSize);
assert(getSize(coffee) === lastSize);}
This test gives us reasonable confidence that our property holds. And if we want more confidence, we can loop more.
We can write out this last-write-wins property much more succinctly:
_.isEqual(coffee.setSize(a).setSize(b),
coffee.sizeSize(b))
This is a variation on idempotence, which we’ll see soon.
By generalizing all of the specific, known values to variables (unknown values), we’ve moved into the realm of algebra. Compare it to a property you might have learned in high school algebra class. We all know that:
2 + 5 = 5 + 2
This states only that you can change the order of 5 and 2, but it says nothing about other values we could add with +. To speak about all values, in algebra class we replace the values with variables:
a + b = b + a
This is the same thing we’ve done with the setSize() example. We’ve replaced all specifics with variables (unknown values) and asserted the statement is true. There are too many combinations to think through every scenario. You need to think at a more abstract level. Thinking about algebraic properties instead of specific scenarios brings you to the next level of software design.
this is called the commutative property and we’ll see it again soon
108
Chapter 4
Example properties explained
We saw that setSize(), clickable(), and addAddIn() behave when composed with themselves. These can be written as algebraic properties.
Last-write-wins (a variation on idempotence)
We saw on the last page that setting the coffee’s size has a last-write-wins property, which we can express like this:
_.isEqual(coffee.setSize(a).setSize(b),
coffee.sizeSize(b))
This is a variation on idempotence, which you can read about in the Composition Lens Supplement.
We also need to state the effect of .setSize():
coffee.setSize(b).getSize() === b
Order doesn’t matter (mutual commutativity)
We also saw this equivalence:
_.isEqual(coffee.addAddIn(a).addAddIn(b),
coffee.addAddIn(b).addAddIn(a))
This is classic mutual commutativity, another important algebraic property. We will see these properties and more in the Composition Lens Supplement.
These algebraic properties give us the flexibility to solve the same problem with different expressions. That flexibility comes in handy.
109
Composition Lens
110
Chapter 4
Total/partial property
In the Operation Lens chapter, we learned about total functions. You’ll remember that total functions are functions that give a valid return value for every combination of valid arguments. They have no corner cases, so it is easy to reason about their behavior.
I want to introduce the idea of total properties, which are algebraic properties that hold for all valid combinations of arguments. Most algebraic properties are defined as total. But we can vary them to make them partial.
Let’s imagine the store manager at MegaBuzz says we cannot have more than five total add-ins in a single coffee. After five add-ins, there isn’t room for enough coffee!
How would we solve this in our coffee ordering software? We could modify the set of valid coffees:
function isValidCoffee(coffee) { //=> boolean
...
if(coffee.addIns.length > 5) return false;
...
}
At this point, addAddIn() needs to limit the add-ins or it will return invalid coffees.
function addAddIn(coffee, addIn) { //=> coffee
if(coffee.addIns.length >= 5) return coffee;
...
}
But what does this do to our property? Does this still hold?
_.isEqual(coffee.addAddIn(a).addAddIn(b),
coffee.addAddIn(b).addAddIn(a))
It won’t! If coffee starts with 4 add-ins, coffee.addIn(a) will make it 5, so adding b will do nothing. Similarly if we reverse a and b.
We can modify the property so that it is still true:
coffee.countAddIns() <= 3 ?
_.isEqual(coffee.addAddIn(a).addAddIn(b),
coffee.addAddIn(b).addAddIn(a)) :
true
This is a partial property because it is conditional.
Prefer total properties
We should prefer total properties. They are simpler to understand and let us reason more easily about the behavior of a function.
But if we have a rule that says we can’t have a coffee will more than five add-ins, what do we do? That’s a good question. For now, I’ll simply say: Apply that rule somewhere else. Not only will it make the property simpler, but it has a nice semantic reason.
We are currently working on the coffee orders domain. MegaBuzz, the business, has a rule against 6 add-ins, but we could imagine a competitor setting the number at 7 or even 3. A different competitor might not have a limit at all. The number 5 is rather arbitrary.
Because it is arbitrary, it doesn’t naturally belong in the coffee orders domain. It belongs somewhere in the business rules domain. We will revisit this same problem in the Layers Lens chapter, where we’ll see in more detail how to approach it.
Note that we are making a pragmatic decision (choosing the simpler property) then checking in our domain that it makes sense (we can argue for the add-ins limit to not be part of the domain). We are confirming that our choice fits the domain.
111
Composition Lens
112
Chapter 4
Use property-based testing to test properties
The tests we developed could be called a property-based test (PBT). It uses random values to sample the huge space of possible scenarios. The sample is much better than the handful of examples a human would write. Property-based tests find bugs, often in corner cases we didn’t consider.
There are libraries for developing PBTs in most languages. Teaching how to use those libraries would be a book in itself. We will stick with simple, hand-rolled tests. But I will give some vocabulary to help put some shape to this idea.
Property
Unsurprisingly, the entire test is called a property consisting of generated values and assertions.
Generator
The functions that create random values are called generators. Libraries come with built-in generators for the built-in data types like integers, strings, and collections. You can build custom generators out of the built-in ones.
Size
Generators can create a value of a give size, a relative measure per data type. Numbers are bigger if they are farther from zero. Longer arrays are bigger than shorter ones. Properties are tested from smaller to larger sizes.
Shrinkage
The test may find a big failing case—for instance an array of ten thousand elements. The testing library will shrink the value in tiny steps to the smallest version that still fails. For instance, it will remove one element from the array then test if it still fails. If it does, it will continue until if finds the smallest array that still fails. Typically, this minimal test case is easy to understand. The shrinkage could be drastic, like from 10k elements to 2.
Our hand-rolled tests won’t have size or shrinkage.
Smaller
0, 1, -2
[], [1], [1, 2]
Bigger
10e5, 234321
[3,2,3,5,4,1,2,3,4,5,2]
Built-in generators
anyArrayOf()
Custom generators
anyCoffee()
anySize()
PBT Libraries by language
Google “property-based testing <language>” for other languages.
113
Composition Lens
Super
3 coffees
mega, charcoal,
2 soymilk
mega, charcoal,
2 soymilk
mega, charcoal,
2 soymilk
Total: $12.00
$5.00
$5.00
$5.00
Raw
Soymilk
Espresso
Almond
Mega
Burnt
Galactic
Charcoal
+
+
+
+
+
2
1
0
0
0
-
-
-
-
-
Chocolate
Hazelnut
We just got the UI mockups from our design team. Here is the touchscreen interface for an entering an entire order. Our job is to build it.
This UI has different sections. We can begin to break down the GUI into those sections to understand its structure. We’re going to build this GUI in HTML. We could, of course, build this GUI directly in HTML. But we won’t. Instead, we’re going to use this example as a learning journey to understand how to find deeper structure, model it, and encode it in our language.
HTML
Order GUI
GUIs in General
looking for intermediate structures
We’re looking for an intermediate layer between HTML and the order GUI we are asked to build. This layer is expressed in HTML, with structure that is more specific to our needs. Then we will build this specific GUI out of it.
This may be over-engineering if you were solving the same problem in the real world. But in this book, it will be a useful learning journey, so let’s go!
114
Chapter 4
What is a GUI?
What is a GUI? A question like this focuses our mind and forces us to define the essence of a thing.
There are many answers to the question. And deciding about right and wrong, truth and falsehood, is the work of philosophers. We’re not so interested in that. We are interested in a number of things:
Precision means that our definition can be concisely stated as we mean it. Are we able to state the definition in our formal language?
Clarity means that our definition is unambiguous. It gives us a path forward without kicking the can down the road.
Leverage means it is worth the effort. We want to get more out of it than we put into it. We could hack a GUI together with spaghetti code and brute force. We want a definition that makes building the GUI easier. The leverage we seek comes from structural similarity between the domain and our code.
Finally, the definition must be at the right level of generality. We don’t seek to maximize generality. Instead, we want something that targets the layer we’ve set up. It needs to be built out of HTML and able to build our order GUI and similar GUIs in. Being useful for ALL GUIs is too general. But being useful only for this particular design is too specific.
Keeping those things in mind, try to come up with a definition. It’s a useful exercise. You may not get it right the first try, but it’s worth trying. It will jog your thinking.
Write down your definition before you turn the page. On the next page, you’ll see my definition.
Composition Lens
A model of GUIs
Here is my definition of GUI:
A GUI presents the application’s state and available operations to the user graphically; and it translates the user’s interactions into operations that modify the state.
Here is a picture:
115
State
present
translate
As the state changes, the presenter generates an HTML representation, which the GUI delivers to the browser. The user will tap the touchscreen (the only kind of user interaction we need), which generate events. The translator will interpret those events into mutations, which will be applied to the current state. Then it goes through the cycle again.
There are a number of concepts we need to define, which will form the basis of the architecture of the application.
I will proceed through the definitions of those concepts. Just remember that there is not one right architecture for web UIs. However, this one will be practical for teaching a number of good concepts.
Here are the concepts and their types and signatures:
type HTML = string;
type UIEvent = string;
function translate<T>(uiEvent) //=> mutation<T>
function mutation<T>(T) //=> T
function present<T>(T) //=> HTML
The GUI architecture will handle everything if we pass it present() and translate() functions.
The GUI architecture
Here is the code for the GUI architecture:
let state = {};
let present = (state) => "Hello!";
let translate = (event) => (state) => state;
const container = document.getElementById("gui-container");
container.addEventListener("click", (e) => {
const event = findEvent(e.currentTarget, e.target);
const mutator = translate(event);
state = mutator(state);
container.innerHTML = present(state);
});
function findEvent(top, bottom) {
let event = undefined;
for (let e = bottom; e !== top; e = e.parentElement)
if (e.dataset.event)
event = e.dataset.event;
return event;
}
container.innerHTML = present(state);
<html>
<body>
<div id="gui-container"></div>
<script src="/gui.js">
</body>
</html>
This is a barebones GUI framework, but it will do the job. The first three lines define the behavior of our specific GUI, the rest of the lines are the machinery to make click events work.
Because it is an HTML GUI, we need to define the HTML:
116
Chapter 4
As it is, this GUI will simply display the text “Hello!”. We’ll modify the three variables at the top (state, present, translate) iteratively to define the MegaBuzz order GUI. I suggest you create these two files and view the HTML file with a browser to test it.
Q&A
Why are we defining our own GUI framework? Can’t we just use React? Or Vue?
It’s a good question. I’m defining the GUI framework because I don’t want this book to be about React or Vue. I don’t even want it to be about HTML, but I had to pick something.
The main idea behind runnable specifications is that we can run them. We needed some kind of system that would actually run. If you’re familiar with React or Vue or any other way of building GUI’s, please feel free to use one of those.
However, be careful. When using a practical framework, it is easy to slip into implementation mode, which we are trying to avoid. This simple framework exactly models our GUI model. It has exactly three degrees of freedom (initial state, presenter, and translator). And each of them has very well-defined inputs and outputs. These constraints help us stay in specification mode.
117
Composition Lens
118
A counting GUI
Before we get complicated, let’s understand how this GUI system works by creating a simple counter. There will be a number, and every time you click it, it increments by 1.
0
1
2
. . .
Chapter 4
click
click
click
The first thing is to define the initial state.
let state = {count: 0};
Then we implement the presenter. We’ll return a div containing the number. The div will have a data-event attribute that will be picked up by findEvent().
let present = function (state) { //=> HTML
return `<div data-event="increment">${state.count}</div>`;
};
Finally, we implement the translator, which takes an event and returns a mutation function to be applied to the state. We’ll define it in a functional way, using a modified copy.
let translate = function (event) { //=> function (state) => state
if(event === "increment")
return function (state) {
return Object.assign({}, state, {count: state.count + 1});
};
return state => state;
};
Give the GUI a try. Click the number and it should increment each time.
Displaying the state
As I’m developing the GUI, there are times when I want to know the state as raw as possible. There could be a bug in the presenter, and I want to see the raw state and the HTML from the presenter. A simple way to display the state is to output it as JSON at the bottom of the GUI.
let present = function (state) { //=> HTML
return `<div data-event="increment">${state.count}</div>` +
stateViewer(state);
};
function stateViewer(state) { //=> HTML
return `<pre><code>${JSON.stringify(state, undefined, 2)}</code></pre>`;
}
I also want to see the last event I triggered to make sure things are working as I expect. I’ll add that to the translate function.
let translate = function (event) { //=> function (state) => state
if(event === "increment")
return function (state) {
return Object.assign({}, state, {count: state.count + 1,
lastEvent: event});
};
return state => Object.assign({}, state, {lastEvent: event});
};
The GUI should look like this:
0
1
119
Composition Lens
2
. . .
click
click
click
{
count: 0
}
{
count: 1,
lastEvent: "increment"
}
{
count: 2,
lastEvent: "increment"
}
I hope this simple example shows that we have indeed encoded our abstraction of the GUI and that it gives us the flexibility we need to build our screen. If that’s still not obvious, it will become so as we continue.
120
Building the basic elements
We’ve got our basic GUI framework in place. We have a very simple GUI that displays the state and accepts user input to change the state. But our GUI looks nothing like what we need it to.
We could build our Order GUI directly out of what we already have: The GUI Framework and HTML. The GUI Framework defines the types of the presenter and the translator. HTML defines how it will display in the browser.
However, the number of structural concepts in our GUI is quite limited and repeated. You might already be able to see that. But the structural concepts of HTML are abundant. We will want to take advantage of that and add a GUI Elements layer between the Order GUI and the GUI Framework.
Those GUI elements define more specific concepts that are less applicable to all apps and more applicable to our app. The reduction in generality is one of the reasons it can give us leverage. But only if we find concepts that help us build the GUI we want.
Chapter 4
HTML
HTML
GUI Framework
GUI Framework
GUI Elements
Order GUI
Order GUI
Organic and Synthetic approaches
There are two main approaches to encoding a domain. We will refer to them as “organic” and “synthetic”. In the organic approach, you begin by coding the Order GUI with what you already have. As you recognize regularities in the code, you begin to factor those out into reusable parts. Those parts form a lower layer.
The synthetic approach takes the opposite approach. You first identify regularities in the domain, then encode those as concepts in your model in the layer. Then you build the Order GUI out of those. Both approaches have merit. Both have negatives.
The organic approach is good for exploring a domain where the structure is not apparent to you. By diving in and coding, you are able to make forward progress. And you often see the structure later as you are coding. We commonly discover structure due to repeating patterns. But we can also find it by encountering difficulties. Things that are harder than they should be probably mean you’ve got the structure wrong.
The synthetic approach doesn’t start with the code. It starts with an analysis of the domain. You look for repeated patterns and important structures. You develop an understanding of those, then you encode it in your programming language. The synthetic approach is good when the domain is well-understood (such as accounting) and you have a way to explore it, such as access to an expert (an accountant).
The synthetic approach is often misunderstood. The caricature of it is that a programmer, with little understanding of a domain, on their own begins dreaming up abstractions, then coding those. But this is not how it works. It takes disciplined study and skilled analysis before coding happens.
The truth is that we code in both modes alternatively. Both domain analysis and exploratory coding are great sources of information. This is why we model in code and seek rich feedback. We might analyze the domain, encode what we discover, then find that there was something we missed, which we explore organically.
put feedback here
121
Composition Lens
122
Discovering the structure of our GUI
Super
3 coffees
mega, charcoal,
2 soymilk
mega, charcoal,
2 soymilk
mega, charcoal,
2 soymilk
Total: $12.00
$5.00
$5.00
$5.00
Raw
Soymilk
Espresso
Almond
Mega
Burnt
Galactic
Charcoal
+
+
+
+
+
2
1
0
0
0
-
-
-
-
-
Chocolate
Chapter 4
Hazelnut
Let’s take a close look at our GUI and uncover the structures within. I’ll mark up the image and discuss each structural pattern.
We see several repeated elements and patterns:
With these six components, we can build this GUI. Let’s get to work on the next page.
vertical layout
horizontal layout
border
image
text
frame
Composition Lens
123
What is a component?
The first thing we need to decide is, “What is a component?” We are building at a certain layer in the stack. We are building on top of the GUI framework, which only gives us three things to define:
HTML
GUI Framework
GUI Elements
Order GUI
these pieces are given to us by the GUI Framework
We will these components within the GUI Elements layer:
Then we can build our GUI using those in the Order GUI layer. But that brings us to a tough quesion: What is a component? Let’s try this definition on for size: A component takes some part of the state, renders it to HTML, and can be composed with other components into a presenter. It will have this type signature:
function component<T>(T) //=> HTML
Notice that that is the same type signature as presenter. This is actually a very good thing because it means that renering a partial state is no different from rendering the full state. We can visualize each component separately using the same framework. And if we get the composition right, those will also build up into presenters, so we can see partial GUIs using the same framework.
124
Chapter 4
Complex operations first
We know what components we need, but where do we start? We could start with the simple ones, like text, because then we can knock them out before we go to the harder ones. But I have found through experiences that it is always better to start with the complex ones.
When we define the complex operations, we immediately face the most difficult problems. Solving those problems gives us a lot of useful information about how the pieces work together. And that information usually results in us having to rework something. Maybe the function needs a new argument, or perhaps the return type needs to be augmented.
In any case, any code we’ve written will have to be revisited at the least and rewritten at the worst. If we’ve written the simple ones first, all of that work could be scrapped.
Another way to look at it is that the complex operations have more interesting properties. By defining them and exploring the properties, the complex operations reveal design constraints that help us make design decisions in other parts of the code. We have more information. And so then we can make better decisions elsewhere.
Contrast this with what I was taught to do, which was to create a class, add some instance variables, then immediately tackle the easiest methods: the getters and setters. By the time I got to the more complex operations, I had a much better idea of what instance variables I should even have. I would either go back and replace the wrong instance variables or leave them, creating a mess. Complex operations are harder, but they contain more information.
The most complex operations are typically the combination operations. They take two or more things that are typically already complex and combine them into an even more complex thing. In our case, the two combination operations are the horizontal and vertical layout functions. These will take two or more things and lay them out next to each other in the corresponding direction. Let’s get to those first.
Composition Lens
Horizontal and vertical layout
The horizontal layout simply means two or more components will be put next to each other horizontally, like so:
This means we know some of the arguments to the constructor function:
function horizontal(components) //=> ??
We will pass an array of components (which have the type signature of a presenter) as the argument. What should the return value be? I suggest it should be a presenter as well. That way, we can present what horizontal() produces.
function horizontal(components) //=> presenter
Here our notation is failing a little, so I’m going to stop calling them components and use the same name for both. The second name is not helping.
function horizontal(presenters) //=> presenter
The function horizontal() will take an array of presenters, will package them up , and will return a presenter. This is an important property and we’ll explore it in detail shortly.
Vertical layout will look very similar:
function vertical(presenters) //=> presenter
There is one more thing that we need to add, which is that both of these presenters could be clickable. We need to provide an optional event name:
function horizontal(presenters, event) //=> presenter
function vertical(presenters, event) //=> presenter
125
The other presenters
126
Chapter 4
Writing the function signatures for the other presenters should be pretty straightforward. We will continue to write them from the most complex to the least complex.
Frame and border are a kind of mutation function. They take a presenter and draw either a dark box (frame) or a light, rounded box (border) around it.
function frame(presenter) //=> presenter
function border(presenter) //=> presenter
Text and image are simple constructor functions for presenters. They also need to be clickable, so we pass an event.
function text(string, event) //=> presenter
function image(url, event) //=> presenter
With all of the function signatures written, we should look for some that can be defined in terms of the other. I don’t think we have any. Each is rather primitive. Therefore, it’s time to move onto implementation.
Again, we want to implement the more complex ones first, which are the horizontal and vertical layout presenters. These operations are combining operations. Here is my implementation:
The returned presenter renders all of the children, then wraps them in a flex-box div, which will lay out the children horizontally. We can write a similar function for the vertical layout:
Implementation of horizontal and vertical layouts
function horizontal(presenters, event) { //=> presenter
return function(state) { //=> HTML
let children = presenters.map((p) => p(state));
return `<div ${event?`data-event="${event}"`:""}
style="display: flex;
flex-direction: row;
align-items: flex-end;"
>
${children.join("")}
</div>`;
};
}
function vertical(presenters, event) { //=> presenter
return function(state) { //=> HTML
let children = presenters.map((p) => p(state));
return `<div ${event?`data-event="${event}"`:""}
style="display: flex;
flex-direction: column;
align-items: center;"
>
${children.join("")}
</div>`;
};
}
127
Composition Lens
128
Chapter 4
Implementation of frame and border
The next two operations to work on are the mutation function, frame and border.
function frame(presenter) //=> presenter
function border(presenter) //=> presenter
They each take a presenter and return a presenter. Frame adds a dark rectangle around the presenter. And border adds a light rounded rectangle around the presenter. Let’s start with frame.
function frame(presenter) { //=> presenter
return function(state) { //=> HTML
return `<div style="border: 1px solid black;
padding: 1em;"
>${presenter(state)}
</div>`;
};
}
They each take a presenter and return a presenter. Frame adds a dark rectangle around the presenter. And border adds a light rounded rectangle around the presenter. Let’s start with frame.
function border(presenter) { //=> presenter
return function(state) { //=> HTML
return `<div style="border: 1px solid grey;
border-radius: 1em;"
>${presenter(state)}
</div>`;
};
}
Implementation of text and image
The final two operations, text and image, are constructors. They take a configuration and return a presenter.
function text(string, event) //=> presenter
function image(url, event) //=> presenter
Text displays a small bit of text. It’s quite simple:
function text(string, event) { //=> presenter
return function(state) { //=> HTML
return `<div ${event?`data-event="${event}"`:""}
style="text-align: center;"
>
${string}
</div>`;
};
}
Image simply displays an image, given its URL.
function image(url, event) { //=> presenter
return function(state) { //=> HTML
return `<img src="${url}"
alt=""
${event?`data-event="${event}"`:""}
>`;
};
}
These two constructors are our simplest operations. And now we’re done with a first, synthetic pass. Remember, the synthetic approach analyzes the domain for repeated patterns, abstracts them, then encodes them into code.
Now that we’ve extracted what we can, we should put it to the test with an organic pass. In the organic approach, we build something with the pieces we have, then look for repeated patterns in the code, abstract them, and encode them. Let’s build our GUI using what we’ve got.
129
Composition Lens
130
Encoding the size buttons incrementally
We’re going to encode the size buttons a small piece at a time. Here is the ideal design with its parts labeled with the concepts we’ve developed.
Chapter 4
Super
Mega
Galactic
image
text
border
vertical
horizontal
I want to simulate on these pages what it is like when you can run code quickly to visualize the code as it evolves. It might seem like a waste of space, but don’t worry, there’s always more space.
I’m going to put the execution of the code (the image it produces) at the top and the code that produced it at the bottom. Each page will add a little bit more until we’ve completed the whole part. This will simulate what it is like to have your code auto reloaded as you modify it. For instance, I’ve often programmed with my code editor on the left of my screen and a web browser on the right. As I change the code, the browser automatically shows the result of running the code (a web page).
As we develop the code specification of our GUI, we can run it and see it, and therefore evaluate it against the design.
Let’s get started on the next page.
Encoding the size buttons incrementally (continued)
let presenter = image(
"https://mega.buzz/super.png"
);
let presenter = vertical([
image("https://mega.buzz/super.png"),
text("Super")
], "size—super");
let presenter = horizontal([
vertical([
image("https://mega.buzz/super.png"),
text("Super")
], "size—super"),
vertical([
image("https://mega.buzz/mega.png"),
text("Mega")
], "size—mega"),
vertical([
image("https://mega.buzz/galactic.png"),
text("Galactic")
], "size—galactic")
]);
We start with just a tiny piece, the image of the super size.
Then we add the label below it and make it generate an event.
Now we make all three sizes, laid out horizontally.
We have our three sizes. There are a couple of things missing. One, we don’t have an event listener that can handle these events. Two, we don’t have the current size highlighted. But before we get to those, I feel a little disconnected from the state. We are generating events, but I can’t see them. Let’s fix that.
Browser
Browser
Browser
Editor
Editor
Editor
Super
Super
Mega
Galactic
131
Composition Lens
Encoding the size buttons incrementally (continued)
We are generating events, yet nothing is happening on the screen. This is disconcerting. I never want to go too long without seeing the result of changes. We want to know that the changes we make to code are having the effect we want so that we maintain fast feedback.
There’s a simple solution in web programming: We display the state as JSON in the GUI as part of the presenter. Here’s my take on it:
Editor
let stateViewer = function(state) { //=> HTML
return `<pre><code>${
JSON.stringify(state, null, 2)
}</code></pre>`;
}
let coffeePresenter = horizontal([
vertical([
image("https://mega.buzz/super.png"),
text("Super")
], "size—super"),
vertical([
image("https://mega.buzz/mega.png"),
text("Mega")
], "size—mega"),
vertical([
image("https://mega.buzz/galactic.png"),
text("Galactic")
], "size—galactic")
]);
let presenter = vertical([
coffeePresenter,
stateViewer
]);
Now we can see the state.
{}
Browser
132
Chapter 4
Super
Mega
Galactic
Encoding the size buttons incrementally (continued)
let state = {
coffee: {
size: "super",
roast: "burnt",
addIns: {}
}
};
let translator = function(event) { //=> mutator
return function(state) { //=> state
return Object.assign({}, state, {
lastEvent: event
});
};
}
Now we can work on the state and translator. Let’s add an entire coffee, with default size and roast:
Before we write the entire translator, we can write a simple translator that records the last event to the state. That way, we can begin seeing the effect sooner. Yay feedback!
Now when we click on a coffee, we can see some effect! We can test all three coffees to make sure the right event is being sent.
We now have a complete loop in our architecture. Changes to the state are reflected on our GUI. It’s now time to wire up the coffee state change to the events.
Browser
Editor
{
coffee: {
size: "super",
roast: "burnt",
addIns: {}
}
}
{
coffee: {
size: “super”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—mega”
}
Super
Mega
Galactic
Super
Mega
Galactic
Browser
Editor
133
Composition Lens
134
Encoding the size buttons incrementally (continued)
We’ve got our basic translator adding the last event to the state so we can see it. That let us check that our events were working properly. Now we we handle each of the events. Here is my take on the translator.
Editor
let translator = function(event) { //=> mutator
let [attribute, ...args] = event.split("—");
let mutator = function(state) { return state; }
if(attribute === "size")
mutator = function(state) { //=> state
return Object.assign({}, state, {
coffee: setSize(state.coffee, args[0])
});
};
return function(state) { //=> state
return Object.assign({}, mutator(state), {lastEvent: event});
};
};
{
coffee: {
size: “super”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—super”
}
{
coffee: {
size: “mega”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—mega”
}
{
coffee: {
size: “galactic”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—galactic”
}
Super
Mega
Galactic
Super
Mega
Galactic
Chapter 4
Super
Mega
Galactic
Browser
Browser
Browser
Composition Lens
Encoding the size buttons incrementally (continued)
It’s now time to add the border around the current size. We will need at least one conditional to to implement it, yet we don’t have any components that make conditionals. Let’s build a highlight component that contains that conditional.
function highlight(presenter, predicate) { //=> presenter
let highlighted = border(presenter);
let unhighlighted = presenter;
return function(state) { //=> HTML
if(predicate(state))
return highlighted(state);
else
return unhighlighted(state);
};
}
highlight() takes a presenter and a predicate (a function of the state). We make a highlighted version of the predicate by wrapping the presenter in a border. The unhighlighted one is just the presenter by itself. Inside of the returned presenter, we call the predicate on the current state. If it returns true, we call the highlighted version, otherwise we call the unhighlighted version.
We can use it like this:
let coffeePresenter = highlight(
vertical([
image("https://mega.buzz/super.png"),
text("Super")
], "size—super"),
(state) => state.coffee.size === "super"
);
135
Super
Super
136
Encoding the size buttons incrementally (continued)
Here is the the code for all three size buttons.
Editor
let coffeePresenter = horizontal([
highlight(
vertical([image("https://mega.buzz/super.png"), text("Super")],
"size—super"),
(state) => state.coffee.size === "super"
),
highlight(
vertical([image("https://mega.buzz/mega.png"), text("Mega")],
"size—mega"),
(state) => state.coffee.size === "mega"
),
highlight(
vertical([image("https://mega.buzz/galactic.png"), text("Galactic")],
"size—galactic"),
(state) => state.coffee.size === "galactic"
)
]);
{
coffee: {
size: “super”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—super”
}
{
coffee: {
size: “mega”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—mega”
}
{
coffee: {
size: “galactic”,
roast: “burnt”,
addIns: {}
},
lastEvent: “size—galactic”
}
Super
Mega
Galactic
Super
Mega
Galactic
Chapter 4
Super
Mega
Galactic
Browser
Browser
Browser
Finding repeated code patterns
It is no surprise that the code for the three size buttons is very similar. We could use the organic approach to encode that repeated structure into the model. Let’s start with an obvious pattern: the labeled image.
Editor
let coffeePresenter = horizontal([
highlight(
vertical([image("https://mega.buzz/super.png"), text("Super")], "size—super"),
(state) => state.coffee.size === "super"
),
highlight(
vertical([image("https://mega.buzz/mega.png"), text("Mega")], "size—mega"),
(state) => state.coffee.size === "mega"
),
highlight(
vertical([image("https://mega.buzz/galactic.png"), text("Galactic")],
"size—galactic"),
(state) => state.coffee.size === "galactic"
)
]);
Composition Lens
Super
Mega
Galactic
Browser
137
Design
Raw
Burnt
Charcoal
Espresso
Hazelnut
labeled images appear frequently in the design
It’s a good idea to double-check with the domain that it is a valid concept. In this case, it is repeated so much, we know we have found something good.
Labeled image
The goal of the organic approach is to find new domain structures by looking at patterns in the code. We found a common pattern, we verified it in the domain (the GUI design), so let’s encode it.
Note: This is a definition, not an implementation. That’s a great sign that we’ve discovered important ideas in the domain.
Let’s encode our coffeePresenter again using this new function.
let coffeePresenter = horizontal([
highlight(
labeledImage("https://mega.buzz/super.png", "Super", "size—super),
(state) => state.coffee.size === "super"
),
highlight(
labeledImage("https://mega.buzz/mega.png", "Mega", "size—mega"),
(state) => state.coffee.size === "mega"
),
highlight(
labeledImage("https://mega.buzz/galactic.png", "Galactic", "size—galactic"),
(state) => state.coffee.size === "galactic"
)
]);
138
Chapter 4
function labeledImage(URL, label, event) {
return vertical([
image(URL),
text(label)
], event);
}
Q&A
There’s a lot more repetition, including the call to highlight(). Why don’t we include that in our new function?
That’s a good question. We certainly have the choice. If we were trying to eliminate all repeated structure, that would certainly be the way to go. But that is not our goal. Our goal is to find important structure in the domain by using repeated code as a clue.
When we look at the design, we see that the labeled image (an image with related text below it) is repeated even when it is not highlighted. So the labeled image, without highlighting, is important.
Another way to look at it is that our goal is to find the smallest structure we can assign a meaningful name to. A meaningful name is an important sign that it is part of the domain.
But also, don’t worry. There’s more refactoring to do. The organic approach will yield more structure.
139
Composition Lens
140
Chapter 4
Padding
One thing that is bothering me is that there is not enough padding around the labeled image. The highlight is much too close.
I propose that we need a padding component that wraps another component in whitespace. Here we are not looking at repeated code (organic approach). What is happening is we are comparing the encoding (as it results in a GUI) to the domain and deciding we need to go back to the synthetic approach to find a new domain concept.
Here’s a first crack at it:
function pad(presenter) { //=> presenter
return function(state) { //=> HTML
return `<div style="padding:10px">${presenter(state)}</div>`;
}
}
Now we can wrap the labeled image in padding under the highlight:
let superPresenter = highlight(
pad(
labeledImage(
"https://mega.buzz/super.png",
"Super",
"size—super"
)
),
(state) => state.coffee.size === "super"
);
Super
Mega
Galactic
Browser
Design
Super
Mega
Galactic
Super
141
Mega
Border jumping
There’s one more thing bothering me with it that is hard to explain. But I’ll try. When I click on the different sizes, things jump around a tiny bit. Maybe one or two pixels. It doesn’t block the functionality, but it’s visually disturbing.
What’s happening is that the 1-pixel border around the labeled image is appearing and disappearing, changing the position of the elements within. A solution is to add an invisible 1-pixel border instead of no border in the highlight.
function whiteBorder(presenter) { //=> presenter
return function(state) { //=> HTML
return `<div style="border: 1px solid white;
border-radius: 1em;"
>${presenter(state)}
</div>`;
};
}
function highlight(presenter, predicate) { //=> presenter
let highlighted = border(presenter);
let unhighlighted = whiteBorder(presenter);
return function(state) { //=> HTML
if(predicate(state))
return highlighted(state);
else
return unhighlighted(state);
};
}
And this fixes it! We are moving fast by being able to visualize our changes quickly.
Composition Lens
Super
Mega
Galactic
Super
Mega
Galactic
142
Chapter 4
Refactoring click events
I’d like to do one more refactoring before we summarize what’s going on. The final thing I see is that we’re duplicating our event code in every element. If an element should be clickable, we pass it an event name, which sets a data-event attribute on it. We can be more targeted and make the clickability another thing we can wrap our components with.
Here is our existing code:
function image(url, event) { //=> presenter
return function(state) { //=> HTML
return `<img src="${url}"
alt=""
${event?`data-event="${event}"`:""}
>`;
};
}
function text(string, event) { //=> presenter
return function(state) { //=> HTML
return `<div ${event?`data-event="${event}"`:""}
style="text-align: center;"
>
${string}
</div>`;
};
}
We can extract this into a new component:
function clickable(presenter, event) { //=> presenter
return function(state) { //=> HTML
return `<div data-event="${event}">
${presenter(state)}
</div>`;
};
}
I like that it has gotten rid of the conditional. Event should always exist.
Using clickable()
Now that we have clickable, we can remove the event arguments and logic from text(), image(), vertical(), horizontal(), and labeledImage(). Here is what the coffee presenter now looks like using clickable().
let coffeePresenter = horizontal([
clickable(
highlight(
labeledImage("https://mega.buzz/super.png", "Super"),
(state) => state.coffee.size === "super"
),
"size—super" ),
clickable(
highlight(
labeledImage("https://mega.buzz/mega.png", "Mega"),
(state) => state.coffee.size === "mega"
),
"size—mega"
),
clickable(
highlight(
labeledImage("https://mega.buzz/galactic.png", "Galactic"),
(state) => state.coffee.size === "galactic"
),
"size—galactic"
)
]);
I’ll leave the rest of the refactoring (removing events from individual components) to you. There’s plenty more repeated structure that we can encode, but I’ll leave that to you as well.
Now I want to talk about what has made all of this possible.
143
Composition Lens
Fit, layers, and closure: The secret to expressivity
144
Chapter 4
We built a set of GUI components that work well together. They allow us to build up larger interfaces from small parts. However, this chapter is not about the GUI. It’s about the deeper principles that allow the GUI to be expressed.
There are three things important aspects to point out:
These three things combined give us a lot of leverage over writing everything directly in HTML. Ultimately, HTML is all that is rendered. But the intermediate structures we discovered means we can:
Let’s see how each aspect helps us develop leverage.
Concepts with high fit
We used the synthetic and organic approaches to make sure the concepts in our code corresponded to concepts in the domain. In the synthetic approach, we directly analyzed the domain for important structures, then encoded them. In the organic approach, we found repeated structure in our code, looked for a corresponding structure in the domain, then decided it was important.
In both cases, we wanted the fit between the structure in the domain and the structure in the code to be as similar as possible. That is, to have a high fit. Similarity of structure means they have the same properties. For example, the design of the GUI (the domain) had size buttons arranged horizontally. We created a combination function (the encoding) that lays out components horizontally. These two structures are in entirely different media, yet they share structure.
Stratified Design
Stratified design means we build the encoding in layers. Each layer uses what’s in the layers below it to define new meaning to be used by the layers above it. I discuss stratified design at length in Grokking Simplicity, chapters 8 and 9.
Stratified design gives you leverage because it each layer gives you a place to encode the important structures from your domain. Each of those structures gives you leverage by naming a repeated structure for reuse. As long as you are encoding domain concepts with high fit, each layer gives you a much richer language for defining your GUI.
Closure property
The closure property is a simple idea from mathematics with a lot of power. Put simply, an operation has the closure property when its arguments are of the same type as the return value. All of our mutation and combination functions have the closure property.
function horizontal(presenters) //=> presenter
function vertical(presenters) //=> presenter
function clickable(presenter, event) //=> presenter
function pad(presenter) //=> presenter
function border(presenter) //=> presenter
The closure property is powerful because it allows for arbitrary nesting. horizontal() can take presenters created in any way, including by hand-coding a function of the right type, or by composing a presenter from other presenters. horizontal() does not care how you make the presenter.
What’s more, horizontal() returns a presenter, so it can be used as an argument to any of the other functions. We can nest to arbitrary depth, in arbitrary ways. Once we are in the land of presenters, there are an infinite number of things we can do with that presenter.
Compare this to what it would have looked like if we had made the functions return different types. What if we had organized them into primitive components, modified components, and layout components, each with their own types.
145
Composition Lens
146
Chapter 4
Would we have a horizontal() function that worked on primitive components and another that worked on modified components? Could we modify a layout component? These are problems we would have to solve. But it shows how we would not get as much leverage out of our encoding.
Now, it turns out that our GUI components are so expressive, we can say the same thing in different ways. This could be a problem. Remember in our add-ins example, these two arrays represent the same thing:
["soy", "almond"]
["almond", "soy"]
We had to solve this problem with a normalization function so that we could make them the same in order to compare them.
However, now we’re not talking about two equivalent pieces of data. We’re talking about two operations in composition, like so:
coffee.addAddIn("soy").addAddIn("almond")
coffee.addAddIn("almond").addAddIn("soy")
We want to say that these two are equivalent. What’s more, we want to allow these two expressions because the customer might tell us what add-ins she wants in arbitrary order. So the barista must be able to click the buttons in whatever order he wishes.
Similarly, there are things we can express in our GUI components layer that don’t make perfect sense, yet they do work. For instance, these two are equivalent:
clickable(clickable(presenter, "never-fires"), "my-event")
clickable(presenter, "my-event")
If I wrap a clickable with another clickable, the last wrap wins because of the way we’ve defined bubbling of events. It doesn’t make sense to write the first line (it has the same effect as the second line), yet we must allow it in order to make the functions total.
clickable(clickable(presenter, a), b)
clickable(presenter, b)
We can’t use _.isEqual() or any other code to express they are the same. They don’t generate the same presenters, and the presenters don’t even generate the same HTML. But the effect of a click is the same and their visual rendering is the same. Therefore, we say they are semantically equivalent.
Conclusion
The composition lens gives us the tools to reason about how operations work in composition—while still separating specification from implementation. Algebraic properties allow us to formalize the behavior of operations while referring only to the function signatures. We also learned how to expand the expressivity of a model by leveraging the closure property. Finally, we saw how we can write tests for our algebraic properties to ensure that they hold.
Summary
Up next . . .
We’ve learned how to reason about a sequence of operations. Those sequences often happen because of user interactions over time. As domain models mature, they typically begin to explicitly model time. In the next lens, we’ll see two ways to do that. But be sure to read through the Composition Lens Supplement, which contains many common and powerful algebraic properties.
147
Composition Lens