SOLID Principles in C++: Theory and Practice for Clean Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

SOLID principles in C++: from theory to practice



SOLID is an acronym for five fundamental principles of object-oriented programming formulated by Robert Martin (Uncle Bob). In the context of C++, these principles help create flexible, maintainable, and testable code. In this article, we will break down each principle with practical examples in C++.



1. Single Responsibility Principle (SRP)



Essence: Each class should have only one reason to change. Simply put — a class handles only one task.



Bad example (SRP violation):

class Report {public:    void generateReport() { /* generate report */ }    void saveToFile(const std::string& filename) { /* save to file */ }    void printReport() { /* print report */ }};


Here the Report class is responsible for generation, saving, and printing — three different responsibilities.



Good example (SRP compliance):

class Report {public:    std::string generate() { /* generate report */ return "report data"; }};

class ReportSaver {public: void save(const std::string& data, const std::string& filename) { // save to file }};

class ReportPrinter {public: void print(const std::string& data) { // print report }};


Now each class has one clear responsibility, which simplifies testing and modification.



2. Open-Closed Principle (OCP)



Essence: Classes should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.



Bad example:

enum class ShapeType { Circle, Rectangle };

class Shape {public: ShapeType type;};

class AreaCalculator {public: double calculateArea(const Shape& shape) { if (shape.type == ShapeType::Circle) { // calculate circle area return 3.14 * radius * radius; } else if (shape.type == ShapeType::Rectangle) { // calculate rectangle area return width * height; } return 0.0; }};


When adding a new shape (e.g., triangle), you would have to modify AreaCalculator.



Good example (via polymorphism):

class Shape {public:    virtual double area() const = 0;    virtual ~Shape() = default;};

class Circle : public Shape { double radius;public: explicit Circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; }};

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 AreaCalculator {public: double calculateArea(const Shape& shape) { return shape.area(); }};


Now to add a new shape, you only need to create a new derived class without touching AreaCalculator.



3. Liskov Substitution Principle (LSP)



Essence: Objects of subclasses should be interchangeable with objects of the base class without breaking program correctness.



Classic violation (rectangle-square):

class Rectangle {protected:    int width, height;public:    void setWidth(int w) { width = w; }    void setHeight(int h) { height = h; }    int area() const { return width * height; }};

class Square : public Rectangle {public: void setWidth(int w) override { width = w; height

Blogs

Book Recommendations