The SOLID principles

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).
C++
// 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 implement save 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.

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 the PaymentProcessor class. Instead, you can create a new class that extends the PaymentProcessor class or implements a common interface, adhering to OCP.
C++
// 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: nameprice, 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 and DigitalProduct
  • Removed the weight property from the Product class, and PhysicalProduct has a weight property and DigitalProduct does not have one. Instead, it has a filePath 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 for FlyingBird and NonFlyingBirdBird can then be an abstract class or interface with different implementations for fly() in FlyingBird (like Eagle) and NonFlyingBird (like Penguin), ensuring that substitutions of base class instances with subclass instances do not affect the program’s behavior.
C++
// 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 like IPrintableIScannable, and IFaxable, and implement only those that are relevant.
C++
// 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 module EmailNotifier to send notifications, it’s a violation of DIP. Instead, OrderProcessor should depend on an INotifier interface, and EmailNotifier should be an implementation of INotifier. This way, the dependency is on an abstraction, not a concretion, and it’s easy to switch to a different notifier in the future.
C++
// 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;
};