From my point of view, the consolidation and internalization of concepts that promote a significant improvement in the quality of software is a “must” for any programmer, so I always take that into account when I am developing software. Having that in mind, in this post I will write about SOLID principles.
SOLID is an acronym that represents the 5 basic principles of object-oriented programming and design. These principles, when applied together, will make it more likely that a programmer will create a system that is easy to maintain and extend over time.
[S]ingle Responsability Principle
[O]pen/Close Principle
[L]iskov Substitution Principle
[I]nterface Segregation Principle
[D]ependency Inversion Principle
There are several articles on the internet that talk about SOLID principles but in this post I will resume and explain each one in a clear way with some code examples of the violation of the principle and its correction as well as examples of real life cases (real life analogies).
Single Responsability Principle
“A class should have one and only one reason to change”
This principle is nothing more than a different perspective for the most fundamental principles of object-orientation: cohesion.
A class with more than one reason to change has more than one responsibility, that is, it is not cohesive. We have several problems with this:
- Difficulty of understanding and, therefore, difficulty of maintenance.
- Difficulty of reuse. With responsibilities intertwined in the same class, it can be difficult to change one of these responsibilities without compromising others (rigidity) and may end up breaking other parts of the software (fragility).
- High coupling, that is, the class has an excessive number of dependencies, and therefore is more subject to changes due to changes in other classes (again the fragility).
On the next code snippet I will demonstrate an example of a violation of this principle:
public class Order{ public void ProcessOrder() { // Process request code } public void SendEmail() { // Send email code } }
What happens in this example is an explicit breach of the principle since the Request class is responsible for sending e-mail. The sending of emails should be specified in a class responsible for it, in a mail class. Therefore:
public class Request{ public void ProcessRequest() { // Process request code } } public class Email{ public void SendEmail() { // Send email code } }
Now every class has its responsibility and the code complies with the specifications shown above.
This principle applies in our daily life because each entity has a responsibility and this is how society works. For example, when going to a bakery, it is expected that bread or cakes will be sold, so in a real-life case it would be incorrect for an individual to go to a bakery to buy a television or schedule a trip to French Polynesia 🙂
Open/Closed Principle
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”
Software is evolutionary, rarely is software made once and will never be modified again. So where does this principle try to get?
- Extensibility
It’s one of the keys to object orientation, when new behavior or functionality needs to be added, existing ones are expected to be extended and unchanged, so the original code remains intact and reliable while new ones are implemented through extensibility. Creating extensible code is a mature developer’s responsibility, using durable design for good quality software and maintainability.
- Abstraction
When we learn about object orientation we certainly hear about abstraction. Abstraction is the way to realize this principle and allows it to work. If a software has well-defined abstractions then it will be open for extension. Derivatives from an abstraction are closed for modification because the abstraction is fixed but behavior can be extended by creating new derivatives of the abstraction.
Straightforward:
To state the open closes principle in a resumed way you can say:
- You should design modules that never change.
- When requirements change, you extend the behavior of such modules by adding new code, not by changing old code that already works.
I’ll use a very simple code example to show a violation of this principle:
public enum GymMembership{Platinum, Gold, Silver} public class Discount { public void SetDiscount(int value, GymMembership membershipType) { if (membershipType == GymMembership.Platinum) { // Set platinum discount } if (membershipType == GymMembership.Gold) { // Set gold dicount } if(membershipType == GymMembership.Silver) { // Set silver discount } } }
As you can see above, we have an example of discount assignment at a gym. This discount is assigned according to the customer membership type.
In the class Discount let’s assume there are initially two membership types, Platinum and Gold and we are verifying through ifs which membership type we are using and run the respective code.
In the future if the gym needs to implement a new membership discount type (Silver in this case) we will need to modify the class Discount, adding a new if condition and for future new membership types we will need to add more ifs. Remembering what was told about this principle and the problems that could lead us to, modifying the class over and over again is a bad practice of software design and a violation of the Open/closed Principle.
On the following code example we will see the best approach for this gym discount implementation using Open/closed Principle:
public abstract class Discount { public abstract void SetDiscount(int value); } public class MembershipPlatinum : Discount { public override void SetDiscount(int value) { // Sets platinum discount } } public class MembershipGold : Discount { public override void SetDiscount(int value) { // Sets gold discount } } public class MembershipSilver : Discount { public override void SetDiscount(int value) { // Sets silver discount } }
Notice that we now have a well-defined abstraction, where all extensions implement their own business rules without having to modify one functionality due to the change or the inclusion of another.
The membership type in silver membership discount was implemented without modifying anything, using only the extension. Besides all, the code is much more beautiful, understandable and easy to apply coverage of unit tests. It is worth mentioning that it’s also in accordance with the first principle of SOLID, SRP (Single Responsibility Principle).
A good real life analogy for this principle: “If you want to add a new paint to your car, you don’t need to change its engine”.
Liskov Substitution Principle
“Derived classes should be overridden by their base classes”
LSP (Liskov Substitution Principle) is a fundamental principle of OOP and states that derived classes should be able to extend their base classes without changing their behavior. In other words, derived classes should be replaceable for their base types, i.e., a reference to a base class should be replaceable with a derived class without affecting the behavior. LSP is about following the contract of the base class.
This principle is an extension of the Open Close Principle and is violated when you have written code that throws “not implemented exceptions” or you hide methods in a derived class that have been marked as virtual in the base class. If your code follows the Liskov Substitution Principle you have many benefits, these include: code re-usability, reduced coupling, and easier maintenance.
In the following class diagram I will explain a violation of this principle in a very simple way:
We can see in the diagram that we have a base class named Board and a derived class named ThreeDBoard and following the rules of class abstraction and polymorphism the methods defined on the base class could be implemented on the derived class. However the derived class is a 3D game and a 3D game needs a third coordinate, in the other way, the base class specifies only two coordinates, so it can’t be possible to make an association betwwen a 3D board game and a 2D board game.
At first, we thought that a 3D game could perfectly derive from a 2D game, it’s in fact a board game like the 3D one, but the need of a third coordinate invalidates that association and in fact the classes are so distinct like an association between a base class Person and a derived class Vehicle.
Concluding the explanation of this principle, I’ll show in a fun but truth way the following image tha demonstrates that Liskov Substitution Principle is present in real life in many diferent ways.
“Just because it’s a “follower”, doesn’t mean is your friend” 🙂
Interface Segregation Principle
“A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.”
When we are creating interfaces we have to take some care and do not think about generalizing our Interfaces too much, because a very generic interface will certainly force us to implement methods or properties that our entity may not use, besides leaving the ugly code , violates the principle of interface segregation and depending on the case may even cause an exception in your application, so be careful when creating generic interfaces.
There are many examples in the internet of Interface Segregation Principle violations but I think this example makes clear this principle.
public interface ISoccerTeam { void ScoreGoal(); void PassBall(); void SaveGoal(); void GiveInstructions(); void CheerTeamUp(); } public class Striker : ISoccerTeam { public void ScoreGoal() { Console.Writeline("GOOAAALLL!!"); } public void PassBall() { Console.Writeline("Pass the ball to teammate!!"); } public void CheerTeamUp() { Console.Writeline("Don't give up!!"); } public void SaveGoal() { } public void GiveInstructions() { } } public class Goalkeeper : ISoccerTeam { public void ScoreGoal() { } public void PassBall() { } public void CheerTeamUp() { Console.Writeline("We can win!"); } public void SaveGoal() { Console.Writeline("Saving possible goal."); } public void GiveInstructions() { } } public class Coach : ISoccerTeam { public void ScoreGoal() { } public void PassBall() { } public void CheerTeamUp() { Console.Writeline("You're going good!"); } public void SaveGoal() { } public void GiveInstructions() { Console.Writeline("Change 4-4-2 formation to 4-3-3."); } }
In the code snippet above, we have an interface that declares five methods and we have three classes using that interface.
The problem here is that we have a generic interface that declares distinct methods and every class using that interface is forced to implement all of them. In this example, the class Striker only needs to implement the methods ScoreGoal, PassBall and CheerTeamUp, but is forced to implement methods that doesn’t need, SaveGoal and GiveInstructions. These two last methods (SaveGoal and GiveInstructions) should be implemented by the class Goalkeeper and Coach respectively. The same problema is present for the Goalkeeper and Coach Class, because they are forced to implement methods they don’t need as well.
To correct this problem using Interface Segregation Principle we need to subdivide the generic interface in smaller and more specific interfaces. The code snippet below specifies in a clear way the new implementation. We need to create the interface IStriker, IGoalkeeper, ICoach and ITeam and every interface declares only methods that belong to a specific context. Now the classes use only their specific interface having the corresponding methods, for example, only the Coach can give Instructions, but it can Cheer Team Up like the others classes using a specific implementation, so we use the interfaces ICoach and ITeam on the Coach class.
Having this principle applied now we implement methods that are really needed on our classes.
public interface IStriker { void ScoreGoal(); void PassBall(); } public interface IGoalkeeper { void SaveGoal(); } public interface ICoach { void GiveInstructions(); } public interface ITeam { void CheerTeamUp(); } public class Striker : IStriker, ITeam { public void ScoreGoal() { Console.Writeline("GOOAAALLL!!"); } public void PassBall() { Console.Writeline("Pass the ball to teammate!!"); } public void CheerTeamUp() { Console.Writeline("Don't give up!!"); } public void SaveGoal() { } public void GiveInstructions() { } } public class Goalkeeper : IGoalkeeper, ITeam { public void ScoreGoal() { } public void PassBall() { } public void CheerTeamUp() { Console.Writeline("We can win!"); } public void SaveGoal() { Console.Writeline("Saving possible goal."); } public void GiveInstructions() { } } public class Coach : ICoach, ITeam { public void ScoreGoal() { } public void PassBall() { } public void CheerTeamUp() { Console.Writeline("You're going good!"); } public void SaveGoal() { } public void GiveInstructions() { Console.Writeline("Change 4-4-2 formation to 4-3-3."); } }
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.”
To understand this principle, it’s important to understand how software development worked prior to the creation of this concept. More traditional software methods use an approach in which high-level modules rely on low-level modules, that is, the high-level policy applied to a low-level object depends directly on how this low-level object is defined. Let’s see a simple example of an unconventional application: a button and a lamp.
Using a diagram to illustrate how a model would be implemented without the dependency inversion principle, the model would look like:
The button will turn off the lamp if it’s off and turn on the lamp if it’s on. Its implementation would look something like this:
public class Button { private Lamp _lamp; public void Trigger() { if (condition) _lamp.TurnOn(); } }
Why would not this program be adequate? Simple. Your Button program would only be suitable for turning a lamp on and off. If the system owner wants this button to be used to turn off an electronic gate, for example, great changes must be made to the Button’s Trigger method so the button operates where it is needed. That is, a high-level module (Button) is depending on a low-level module (Lamp).
To be possible the use of dependency inversion, it is necessary that an additional layer is added. This layer will be responsible for mediating the button with the lamp module, leaving the system model as follows:
With this, the Button will make an implementation that will use TurnOn and TurnOff offered by the Device, while the Lamp will detail the specific implementations needed for TurnOn and TurnOff to execute correctly. For this, the Device uses the concept of interface, that is, the characteristics TurnOn and TurnOff presented by it will only be representative, and will only be implemented on the low level module.
So, returning to our example, if it is necessary that the button will also be used to open an electronic gate, it will only be necessary to add an ElectricalGate class to the model, which represents the specifics implementations to the TurnOn and TurnOff methods defined in the interface, Device.
The model then looks like this:
Finally, we can observe an isolation between the definition and the specific implementation. The behavior of the button is completely independent of which object it is switching on or off.
public class Button { private Device _device; public void Trigger() { if (condition) _device.TurnOn(); } } public interface Device { void TurnOn(); void TurnOff(); } public class Lamp : Device { public void TurnOn() { // turn on lamp } public void TurnOff() { // turn off lamp } } public class ElectricalGate : Device { public void TurnOn() { // turn on electrical gate } public void TurnOff() { // turn off electrical gate } }
Conclusion
Revising the five principles explained in this article:
S stands for SRP (Single responsibility principle) – A class should take care of only one responsibility.
O stands for OCP (Open closed principle) – Extension should be preferred over modification.
L stands for LSP (Liskov substitution principle) – A parent class object should be able to refer child objects seamlessly during runtime polymorphism.
I stands for ISP (Interface segregation principle) – Client should not be forced to use a interface if it does not need it.
D stands for DIP (Dependency inversion principle) – High level modules should not depend on low level modules but should depend on abstraction.
In conclusion, from my point of view and throught experience I think these principles are the key to create a good base to develop quality software. Obviously it’s dificult to follow these principles 100% of the time and with the best aproach, but if we have a minimal understanding of each principle we could avoid many wrong decisions that could be proven fatal in an advanced phase of our project.
References:
Wikipedia – https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)
Code Project – https://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp
Robsoncastilho – https://robsoncastilho.com.br/tag/solid/
Dtdigital – http://blog.dtidigital.com.br/tag/principios-solid/
0 Comments