DEV Community

Cover image for Crafting Maintainable and Scalable Software: Applying SOLID Principles
Mahabubur Rahman
Mahabubur Rahman

Posted on

Crafting Maintainable and Scalable Software: Applying SOLID Principles

SOLID principles are a set of design guidelines that can help developers create more maintainable, understandable, and flexible software. These principles, introduced by Robert C. Martin, are particularly useful in object-oriented programming. Let's explore each principle with examples in C++.

Single Responsibility Principle (SRP)

Principle: A class should have only one reason to change, meaning it should have only one job or responsibility.

#include <iostream>
#include <string>

// Before applying SRP
class User {
public:
    void login(std::string username, std::string password) {
        // logic for user login
    }

    void saveUserData(std::string userData) {
        // logic for saving user data
    }

    void sendEmail(std::string emailContent) {
        // logic for sending email
    }
};

// After applying SRP
class Authenticator {
public:
    void login(std::string username, std::string password) {
        // logic for user login
    }
};

class UserDataHandler {
public:
    void saveUserData(std::string userData) {
        // logic for saving user data
    }
};

class EmailSender {
public:
    void sendEmail(std::string emailContent) {
        // logic for sending email
    }
};

int main() {
    Authenticator auth;
    auth.login("user", "password");

    UserDataHandler dataHandler;
    dataHandler.saveUserData("user data");

    EmailSender emailSender;
    emailSender.sendEmail("Welcome!");

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Open/Closed Principle (OCP)

Principle: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

#include <iostream>
#include <string>
#include <vector>

// Base class
class Shape {
public:
    virtual void draw() const = 0; // pure virtual function
};

// Derived class for Circle
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing Circle\n";
    }
};

// Derived class for Rectangle
class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing Rectangle\n";
    }
};

// Function to draw all shapes
void drawShapes(const std::vector<Shape*>& shapes) {
    for (const auto& shape : shapes) {
        shape->draw();
    }
}

int main() {
    Circle circle;
    Rectangle rectangle;

    std::vector<Shape*> shapes = { &circle, &rectangle };
    drawShapes(shapes);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Liskov Substitution Principle (LSP)

Principle: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

#include <iostream>

class Bird {
public:
    virtual void fly() {
        std::cout << "Bird is flying\n";
    }
};

class Ostrich : public Bird {
public:
    void fly() override {
        throw std::logic_error("Ostriches can't fly");
    }
};

void makeBirdFly(Bird& bird) {
    bird.fly();
}

int main() {
    Bird sparrow;
    makeBirdFly(sparrow); // works fine

    Ostrich ostrich;
    try {
        makeBirdFly(ostrich); // throws exception
    } catch (const std::logic_error& e) {
        std::cerr << e.what() << '\n';
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Interface Segregation Principle (ISP)

Principle: Clients should not be forced to depend on interfaces they do not use.

#include <iostream>

// Before applying ISP
class IWorker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
};

class Worker : public IWorker {
public:
    void work() override {
        std::cout << "Working\n";
    }

    void eat() override {
        std::cout << "Eating\n";
    }
};

class Robot : public IWorker {
public:
    void work() override {
        std::cout << "Working\n";
    }

    void eat() override {
        // Robots don't eat
    }
};

// After applying ISP
class IWorkable {
public:
    virtual void work() = 0;
};

class IFeedable {
public:
    virtual void eat() = 0;
};

class HumanWorker : public IWorkable, public IFeedable {
public:
    void work() override {
        std::cout << "Working\n";
    }

    void eat() override {
        std::cout << "Eating\n";
    }
};

class AndroidWorker : public IWorkable {
public:
    void work() override {
        std::cout << "Working\n";
    }
};

int main() {
    HumanWorker human;
    human.work();
    human.eat();

    AndroidWorker robot;
    robot.work();

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Dependency Inversion Principle (DIP)

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.

#include <iostream>
#include <memory>

// Before applying DIP
class LightBulb {
public:
    void turnOn() {
        std::cout << "LightBulb turned on\n";
    }

    void turnOff() {
        std::cout << "LightBulb turned off\n";
    }
};

class Switch {
    LightBulb& bulb;
public:
    Switch(LightBulb& bulb) : bulb(bulb) {}

    void operate() {
        bulb.turnOn();
        bulb.turnOff();
    }
};

// After applying DIP
class Switchable {
public:
    virtual void turnOn() = 0;
    virtual void turnOff() = 0;
};

class LightBulbDIP : public Switchable {
public:
    void turnOn() override {
        std::cout << "LightBulb turned on\n";
    }

    void turnOff() override {
        std::cout << "LightBulb turned off\n";
    }
};

class SwitchDIP {
    std::unique_ptr<Switchable> device;
public:
    SwitchDIP(std::unique_ptr<Switchable> device) : device(std::move(device)) {}

    void operate() {
        device->turnOn();
        device->turnOff();
    }
};

int main() {
    LightBulb bulb;
    Switch switchObj(bulb);
    switchObj.operate();

    auto lightBulbDIP = std::make_unique<LightBulbDIP>();
    SwitchDIP switchDIP(std::move(lightBulbDIP));
    switchDIP.operate();

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

By adhering to the SOLID principles, developers can create software that is easier to manage, scale, and understand. Each principle addresses different aspects of software design, ensuring that code remains robust and flexible in the face of change. Implementing these principles in C++ requires careful planning and a solid understanding of object-oriented design concepts.

Top comments (1)

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

std::string should invariably be passed by const&, not value.