SOLID principles in C++: from theory to practice
SOLID principles are five fundamental rules of object-oriented design formulated by Robert Martin. They help create flexible, scalable, and easily maintainable code. In this article, we will break down each principle using C++ examples, from simple theory to real-world practical cases.
1. Single Responsibility Principle (SRP)
Theory: Each class should have only one reason to change. In other words, a class is responsible for only one part of the program's functionality.
Problem: Developers often place data logic, saving, and screen output into a single class. This violates SRP.
Practice in C++: Let's split the Report class into three separate classes.
// Bad: violates SRPclass Report { std::string title; std::string content;public: void generate() { /* report generation */ } void saveToFile(const std::string& filename) { /* saving to file */ } void print() { /* printing */ }};
// Good: complies with SRPclass Report { std::string title; std::string content;public: void generate() { /* generation only */ }};
class ReportSaver {public: void saveToFile(const Report& report, const std::string& filename) { /* saving */ }};
class ReportPrinter {public: void print(const Report& report) { /* printing */ }};Conclusion: Each class is now responsible for its own task. The code has become easier to test and modify.
2. Open-Closed Principle (OCP)
Theory: Software entities (classes, modules, functions) should be open for extension but closed for modification. That is, we should be able to add new functionality without changing existing code.
Practice in C++: Use polymorphism and abstract classes.
#include <iostream>#include <vector>#include <memory>
// Base class for shapesclass Shape {public: virtual double area() const = 0; virtual ~Shape() = default;};
class Rectangle : public Shape { double width, height;public: Rectangle(double w, double h) : width(w), height(h) {} double area() const override { return width * height; }};
class Circle : public Shape { double radius;public: Circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; }};
// Class for calculating total area — does not require changes when adding new shapesclass AreaCalculator {public: double totalArea(const std::vector<std::unique_ptr<Shape>>& shapes) { double total = 0; for (const auto& shape : shapes) { total += shape->area(); } return total; }};
int main() { std::vector<std::unique_ptr<Shape>> shapes; shapes.push_back(std::make_unique<Rectangle>(4, 5)); shapes.push_back(std::make_unique<Circle>(3));
AreaCalculator calc; std::cout << "Total area: " << calc.totalArea(shapes) << std::endl; return 0;}Conclusion: We can add new shapes (e.g., Triangle) without touching AreaCalculator.
3. Liskov Substitution Principle (LSP)
Theory: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. Simply put, if you have a parent class and a child class, you should be able to use the child where the parent is expected, and everything should work.
Practice