The SOLID principles are a set of five design principles in object-oriented programming that help developers write code that is easy to maintain and extend. They were introduced by Robert C. Martin, also known as Uncle Bob.
- SRP – Single Responsibility Principle OCP
- Open/Closed Principle LSP
- Liskov’s Substitution Principle ISP
- Interface Segregation Principle DIP
- Dependency Inversion Principle
Here’s a brief overview of each principle with an example:
Single Responsibility Principle (SRP)
- Definition: A class should have one, and only one, reason to change. This means that a class should only have one job or responsibility.
- Example: Consider a
Report
class that has methods for data analysis and methods for printing the report. According to SRP, these responsibilities should be separated into two classes: one for data analysis (ReportAnalyzer
) and another for report formatting and printing (ReportPrinter
).
// Example: Separate classes for handling user data and file operations.
class UserData {
public:
UserData(std::string userName, int age) : userName(userName), age(age) {}
// ... Other user-related functionalities ...
private:
std::string userName;
int age;
};
class FileHandler {
public:
void saveUserData(const UserData& userData) {
// Code to save user data to a file
}
// ... Other file handling functionalities ...
};
Intent: Each class should have one responsibility, one single purpose. This means that a class will do only one job, which leads us to conclude it should have only one reason to change.
SRP states that classes should be cohesive to the point that it has a single responsibility, where responsibility defines as “a reason for the change.”
SRP violated
- As we start adding more domain objects like
Movies
,Games
, etc. you have to implementsave
method for everyone separately which is not the actual problem. - The real problem arises when you have to change or maintain
save
functionality. For instance, some other day, you will no longer save data on files and adopted database. In this case, you have to go through every domain object implementation and need to change code all over, which is not good. - We have violated the Single Responsibility Principle by providing
Book
class reasons to change it:- Saving the
Book
- Code will also become repetitive, bloated, and hard to maintain.
- Saving the
Solution using SRP
- Moving the persistence operation to another class will clearly separate the responsibilities and we will be free to exchange persistence methods without affecting our
Book
class. Book
should only take care of entries & things related to the book.SaveFileManager
all related saving code will be at one place. You can also templates it to accept more domain / class objects.
Benefits
- This improves your development speed and makes your life as a software developer a lot easier.
- Usually the requirements of a system change over time, and so does the design/architecture. The more responsibilities your class has, the more often you need to change it. If your class implements multiple responsibilities, they are no longer independent of each other.
- Isolated changes reduce the breaking of other unrelated areas of the software.
- As programming errors are inversely proportional to complexity, being easier to understand makes the code less prone to bugs and easier to maintain.
- Having a single responsibility means the class should be reusable without or less modification.
Example C++
Open/Closed Principle (OCP)
- Definition: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means you should be able to add new functionality without changing existing code.
- Example: If you have a
PaymentProcessor
class, and you want to add a new payment method, you shouldn’t have to change thePaymentProcessor
class. Instead, you can create a new class that extends thePaymentProcessor
class or implements a common interface, adhering to OCP.
// Example: Separate classes for handling user data and file operations.
class UserData {
public:
UserData(std::string userName, int age) : userName(userName), age(age) {}
// ... Other user-related functionalities ...
private:
std::string userName;
int age;
};
class FileHandler {
public:
void saveUserData(const UserData& userData) {
// Code to save user data to a file
}
// ... Other file handling functionalities ...
};
Intent: classes should be open for extension, closed for modification.
- You should be able to extend a classes behaviour, without modifying it.
- A class should be open for extension but closed for modification. It means that whenever you need new functionality, it’s better to extend the base functionality instead of modifying it.
OCP violated
- Each
Product
object has three properties: name, price, and weight. - After designing the
Product
class and the whole e-commerce platform, a new requirement comes from the clients. They now want to buy digital products, such as e-books, movies, and audio recordings. - Probably you will need to modify the
Product
class, and that change may break other parts of code as well. - We can achieve that by redesigning the
Product
class and making it an abstract base class for all products. - The open-closed principle states that your system should be open to extension but should be closed for modification. Unfortunately what we are doing here is modifying the existing code which is a violation of OCP.
Solution using OCP
- Created two more classes extending the
Product
base class.PhysicalProduct
andDigitalProduct
- Removed the
weight
property from theProduct
class, andPhysicalProduct
has a weight property andDigitalProduct
does not have one. Instead, it has afilePath
property.
Benefits
- Prevent unintentional behaviors of existing components, instead of modifying the current existing code, one should extend the existing component to ensure that the original behavior of the component is not affected.
- Maintainability. This encourages loose coupling between components that are closed modifications and new functionality that is added.
Example in C++
Liskov Substitution Principle (LSP)
- Definition: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. Subclasses should extend their base classes without changing their behavior.
- Example: A solution to this would be to have a more general class, say
Animal
, and then have separate subclasses forFlyingBird
andNonFlyingBird
.Bird
can then be an abstract class or interface with different implementations forfly()
inFlyingBird
(likeEagle
) andNonFlyingBird
(likePenguin
), ensuring that substitutions of base class instances with subclass instances do not affect the program’s behavior.
// Example: Birds
class Bird {
public:
virtual void eat() = 0;
virtual ~Bird() {}
};
class FlyingBird : public Bird {
public:
virtual void fly() = 0; // Flying functionality
};
class Sparrow : public FlyingBird {
public:
void fly() override {
// Implementation for flying
}
void eat() override {
// Implementation for eating
}
};
class Ostrich : public Bird {
public:
void eat() override {
// Ostriches don't fly, so no fly method here
}
};
Interface Segregation Principle (ISP)
- Definition: No client should be forced to depend on methods it does not use. This principle suggests that it is better to have several specific interfaces rather than one general-purpose interface.
- Example: If you have an interface
IPrinter
with methods for printing, scanning, and faxing, a printer class that only supports printing would still need to implement (or at least stub out) the scanning and faxing methods. According to ISP, it’s better to have separate interfaces likeIPrintable
,IScannable
, andIFaxable
, and implement only those that are relevant.
// Example: Multi-function machine
class IPrinter {
public:
virtual void print() = 0;
};
class IScanner {
public:
virtual void scan() = 0;
};
class Printer : public IPrinter {
public:
void print() override {
// Print functionality
}
};
class Scanner : public IScanner {
public:
void scan() override {
// Scan functionality
}
};
Dependency Inversion Principle (DIP)
- Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend upon abstractions. In simpler terms, depend on interfaces or abstract classes rather than concrete classes.
- Example: If you have a high-level module
OrderProcessor
that directly creates an instance of a low-level moduleEmailNotifier
to send notifications, it’s a violation of DIP. Instead,OrderProcessor
should depend on anINotifier
interface, andEmailNotifier
should be an implementation ofINotifier
. This way, the dependency is on an abstraction, not a concretion, and it’s easy to switch to a different notifier in the future.
// Example: Message sending
class IMessageSender {
public:
virtual void sendMessage(const std::string& message) = 0;
virtual ~IMessageSender() {}
};
class EmailSender : public IMessageSender {
public:
void sendMessage(const std::string& message) override {
// Send email
}
};
class NotificationManager {
public:
NotificationManager(IMessageSender* sender) : sender(sender) {}
void notify(const std::string& message) {
sender->sendMessage(message);
}
private:
IMessageSender* sender;
};