PurelyFunctional.tv Newsletter 412: use and abuse of the decorator pattern
Design Tip 💡
use and abuse of the decorator pattern
In the book Head First Design Patterns, the authors present the Gang of Four design patterns in a fun, educational style. One of the patterns they present is the Decorator Pattern. The decorator pattern is simple: wrap an object with another object to modify its behavior. The wrapper object (called the decorator) has the same interface as the original object, so in theory it is usable wherever the original was.
It's a fine pattern. It's used in the
java.io.InputStream library to
good effect. It's what allows us to separate the source of a stream
(such as a
FileInputStream) from the buffering of that stream
BufferedInputStream). They're all
InputStreams, but their behaviors
can be modified by wrapping.
The pattern is fine, but I have to take issue with the example the book uses. I take issue not to disrespect the book (which is great) but to motivate the application of idea of design affordances on the design of code.
In Head First Design Patterns, the authors apply the decorator pattern
to the problem of modeling a cup of coffee from a coffee chain called
Starbuzz. The original design is to have a ton of classes like
HalfCaffCappuccinoWithSoymilk. There are lots
of possible beverages at Starbuzz, so there are many classes. Obviously,
this is a bad design. Their solution is to apply the decorator pattern.
You start with a class like
Cappuccino. Then you
decorate them with options like
SoyMilk. In the book,
these modify the description and cost behaviors. Because all of the
classes extend the
Beverage interface, you've always got a beverage
and you always have an object you can use.
This seemingly solves the problem of exponential increase in the number of classes. You can compose a drink from its components instead of writing out each combination as a class. However, I think it also causes another problem: The number of distinct beverages you can encode is much higher than the number of beverages that exist in the real world. In other words, the affordance of composition using decorators gives us more freedom than we want.
Here's an example: What is the difference between a Mocha Soy House blend and a Soy Mocha House blend? Depending on the order you compose the modifiers, you get different object graphs, yet the beverage they represent in the real world is the same. The book sweeps this under the rug by saying that you can control this problem with other design patterns like factory and builder, or even to use another decorator to modify the behavior again to normalize the behavior. These kinds of pattern gymnastics have always struck me as problematic. But even more problematic is that there is rarely a discussion of the mismatch between what is possible to encode and what should be encoded.
This problem does not exist in the
java.io.InputStream example. No
matter how you decorate your
InputStream, the behavior is different.
For example, the library has
FilterInputStream, both decorators. Buffering after you filter is
different from filtering after you buffer. However, adding chocolate
before the soy milk is the same as adding it after. The decorator
pattern encodes an explicit order. This order may or may not exist in
the underlying system it is modeling. When it exists, the decorator
pattern works well. When it doesn't, now you have two problems.
The discussion of the properties of an encoding system and how they correspond to the underlying system is missing not only from the Head First book, but from my experience, the entire enterprise of design patterns in general. Instead, the discussion is about the code. In the example of Starbuzz, it's about managing the huge number of classes that a naive approach might require. But the chapter completely forgets about the underlying beverages it is modeling. I find this amnesia pervades the design pattern movement. I am not the first to say that design patterns are about solving problems in code, not building correct models. The patterns are useful but the movement mistakes style for substance.
I think bringing in the idea of design affordances can help to ground the discussion. The problems with the designs of shower knobs, stove knobs, and door knobs can help us talk about the problems with software design. Shower knobs are notoriously poorly designed. A user wants to control the temperature and pressure of the water. But they are rarely given that control. Instead, they are given two knobs, one for hot water pressure, the other for cold water pressure. Or they are given one knob that simultaneously controls heat and pressure. These are the kinds of problems we deal with in software. I want to have an unambiguous way to encode a coffee with extras and what I get is an exponential explosion in ambiguity.
So what are the affordances that are desired in designing coffee shop software? We'll get into that in the next issue.
A new book 📖
Yehonathan Sharvit's new book is out in early access. It's called Data Oriented Programming. Yehonathan is the creator of Klipse and is a great member of the Clojure community. He's also a friend. His book is meant to bring data orientation, which we enjoy in the Clojure world, to a wider audience.
No one has described exactly what data orientation is as well as I've seen Yehonathan do. His talk at re:Clojure really impressed me with how far he has gotten in showing the advantages data orientation can provide. This book has the potential for explaining what we do as Clojurists to the rest of the industry as well as to ourselves. Go buy it and support this effort.
Quarantine update 😷
I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.
I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.
Also, if you just want to subscribe for a paid membership, I have opened them back up for the moment. Register here.
Stay healthy. Wash your hands. Wear a mask. Take care of loved ones.
Clojure Challenge 🤔
Last issue's challenge
Please do participate in the discussion at the submission links above. It's active and it's a great way to get comments on your code.
This week's challenge
A grid of 1s and 0s shows the location of land and water. A 1 represents a square full of land, a 0 represents a square full of water. Your job is to calculate the perimeter of the land, both as it touches water and touches the edge.
A grid with a single square filled with land has a perimeter of 4, since there are four sides:
(perimeter []) ;=> 4
Likewise, a single square filled with water has a perimeter of 0:
(perimeter []) ;=> 0
Two squares of land next to each other share an edge, which reduces the perimeter:
(perimeter [[1 1]]) ;=> 6
The edge of the grid is like an implicit encircling of water:
(perimeter [[1 1] [1 1]]) ;=> 8 (perimeter [[0 0 0 0] [0 1 1 0] [0 1 1 0] [0 0 0 0]]) ;=> 8 (same!)
Here are some other weird shapes:
(perimeter [[1 1 1 1 1 1] [1 0 0 0 0 1] [1 0 1 1 0 1] [1 0 0 0 0 1] [1 1 1 1 1 1]]) ;=> 42 (preimeter [[0 1 0 0] [1 1 1 0] [0 1 0 0] [1 1 0 0]]) ;=> 16
Please submit your design process as comments to this gist. Discussion is welcome.