So I was “forced” to review them, especially because I couldn’t remember the benefits of two principles.
And we had a nice discussion mainly about them.
Besides, I also don’t feel that the materials we have been through relate how or why the subject helps us to write cleaner code.
From now on, this text will refer to methods, classes, packages, etc as a “unit”.
It has the subtle meaning that SOLID (and clean code) should be designed into all code levels.
Let’s start by looking at this unclean code (it uses a lot of branching and has variable names that are hard to decipher), to be refactored later.
It's not impossible to understand it, but it's unnecessarily hard to do so:Off course, in a real codebase, you have more clues to understand what a piece of cryptical code meansWhat is clean code?Code embraces entropy.
It becomes messier and messier as time goes by.
It’s the developer’s job to adopt a set of good practices to keep the codebase maintainable.
Clean code is easy to be read by humans because it uses good names for the units and their variables.
It also has less complexity i.
has fewer conditionals, because our brains are limited to deal with complex scenarios (especially with big systems).
Here’s the same code without cryptic names and with more pruned branches.
Now, at least, it is readableWhat SOLID stands for?SOLID is an acronym for 5 principles, or guidelines, that help you to write better code.
Each letter is important as the other but they can be identified and practiced separately.
S stands for “single responsibility principle” [SRP], meaning that a unit should have one reason and only one reason to change.
Code which complies to SRP tends to be cleaner because this principle asks us to specialize a unit to do only one job very well, so in case of changes or fixes, we’re accurate on where to look first.
Small, specialized units:Hide fewer bugs, as there’s less to test on them, their testability increasesHave less code to be read and thus understanding them is easierAre much simpler to compose, and thus good candidates to DRY the codebaseSometimes it’s hard to spot the unit’s responsibility, for instance, a method that knows how to select something AND uses that something could be broken into two methods, like the example below:Now it's easy to see which method is responsible for what.
If we need to create a new kind of damage, remove an existing one or even tweak it, the main unit doesn't have a reason to change.
As you can see, an unit becomes smaller but for that you have to create auxiliary units.
There’s nothing wrong with having more of those at a first moment and in most scenarios — if you're doing web is pretty rare that dividing your main method in smaller ones will impact performance.
O stands for “open-closed principle” [OCP], a fancy name just to say that it should be possible to change how a unit behaves even without modifying its code.
There are a few ways to do so, like reading external files to either configure or add functionalities to your software, changing the base unit of the code (when using inheritance) or complying with other SOLID principles ????If we follow the example from SRP, the method that selects that something could rely on an external list of items so the method itself doesn’t change even if we need to add new items or remove ones no longer used.
The unit which creates damage now simply declares what it does: it selects an item and then creates it.
How this happen was delegated for each kind of damage (SRP + OCP) so it doesn't concert the function itself.
As there were few kinds of damage on the example, it might be hard to see that OCP reduced code complexity i.
it eliminated if-else blocks, and/or potential switches.
The best way to write good code is not write any codeI feel like this example got over-complicated, sorry if it was the case but keep reading, there's a reason why explained on the last letter of SOLID — so keep OCP in mind!L stands for “Liskov’s substitution principle” [LSP], it states that a program should stay correct when you change an instance for another, given that the replacement is a subtype of the original instance.
Formally: an instance of type T should always be replaceable by an instance of type S given that S is a subtype of T.
The classical example of LSP is that even though in maths a square is a kind of rectangle, in a program it might not be the case because changing a square’s height means changing its weight too.
So, when programming, squares might not be rectanglesSo far as I know, LSP is the only principle you can safeguard with automated tests.
Detecting it manually is a hard task.
LSP reduces coupling, but to be honest, I couldn't think of how it makes your code cleaner besides that.
A more realistic example is when the base class has an attribute which is set on the subclass and it becomes a blocker to realize operations on the base classI stands for “interface segregation principle” [ISP], meaning that you have to design your interfaces specifically to their different clients.
You achieve ISP by designing interfaces which give your clients only what they need, instead of giving them a general-purpose interface.
Clients might be web pages, mobile, other systems or other developers.
So having broad interfaces for API integrations might still comply to ISP.
It’s easy to violate this principle, especially because we tend to think that clients would benefit from being able to use our APIs in all its glory.
For the outside world, we now state clearly that there are at least three ways for a character to interact with its target: hit, cast a spell or attack them.
We control HOW it happens because exposing the raw method would allow even invalid usages of it (like passing a null attack).
Units complying to ISP state much more clearly what they were designed to do.
D stands for “dependency inversion principle” [DIP], that is, instead of relying on the code implementation, you should rely on its abstraction.
For languages which you have explicit interfaces, like C#, this is fairly easy.
But DIP can also be used on more dynamic languages.
NPC, Player, and Enemy are characters too, so they can be used interchangeably when functions expect Character.
Otherwise, a function dedicated to each class would have to be created — and, as it would be internal functions, they do not count as ISPYou can use constructors, parameters, and reflection to do do that inversion, and with that, your unit is decoupled of concrete units giving you the flexibility easily test stuff.
An example of how relying on the arguments make lie easier: writing tests can override the default DAMAGES constantI couldn’t think about how DIP helps us clean code though.
ConclusionI hope that this perspective which tried to tie SOLID to how it cleans code was nice, and honestly, not much of a stretch on the subjects.
Both of them help you to write code that’s readable (for humans) and maintainable.
Nice to have them in mind when designing units.
Help me with some criticism and spreading this text by clapping and/or sharing a few times.
.. More details