SOLID принципы в C++: теория и практика с примерами кода

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

SOLID принципы в C++: от теории к практике

Принципы SOLID — это пять фундаментальных правил объектно-ориентированного проектирования, сформулированных Робертом Мартином. Они помогают создавать гибкий, масштабируемый и легко поддерживаемый код. В этой статье мы разберём каждый принцип на примерах C++, от простой теории до реальных практических кейсов.

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

Теория: Каждый класс должен иметь только одну причину для изменения. Другими словами, класс отвечает только за одну часть функциональности программы.

Проблема: Часто разработчики помещают в один класс логику работы с данными, их сохранение и вывод на экран. Это нарушает SRP.

Практика на C++: Разделим класс Report на три отдельных класса.

// Плохо: нарушает SRP
class Report {
    std::string title;
    std::string content;
public:
    void generate() { /* генерация отчёта */ }
    void saveToFile(const std::string& filename) { /* сохранение в файл */ }
    void print() { /* вывод на принтер */ }
};

// Хорошо: соблюдает SRP
class Report {
    std::string title;
    std::string content;
public:
    void generate() { /* только генерация */ }
};

class ReportSaver {
public:
    void saveToFile(const Report& report, const std::string& filename) { /* сохранение */ }
};

class ReportPrinter {
public:
    void print(const Report& report) { /* печать */ }
};

Вывод: Каждый класс теперь отвечает за свою задачу. Код стал проще тестировать и изменять.

2. Open-Closed Principle (OCP) — Принцип открытости/закрытости

Теория: Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. То есть мы должны иметь возможность добавлять новую функциональность без изменения существующего кода.

Практика на C++: Используем полиморфизм и абстрактные классы.

#include <iostream>
#include <vector>
#include <memory>

// Базовый класс для фигур
class 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 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;
}

Вывод: Мы можем добавлять новые фигуры (например, Triangle), не трогая AreaCalculator.

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Теория: Объекты в программе должны быть заменяемы экземплярами их подтипов без нарушения корректности программы. Проще говоря, если у вас есть класс-родитель и класс-наследник, то вы должны иметь возможность использовать наследника там, где ожидается родитель, и всё должно работать.

Практика на C++: Классический пример нарушения — наследование Square от Rectangle.

// Нарушение LSP
class Rectangle {
protected:
    int width, height;
public:
    virtual void setWidth(int w) { width = w; }
    virtual 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 = w; // нарушение: меняем и высоту
    }
    void setHeight(int h) override {
        width = h;
        height = h; // нарушение: меняем и ширину
    }
};

// Исправление: не наследуем Square от Rectangle
class Shape {
public:
    virtual int area() const = 0;
};

class Rectangle : public Shape {
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    int area() const override { return width * height; }
};

class Square : public Shape {
    int side;
public:
    explicit Square(int s) : side(s) {}
    int area() const override { return side * side; }
};

Вывод: Если наследник меняет поведение родителя так, что нарушает ожидания, — это нарушение LSP. Лучше использовать композицию или отдельные классы.

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса

Теория: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько специализированных интерфейсов, чем один большой.

Практика на C++: Разделим «толстый» интерфейс Worker на два.

// Плохо: один интерфейс для всего
class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

// Хорошо: разделённые интерфейсы
class Workable {
public:
    virtual void work() = 0;
};

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

class Sleepable {
public:
    virtual void sleep() = 0;
};

// Робот работает, но не ест и не спит
class Robot : public Workable {
public:
    void work() override { /* работа */ }
};

// Человек делает всё
class Human : public Workable, public Eatable, public Sleepable {
public:
    void work() override { /* работа */ }
    void eat() override { /* еда */ }
    void sleep() override { /* сон */ }
};

Вывод: Робот не вынужден реализовывать методы eat() и sleep(), которые ему не нужны.

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Блоги

Рекомендаций по книгам