In this post we’re going to dive into the S.O.L.I.D design principles. We’re going to do this by using code examples & illustrations. One of the best ways to understand theseprinciples is by solving a business case with code that violates these principles and then refactoring the code to adhere to the principles. At a high level first, let’s explore the definition of each principle.
Single Responsibility – Classes/methods should do one thing and one thing only.
Open-Closed – Classes should be open for extension closed for modification.
Liskov Substitution – If S is subtype of T and T has a property p then an object of S must also have a property of p.
Interface Segregation – Clients should not be forced to depend upon interfaces that they do not use.
Dependency Inversion – High level objects should not depend on low level objects. Both should depend on abstraction.
So that was a high-level explanation, below we’ll take a deeper dive into each one.
Single Responsibility Principle – Classes/methods should do one thing and one thing only.
Avoid tightly coupled classes, doing multiple things
Classes and indeed method should do one thing and one thing only & do that well. This concept was explained well by Robert C. Martin in his book “Principle Patterns and Practices of Agile Software Development”. Martin suggests that we define each responsibility of a class as a reason for change. If you can think of more than one reason to change a class, then it probably has more than one responsibility. Let’s take a look at an example.
Business Case: A user in a shopping cart has a multi-buy offer. We want to add this offer to the member.
Breaking Implementation: This implementation breaks the single responsibility principle because this class is responsible for doing many tasks including validation, offer assignment and sending confirmation email.
Adhering Implementation: Separate the code into single responsibility classes.
OfferValidator – The single responsibility of the offer validator is to simply validate the offer and any other offers.
OfferService – The single responsibility the offer service is to ensure a member is attributed this offer.
EmailService – The single responsibility the email service is to send emails.
OfferController – The single responsibility the offer controller is to control the flow of the program. One may argue that now the controller is doing more than one thing; but the single responsibility of the controller is to control the flow of the program, it delegates the actions of validation and sending emails to other classes.
Open-Closed Principle – Classes, methods should be open for extension and closed for modification.
Chest surgery is not required when needing to put on a coat
The general idea of this principle is simple. One should be able to add new functionality to a class without the need for modifying the class. Think composition & design patterns like the Strategy pattern as ways to achieve this principle. Here’s an example:
Business Case: the need of a payment service to process payments.
Breaking Implementation:
This implementation breaks the open closed principle because the PaymentService is not open for extension. By that we mean, what happens if we want to extend the code to allow a new payment type such as PayPal. We would have to modify the class to write a new method like `processPaymentWithPaypal` hence breaking the open for extension and closed for modification.
Adhering Implementation: Here as you can see; we’ve introduced a new interface called PaymentMethod; all payment types will need to implement this interface. If the need arises to add another payment type, we simply write a new class that implements this interface and pass that to the payment service. Now the PaymentService can take several payment types (i.e. CreditCard/PayPal/Bitcoin) hence making it open for extension and as we do not need to modify the PaymentService; hence it’s closed for modification.
Liskov Substitution – If S is subtype of T and T has a property p then an object of S must also have a property of p.
“If it looks like a Duck, Quacks like a Duck, but needs batteries, – You probably have the Wrong Abstraction.”
What this principle really states is that if we have a property/method in a parent class; this property/method should be valid for all subclasses; without invalidating the correctness of the subclass. Let’s explore this by the following diagram:
Here ‘S’ is a subtype of ‘T’; if ‘T’ has the property of ‘p’ then ‘S’ must also have the property of ‘p’.
Business Case: A transportation representation is required for cars and bicycles.
Breaking Implementation:
The Bike class does not support property p -> startEngine() of class T -> TransportationVehicle
Adhering Implementation: In the above case the wrong abstraction was choosen. One way to solve this problem is to remove the startEngine() method from TransportactionVehicle and to write a new abstract class called EnginePoweredTransportactionVehicle that extends the TransportactionVehicle. Therefor the bike classes, or any other transportation vehicles that do not use an engine, can extend the transportation vehicle without needing to implement any engine methods.
Interface Segregation Principle: Clients should not be forced to depend upon interfaces that they do not use.
Don’t force client to depend on things they don’t use.
This principle is fairly easy to comprehend. If we add too many methods to our interfaces, they become fat. Fat interface need to be split into many smaller relevant interfaces so clients can use methods that are only relevant to them. This leads on to clients should not be unnecessarily forced to provide dummy or empty implementations for interface methods that they do not use.
Business Case: Create a class for auction bidding on items
Breaking Implementation: Here we have an interface for the Auctions; it combines the bidding on an auction and the adding of an item to an auction. Conceptually these two actions should be separated into their own interfaces. More importantly, if we want to include different types of Auction (e.g. Standard Auction, Buy Now Auction) we should not have to duplicate the code for adding/removing an auction item for both implementations.
Adhering Implementation:
The solution is rather simple; breaking down this fat interface into two interfaces AuctionBidding, AuctionItem deals with this problem.
Dependency Inversion: High level objects should not depend on low level objects. Both should depend on abstraction.
You wouldn’t solder a lamp directly into the electrical socket
This principle is about decoupling dependencies between classes. When one class knows explicitly about the design and implementation of another class, changes to one class raises the risk of breaking the other class. It’s best illustrated via diagrams.
Business Case: We would like to add a user to the database.
Breaking Implementation: The Controller is high level module, it should not be depending on the concrete DB repository implementation.
Adhering Implementation: Here IRepository is the abstraction. In this case one can change the repository implementation say from MySQL to Oracle without effecting the Controller. Quite often to comply with this principle, dependency injection is used allowing use to inject the DB repository were we need it.
Conclusion
In order to produce high quality software maintainable, scalable and testable the S.O.L.I.D design principles are an essential guidelines for developers for keep in mind. They may appear confusing at first but as you write more and more code such princieples will become second nature. As developers should not only be aware of them but if we are serious about creating ‘high quality’ software we must aim to refactor our code to abide by these principles.
Happy coding 😊














Leave a comment