Our main objective as developers should be to keep complexity low, this wasn’t entirely clear to me until I started working on bigger projects. I liked the idea of following SOLID principles to produce cleaner and more readable code but didn’t realize I should have been striving for ways to reduce complexity as much as possible. Which is precisely the reason why SOLID came to be.
Why is complexity the enemy?
We have finite mental resources, there is a limit to how much information we can hold in our minds at one time. If we exceed this limit we have to stop and go back, trying to break the problem down into smaller pieces. Software can get really big, really fast, and it’s our job to understand it. That’s what we get paid for. If anyone could understand code right away then there wouldn’t be much use for software developers. If the solution to understanding a problem is breaking it down into smaller parts, why not write the solution in small pieces right from the start?
SOLID principles help us do that, they are a guide to keeping the parts of our solution simple and as de-coupled from one another as possible. This enables us to focus on a small part at a time without having to think about the rest. To help us think this way we can use abstractions. An abstraction is a representation of something. Why use abstractions instead of the object directly? To reduce complexity. It’s easier to think about a car instead of thinking of a collection of wheels, doors, an engine, hydraulics, cooling systems, etc. In this case, the car is an abstraction of all its parts. You know what a car does and how it can help you accomplish what you want, go for food for example. SOLID principles help you keep your abstractions simple, coherent and de-coupled from one another so that you have an easier job working with them.
S – Single Responsibility Principle
Whats easier to think about, a lamp that has a switch to produce light or a lamp that has a clock with a timer to switch on and of at a preset time to produce light? Do you feel how concepts start to stack up in your mind as you think about them? The same thing happens when you have a class, or a method for that matter, that does more than one thing. Instead of being able to focus your mind on just one concept you have to start to juggle your attention between different ones, the abstraction has lost its pure meaning. Going back to our lamp example, if you have a lamp that works with a timer you might be tempted to abstract it away as a whole concept, but in practice, it would be better if you used two abstractions, one for the lamp and one for the timer. Then when you have to maintain the lamp it’s only the lamp you are thinking about, and if you think of a better way to implement a timer in the future, you don’t have to worry about light bulbs.
O – Open Closed Principle
This one is more subtle, the abstraction should maintain it’s meaning but we still should be able to modify it in a way that minimizes errors and re-work.
When a class is coherent and works as intended, and has been tested as such, changing it represents work and risk. On one hand, you have to think what impact the change you are making will have on the clients of the class, and on the other hand, you will have to alter unit tests and re-run them. Having the risk that some unintended side-effect can break part of your code.
A way to minimize these is to leave the working code ‘as-is’ and extend it to incorporate the new functionality, either by composition or inheritance.
If you think this way from the start then the overhead is minimum when a change is required and the risk of breaking something is minimized too.
L – Liskov Substitution Principle
Remember inheritance right? A child object ‘inherits’ some properties from his parent. The idea behind the Liskov principle is that the child object should always respect the idea that a child ‘is-a’ parent in some other form. This allows us to work with class children interchangeably, reducing the complexity of working with them. If you didn’t maintain this ‘is-a’ relationship then a child could grab some of its parent’s meaning and change it to mean something else. When this happens you cannot think of the children of a class as a whole anymore, requiring to check what each of them means. This forces you to keep more concepts in your mind, growing complexity.
For example, if the parent has a functionality to get its color that can be overridden by its children, it makes no sense for a child to use it to represent its shape. If you do that then you could not call getColor on any object of type parent and expect to get its color. Sometimes you will get its color and sometimes its shape, depending on which child you are asking.
I – Interface Segregation Principle
This principle reinforces the idea of keeping the integrity of the abstraction. If you start to implement methods from an interface that you don’t need, with only a ‘throw not implemented exception’, then that abstraction begins to corrupt. A lamp is no longer a lamp, but a lamp with some dead things attached to it. Thus if you have an interface that contains methods that can be used independently of one another you should break it up. This way you can use only the parts that make sense together and leave the unnecessary out.
D – Dependency Inversion Principle
So the lamp needs a timer, there are many ways to go around joining this two together. The worst would be to create a timer inside the lamp and use it, this binds the two permanently and once more you are forced to think of the two when you want to work on any one of them. A good rule of thumb is, if you are using and creating an object in the same place then there is something fishy going on with the abstraction. A better way to solve the problem is to abstract the timer behind a contract that states what the timer can do and then indicate the lamp has a dependency on it. This way you could ‘inject’ the timer into the lamp and keep the parts isolated from one another. The lamp only knows that the timer can be on or off at a given time and work with that. If tomorrow you want to rewrite the timer completely, as long as the contract stays the same, the lamp has nothing to worry about.