October 6, 2022
We can think of these as a general guide. There are many more, here are the ones I come up often, they are five and go by the acronym of S.O.L.I.D.
The good thing about SOLID is that helps you decouple/breaking apart your code. Giving our modules independence of each other and make our code more maintainable, scalable, reusable and testable.
A quick disclaimer: In the world of OOP sometimes is easy to fall into an obscure path of developing an hierarchy of deeply nested classes that makes it even worse (looking at you J**a & M$ C# devs) . So take the next points as advice or ideas, suggestion, not a mandatory thing.
We should aim to break our code down into modules of one responsibility each.
For example we have a big class that's performing unrelated jobs:
We can now identify what the class does, test it more cleanly, and we can reuse parts of it anywhere without having to worry about irrelevant methods!
We design our modules to add new functionality in the future without having to actually makes change to them. Instead we should extend a module to add to it, wrapping it, or something else.
In practice, we could create an object/structure lets say named 'Controller', and than if we want to create new derived classes like 'AdminController'.
This goes both ways, we should add new functionality by creating new derived classes and leave the original class implementation as it is, and also create the base class in a way it can be extended and used.
We should only extend modules when we are absolutely sure they are the still the same type as heart.
In this simple example, do you see what's wrong? I will be weird for Bicycle to extend Vehicle because doesn't really have an engine or a fuel type. (and yeah, assuming is a classic bicycle )
In short: Extend things that makes sense to extend, don't do it just to grab some kind of functionality/behavior from it. If hat's the case, it should extend something else that fits it's design... or become it's own type.
Our modules shouldn't need to know about functionality they don't use, we should split our module into smaller abstractions, like interfaces... and then we can compose it to form the exact set of functionality the module requires.
This becomes very useful in Testing, as it allows us to mock only the functionality that each module needs.
Rather than creating large interfaces, create multiple smaller interfaces to allow clients to focus on the methods that are relevant to them.
In the example or HP Printer module extends IPrinter, but our printer can't scan at all; so that breaks this principle. Instead we should have something like:
No class should be forced to implement any interface method(s) that it does not use.
Its says that our modules instead of talking directly to other modules inside your code, we should always communicate abstractly, usually via the interfaces we define.
This isolates our modules completely from one another, meaning we can swap parts as we need to. I'd say this is more important on lower level modules.
Because they communicate with interfaces now, our modules don't need to know what type of 'implementation' they are getting. Only that they take certain inputs and return a valid output.
Looking at the example, we can see that instead of communicating directly to GooglePay from our modules, we go thru our interfaces and that allow us to fully decouple our higher level code, from the lower level code (The specific implementations of it), we could add new or swap to different ones without touching the rest of our application.
I'm a 32-years old programmer and web developer currently living in Montevideo, Uruguay. I been programming since I was about 15 y.o, and for the past 12 actively working in the area.