S.O.L.I.D Principles is also known as the first five object-oriented design(OOD) principles, to improve software maintainability and extensibility. Introduced by Uncle Bob (Robert C. Martin), those principles are indispensible for my daily DevOps life.
The acronyms stands for:
- S - Single responsibility principle
- O - Open-closed principle
- L - Liskov substitution principle
- I - Interface segregation principle
- D - Dependency inversion principle
Recently I’m getting more actual practices on those principles with real life projects (via the heavy usage of interface), rather than just reading them on textbooks.
Single responsibility principle
A class should have one and only one reason to change, meaning that a class should have only one job to do.
CalculateFinalTotal() is taking up extra responsibility (calculate discount) than it should, we extract the responsibility and assign it to
But hang on, this leads to the violation of next principle.
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
If we add a new
premium in our customer database,
GetDiscount() will have to be modified to add another
GetDiscount() is clearly against the O, and by doing so we have to ensure all the previous conditions are still working as intended (extra testing workload). This is where interface comes to our rescue.
And now we should get something like this:
This way we won’t have to modify
CalculateDiscount() each time when a new
Customer type is added. Instead we just implement (extending not modifying) a new
IDiscount and feed that into our
OrderCalculator will return a different result even though its logic is unchanged. In this case, three memberships return different discount rates.
Liskov substitution principle
Let be a property provable about objects of type . Then should be true for objects of type where is a subtype of .
Liskov substitution principle is a bit trickier to understand. In short, the derived class or child class should not break the behaviours of its parent class.
Now we want to encourage customers to upgrade their membership status by introducing a promotion discount.
We add method
Premium customers have reached the top tier of our membership system, we do not want to offer them this incentive, so we do not implement
The polymorphic behaviour of interface allows us to do the following:
PremiumDiscount doesn’t have the concrete implementation of
AddPromotionDiscount(), this code snippet is syntactically correct but will still throw an exception during runtime. How do we avoid this? Let’s look at the next principle I.
Interface segregation principle
No client should be forced to depend on methods it does not use.
This is extremely important for DevOps as every time we ship out a new feature, we try to avoid modifying the existing ones.
Continue from the L, we should again segregate
IDiscount with their functions:
Now we let
PremiumDiscount to implement
IMembershipDiscount only, the rest should have implemented both
A graphical illustration of what’s going on here:
graph LR; Basic(BasicDiscount) Pro(ProDiscount) Premium(PremiumDiscount) Membership("CalculateDiscount()") Promotion("AddPromotionDiscount()") subgraph IMembershipDiscount subgraph IPromotionDiscount Basic Pro end Premium end subgraph Funtions Membership Promotion end Basic-->Membership; Pro-->Membership; Premium-->Membership; Basic-->Promotion; Pro-->Promotion;
And now your IDE should happily point out your syntax error, since
PremiumDiscount has never implemented
IPromotionDiscount thus has no
AddPromotionDiscount() is an useless method for
PremiumDiscount, therefore we shouldn’t force it onto
PremiumDiscount. By segregating
IDiscount into two interfaces, we conform to the I. This structure now also satisfies the L.
Dependency inversion principle
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
As the project grows larger and larger, it will become extremely difficult to maintain if we don’t decouple them properly, a small change in one base class could lead to a massive overhaul of its derived classes. We should make sure our business logics only depend on abstractions.
One example is the use of database. We often run into the need of switching database (like from MySQL to PostgreSQL), it could have been a nightmare but we followed the D and used interface for our database logic.
We have clearly defined the methods needed for our database operation, and switching to another database will almost certainly change the query syntax. If that happens, we simply implement another
Since everything is based on the abstraction, it ensures that we don’t rely on a single provider of service (like SMS and OAuth) and have the ability to switch between providers quickly without interrupting other parts of the application. This to a certain extent also conforms to the S.
Because the flexibility interface provides, we use interface heavily during development to comply with S.O.L.I.D principles.
Another beauty of interface is that, they are nothing but a bunch of method definitions, there is no concrete implementation but they provide a set of clear guidelines on input/output. The test team is able to test the application before we even finished developing our logic.
Following S.O.L.I.D we make our code a lot easier to understand, maintain, expand, test etc. For DevOps this means dramatically reduction off development time needed yet we deliver better product quality.