For a long time now I’ve been looking for, and not finding, good free training material that takes clean code (or clean architecture) and combines it with the SOLID principles in a good, practical set of reference steps. And for a while now I’ve been wanting to compile such a set of reference steps perhaps not as blog posts, but rather as a structured approach that follow the actual day-to-day, distilled into the crucial bits, that can be used as course material for self-study. Then, this quick thread happened:
Can you please suggest some design material links to help a junior self taught to understand it better?.. Please 😊— Smiley #INFIEARTH (@MbueloRamafamba) January 26, 2019
And then this quick thread:
I find that there are too much of these gotchas, niches and "ooh latest!" blog posts, and very very little really basic stuff. Underlying fundamental basic stuff.— Henk Roux (@helloserve) January 30, 2019
So I was inspired. But what will this series cover?
At the time of writing I need to revamp my personal website, and I have a few ideas that I want to spin into software as services as part of the new build to perhaps generate some alternate income from. This material will cover that build-process, focusing on the following pillars:
- Clean Code
- SOLID principles
- Test Driven Development
- Domain Driven Design
Throughout these posts I will introduce concepts that apply to the process in general. The first of these concepts are the different levels.
In all aspects of software development, there are different levels. Architecture isn’t just one document, and design doesn’t happen in just one place. There are high-level and system level overviews, infrastructure architecture, tech-stack and solution level implementation considerations as well as low-level integrations and exchanges. All of these things combine, so you have to design on all of these levels. To illustrate in a more physical medium, there’s no point in an architect drawing out only the roof of a new building, without considering pillars and other load-bearing walls.
As you move up and down the hierarchy of levels, you have to identify all the required entities and constructs to sufficiently complete that level. You have to make decisions on each level on the dependencies within the level, and you will certainly at some point separate different parts of the level, and focus on each of these independently.
And the most important thing to realise is that this applies just as much to the junior developer facing a single user story as it does to the solutions architect facing an enterprise requirement. The only way to build good API is to design good API. On every level.
Throughout these steps, I will always start at the highest level and get the essentials correct. Then move down a level (perhaps single out a specific area or break it up into smaller pieces), and move down all the way to a sufficiently enough level of detail to see how all of that will bubble back up to the top to support the high-level overviews, assumptions and requirements.
Perhaps you’ve heard of “analysis paralysis”? Diving down a rabbit hole of design iterations is dangerous if you don’t have an exit strategy. That strategy is typically different levels of abstractions. You have to learn to identify those levels of abstractions, and places or areas to rely on abstractions. In clean code literature, this is referred to as “deferred decisions”.
When facing even a single user story (not to say an entire requirements document), you might already have at least two places of abstraction to exit on, which is presentation and persistence. You can split your design at these abstractions and focus on the different parts at different times. The SOLID principles make identifying these places of abstractions pretty easy, and so you can time-box design sessions in a meaningful way and show progress quicker and more reliably.
But what is an abstraction? In its simplest form it’s a definition that promises or guarantees specific behavior. We’ll shortly see many examples of this.
At the highest level of my domain for my own website and services, I have the following requirements:
- Display project
- Author project
- Display blog posts (optionally related to a project)
- Author blog posts (optionally related to a project)
- Publish blog posts
- Take in an order (a part, assembly or URL)
- Process an order
- Update an order
- Notify about an order
- Complete an order
- Show an assembly
- Drill down into assembly (show sub-assembly)
- Drill down to individual part (show part)
- Add new assemblies and parts
- Link parts as substitutions (similar)
- Maintain assembly and part details
Don’t worry about what these different concepts are (although blog and order are pretty straight forward). This is just the basic, high-level requirements.
Lets define the main parts of the domain, and establish a hierarchy of domain entities and concepts.
- Drill down
Do you see the differences here? I’ve identified the main entities (or nouns) that appeared in all the texts, and then extracted all the interactions (or verbs) per entity. Another difference?
I changed some of the language. Author, Take and Add became Create and Maintain became Update and Delete, and Display became Read. But why did I choose to change the language? Regularly, a user has a different name for something than what we as programmers typically call it. They don’t update anything, they edit. And they add instead of create. This duality in language is always a problem, and it is a skill in itself to fluently converse with a client in their terminology, and convey that same message to your team in typical or common technical terminology. Practise it when you design. So, we have clear CRUD (Create, Read, Update and Delete) functionality on all the entities. The names now match the acronym. But there are also some interactions that didn’t change. Notify, Publish, Complete and Drill Down. These might mean something special (we don’t know yet), and it will be the job of the requirements gatherer (a business analyst or systems analyst) to define these terms using clear, unambiguous language for the designers and developers.
Plate Tectonics - a simile
On this highest level we already have well-defined separations. These are the main continents of your domain, and their interactions and abstractions now need to be defined. As we delve deeper into this design, some of these splits might move a bit, become more clear or result in smaller earthquakes that show us cracks in our design
A good communication tool is a diagram. Things like architectures are communicated this way. Let’s draw a domain diagram that you would typically draw in a whiteboard session:
What are those arrows though? If we glance back at the original high-level requirements, there is one bit of language there that we haven't considered yet. It appears that a blog post may be related to a project. This shows us there is a dependency here, but which way? The language defining dependencies are often ambiguous and unclear. A clue here is that the reference to a relation appears on the text about the blog, and not on the project. Thus it seems that the blog will be dependent on the project.
Similarly, an order can be for a part or an assembly (and a URL, but it is not a central concept here). The reference to the relation, again, appears on the order which gives us a clue. These relationships are included in the diagram already.
Dependencies as abstractions
Now that we have identified the dependencies, how do we go about catering for it in the main system architecture? Before we get into that detail, consider the following: do we always want to hog along references to Project whenever we talk, code and test against the blog entity? Will it help us in any way to always have to include
in all our classes and code files for a blog? To do this is Accidental Complexity. It’s not wrong by any means, but we can probably do better. And that would involve using abstractions. Let’s consider an interface called
IBlogOwner. A blog post carries around a reference (or property that points) to this interface. The Project entity can then implement the interface (or fulfill the guarantee of the abstraction). So what’s happened here?
SOLID: D for Dependency Inversion
We have inverted the perceived dependency that a blog post has on a project. This is apparent in more than one way:
- A blog post now simply deals with its owner through a well-defined API that is this interface. It is now only dependant on an abstraction within its own namespace.
- We can now test all the blog post code in isolation, and provide stub or mock implementations of an owner where required.
- This is our “analysis paralysis” exit strategy around designing the blog post entity. We don’t have to consider anything beyond this abstraction.
- Any entity (not just Project) that wants to be an owner of a blog can now define themselves as being an owner, e.g. they implement that interface. This means these entities are strongly typed as owners, and it is clear to anyone reading the code that they are owners, or to see through tools like “Find all references” all the possible types of owners.
- The Blog entity doesn’t have to be maintained when a new entity is added that can be an owner. This means that changes to requirements later on carries less of an impact on the code base.
So how would we deal with the dependencies between a part or assembly to the order? You can bike-shed for hours now already whether Assembly should inherit from Part (perhaps it’s just a collection of Parts, right?) which means every Part is also a collection of Parts. Or, you can exit before all that by introducing an abstraction called
IOrderableItem. The Order will have a list of these interfaces as it’s items, and that’s all we care about. We have now deferred the decision on how to deal with Assembly and Part by inverting the dependencies here too.
But what if we created instead an interface called
IProject. Would that be the same? You could certainly employ it in the same way as explained above, but then later, if I wanted to write a blog post about a specific part, the Part entity would have to implement
IProject. That is pretty poor, since it will lead to very confusing code (a part is also a project?). It will make it difficult to maintain and for new team members to understand. Naming stuff in your code has to be aligned with your design. It is absolutely critical to be as explicit and literal as you can be, and have good foresight and common sense when naming things.
Update the diagram
Now that we’ve got our relationships defined, lets update the diagram:
For the moment, we’ll stop here on this level. We have covered a lot of ground already, and unpacking Part and Assembly will probably result in too much redundant reading at the moment.
The domain overview level already has a lot of detail now, but never make the mistake of designing into too much detail upfront. By using dependency inversion we avoided an overly complicated discussion and created well-defined sub-domains within our overall domain. The broad strokes design at first feels superfluous, but it is crucial for clear understanding and for keeping complexity in check.
When your base class serves as a common implementation (as opposed to a common data model) you might sometimes want to force an implementation or behavior, but also allow the behavior to be extended without it being modified or omitted completely. What do I mean by this?
Here’s a question for you: how quickly do you think it is best to fail in your code? And at what level do you think it should be handled? My answers to those two questions are immediately, and at no level. Let me explain.