PurelyFunctional.tv Newsletter 459: Revisiting the open/closed principle
Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.
Design Idea 💡
Revisiting the open/closed principle
This essay continues the series about Domain Modeling.
I've been thinking about domain model evolution. And that brought me to the open/closed principle. The open/closed principle is the O in SOLID, a collection of software design guidelines widely supported in the OOP community. Once again, I found that Clojure has a perfect answer to the problem the principle tries to solve.
The open/closed principle has always been my least favorite of the SOLID principles. It states that "software entities should be open for extension but closed for modification." But why? What's wrong with modification, and what's so great about extension? The other principles seem self-evident. However, descriptions of open/closed tend not to justify it, only describe it. We need to go deeper to understand it.
According to Wikipedia, Bertrand Meyer invented the ideas of openness and closedness. And he posed them first as a problem.
Let's say a module is open if you can extend its interface. For example, you can add a new public function. Or you could add fields to the maps passed into existing functions.
Let's say a module is closed if used by other modules. Changes to a closed module may affect existing clients.
When Meyer formulated the problem, one of the significant issues was that compilation units were not extensible. If you modified the code of a module and recompiled it, it would not be compatible with existing clients (without them also recompiling). So, the problem was: How can you make a module both open and closed? In other words, how can you safely extend a module without breaking existing clients?
Meyer's solution was classical OO inheritance. You can extend an existing class by creating a subclass, overriding old methods, or adding new ones. Existing clients remain unaffected. The principle makes sense stated this way as a solution to a problem.
However, the problem usually does not apply in modern times. For instance, in Java, what is the harm in adding a new method to a class? No existing client needs to be modified or recompiled. In Clojure, the compilation unit is the function, not the module, and var indirection allows us to recompile them independently. We can ignore that part of the problem.
The other part of the problem remains: how do we trade-off between the need to extend your software and the need to avoid breaking existing clients? In modern OO, the recommendation is to use interfaces as extension points. Instead of subclassing a class, you implement an interface with a new class. The existing classes don't have to change, but you also have a new thing that you can use in place of the old ones.
To avoid modifying existing code, you have to put in speculative extension points. That's just asking for YAGNI interfaces, which I see in commonly Java-style code. Interfaces as extension points in Java are helpful, but exactly how and why is a subject for another essay.
Let's turn, instead, to the Clojure perspective. Rich Hickey presents good ideas in Spec-ulation that address this. He urges us to consider the actual dependencies between modules and what contracts they promise.
For example, we typically think "module A" depends on "module B".
But really, certain functions in "module A" call specific functions in
"module B". As long as the behaviors of those functions in "module
B" don't change, other changes to "module B" should be no concern to
"module A". Further, some changes to those functions would also not
matter to "module A". For example, if a function
b() in release 1.0
promises always to return a long (and makes no other promises about the
b() should be okay restricting the return value to
positive longs in release 2.0, since positive longs still fulfill the
But notice that this clarifies Meyer's original problem: A module needs to change its functionality, yet it must not break its promises to existing clients. We need to catalog what changes are permissible within those constraints. We can call these extensions, since they don't remove or break existing promises. As long as we stick to extensions, our clients should be fine.
Awesome book 📖
What Do You Care What Other People Think? by Richard Feynman are more memoirs about his life. These get a lot more personal than the previous book, but they also include his recollections of the investigation of the Challenger disaster.
Programming Media 🍿
Clojure 1.11.0-alpha4 chat features Alex Miller, Ghadi Shayban, and Michael Fogus talking about the new features in this alpha release of 1.11. If you're into the nitty gritty implementation details, this is great.
Book update 📘
Grokking Simplicity is still selling strong. I love getting messages on Twitter or over email about how it is affecting people's coding. Here's one:
You can order the book on Amazon. Please leave a rating and/or review. Reviews are a primary signal that Amazon uses to promote the book. And they help others learn whether the book is for them.
You can order the print and/or eBook versions on Manning.com (use TSSIMPLICITY for 50% off).
Pandemic 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.
Stay healthy. Wash your hands. Wear a mask. Get vaccinated if you can. Take care of loved ones.
Clojure Challenge 🤔
Last issue's challenge
This week's challenge
Here's an interesting cipher.
- Treat all letters as uppercase, and convert them to uppercase if needed.
- The first alphabetical character of the string will not change.
- All subsequent alphabetical characters are shifted toward Z by the alphabetical position of the preceding alphabetical character.
- Non-alphabetical characters are left as-is.
Your task is to write an encoder and decoder for this cipher
(encode "") ;=> "" (encode "a") ;=> "A" (encode "hello") ;=> "HMQXA" (encode "newsletter") ;=> "NSBPEQYNYW" (encode "1 hug") ;=> "1 HCB" (decode "") ;=> "" (decode "1") ;=> "1" (decode "HMQXA") ;=> "HELLO"
Note that you should always be able to decode a string that was encoded and get back the original string uppercased.
Thanks to this site for the problem idea, where it is rated Very Hard in Java. The problem has been modified.
Please submit your solutions as comments on this gist.