PurelyFunctional.tv Newsletter 414: Constrain your design with composition first
Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.
Design Tip 💡
Constrain your design with composition first
Last week, we talked about choosing affordances from use cases to help us constrain and inform the design of our model. But not all affordances are created equal. Some affordances are more useful for constraining the model. We should start with those, because they will make the process easier.
It will help to look at one of the least constraining affordances, namely, a simple data accessor. For this example, let's look at a contact data representation, like in an address book. And for our data accessor, we need to get the user's full name. This accessor doesn't constrain the design very much. It still gives us lots of freedom for how to represent the contact. In fact, it only tells us that we have to have the full name, or the data for constructing the full name, somewhere in the data.
To be really explicit, here are some ways we could implement the accessor. Each way gives us different constraints for how to store data:
(get contact :fullname)direct getter
(get-in contact [:personal-information :name :fullname])nested getter
(str (:first-name contact) " " (:last-name contact))calculated from other fields
(:fullname (fetch-contact db (:id contact)))even stored in the database
There are certainly more ways. Because it has so many possibilities, it's not that useful for constraining the design. We should implement this accessor, and others like it, later.
On the other end of the spectrum, composition operators constrain the problem the most. Composition operators take two or more entities and return a composed entity. In my experience, these types of operators help constrain and inform the design.
Here's an example:
Synchronizing contacts across devices is a feature we will want to have. If I edit a contact on my phone, but also edit it on my laptop, when they connect to the network, the contacts should eventually become consistent. We can look at it pairwise, like this:
(sync contactv1 contactv2) ;; contactv3
This operation takes two versions of the contact that have diverged and returns a third, merged version that will replace them.
How do we implement this operation? Well, it's not obvious. We can start to elaborate different scenarios, like if you modify different fields, take the newer versions. If you modify the same fields, then you need to make a tough decision. Maybe you take the more recent. Or you somehow keep both.
However you choose to define the operation, notice that it's starting to add more concepts to the domain. Before, we had contacts with fields and values. Now we have modifications to fields that store their modification times. These concepts have to be reified into the model. That's what I mean that the operation, the affordance, can inform the model.
This particular affordance also constrains the data model at a lower level than the accessor model. Synching forces us to ask questions like "How do we store edits to a contact so they can resolve later?" Do we store a contact as a sequence of edits? Or do we store only the last edit, with the value it replaced, the new value, and the time? We would have to further define the desired behavior, but if we do, we would probably find that a very small number of models would even be possible, and one is clearly better.
The full name accessor would still be easily implemented. It even seems,
like a trivial problem now.
Design is an iterative process. Usually the stuff we build is too complex to do it all at once. It would be preferable to look at all affordances and synthesize a design with all of them in mind. But we cannot. We have to proceed more or less linearly. That's why we need to start somewhere.
Here's my recommended order for defining affordances. They start with the most complex, because those constrain and inform the most, to the least complex:
- Composition - take two like entities and form a third
- Modification - take one entity and return a copy of it modified
- Translation - take one entity and return a different kind of entity
- Accessors - take on entity and answer a simple query
By starting with the most complex, we get the hard problems out of the way. We want to ensure that the more complex operations are even possible before we optimize the simple operations for ease.
Awesome book 📖
The Art of Doing Science and Engineering by Richard Hamming
Richard Hamming distilled the lessons of years of work at Bell Research Labs into this book, trying to show how mindset and perspective can dramatically increase your chances of working on interesting problems and finding significant results.
I read this book over several months since I found it very dense and challenging in a good way. There was a lot to absorb and think about. It is definitely worth a second reading.
You may also be interested in Hamming's Turing Award Lecture.
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
- Minimum steps to palindrome - submissions
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
Decimal to binary
Write a function that takes a string and replaces all occurrences of numbers with their decimal form.
(d->b "I have 2 arms.") ;=> "I have 10 arms." (d->b "That costs 3 dollars. But I only have 1 dollar.") ;=> "That costs 11 dollars. But I only have 1 dollar." (d->b "I was born in 1981.") ;=> "I was born in 11110111101."
Any contiguous string of digits is a number. That means that spaces, periods, and commas separate numbers instead of indicating differnt parts. For instance, 2,000 is the number 2 followed by the number 000.
Please submit your design process as comments to this gist. Discussion is welcome.