Design: What is simplicity?
There is no such thing as universal characteristics for simplicity: some think this is related to size (the number of code lines — but you can write too complex things on a few lines — or the number of concepts involved — but reducing that number mechanically increases the complexity of the remaining concepts) while others believe it is related to the nature of concepts themselves (privileging a programming paradigm over another).
Actually simplicity is relative to both the task to do and the people who does it. As software development is most often a team effort however, it is important to grasp what are the most shareable traits of simplicity in software development.
The goals of simplicity
Maybe asking the question of the benefits of simplicity can help to grasp its definition. Why are we looking for simplicity? Obviously to make our work easier. We want:
- productivity: to write quickly so you can deliver ASAP ;
- maintainability: to be able to understand (readability) and change quickly so that you can debug/fix or evolve.
As you can see, a common value behind simplicity of development is speed of development.
As you may have spotted as well, those goals seem at odds with each other: writing quickly often results in poor maintainability (difficulty to read/understand, difficulty to change) and, conversely, being able to fix or evolve code quickly often requires careful writing.
In most professional environments you will be pressured to deliver quickly and so to prioritize goal 1 over goal 2. This would a short-term, non-rentable choice however, as:
- a good maintainability warrants a good delivery pace ;
- by speeding up you may be thinking “too fast” and have a higher risk of introducing bugs ;
- we read code more frequently than we write it (often once) so you should not give more importance to something you do only 20% of the time.
Because of that, maintainability should be our primary goal, and productivity shall emerge from that because your design will be kept agile (i.e. change-friendly).
How to make things simple?
Global simplicity is only reachable for simple apps. Most applications provide value because they handle complex problems, so we have to find out how to design maintainable solutions to complex problems.
One renowned way to simplify complex things is to split it as a set of more simple things. Each time your app is refactored to use a component with a single responsibility, local simplicity is achieved. That component can then both solve its simple problem and be maintained easily.
When to split?
What is the proper granularity between too complex and too simple? A couple of principles can help deciding:
- the SRP principle states that as soon as you handle more than 1 concern, you should split your code. Conversely, if you want a component to not do something, you should split it too with another component responsible for the optional part.
- the cohesion of your class, that is, the propensity of a class’s method to use most of the class’ attributes, is also a big clue about the need for split. If some methods are only using a sub-set of the class’ attributes, they probably should better fit in a separate class.
Some people might be concerned by the idea that splitting adds layers and that communications between those layers might be costly in terms of:
- development time: you have to implement APIs between those layers ;
- performance, especially if the layers are remote, like in micro-services architectures.
So, following some YAGNI mantra, one may object that : “You won’t need to change that part”. However, this is missing the point of why we want to split in parts.
The reason for splitting has never been because we thought we’ll need to “change that part”. That is a remote possibility, but the actual reason for splitting is to separate concerns in the first place. “What if I needed to change that part ?” is just a fictional scenario that makes you design each part so that it is more maintainable and more testable (by limiting each part dependencies, not testing multiple things at the same time, etc.).
So what we’re talking about here is a logical split that have no more performance implications than a function call compared to inline code. There is no physical split implied here and, if so, it should be discussed in a separate architectural discussion.
Finally, one might then argue that the sole fact of insulating code in distinct functions or object classes is a development overhead that you could avoid. But this is looking only at the dark side of the coin, thus forgetting the bright side of a better maintainability. The gain of an increased maintainability is far beyond the cost of a function or object call. As Robert C. Martin stated in his reference book Clean Code:
Do you want your tools organized into toolboxes with many small drawers each containing well-defined and well-labeled components? Or do you want a few drawers that you just toss everything into?
Only simple programs can achieve global simplicity. The majority of others can and should only target local simplicity, that is, simplicity of the code part you’re maintaining at a given time. This can only be achieved by proper code split in simple abstractions according to the SRP principle.