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

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

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

Начать курс

Введение: SOLID в контексте C++

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

В этой статье мы разберём каждый принцип на практических примерах кода C++ (17/20), акцентируя внимание на нюансах реализации: использование std::unique_ptr, std::function, CRTP, виртуальных деструкторов и современных идиом.

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

Формулировка: Класс должен иметь только одну причину для изменения.

На практике это означает, что класс отвечает за одну бизнес-сущность или одну операцию. Нарушение SRP приводит к «сцеплению» ответственностей, что усложняет тестирование и модификацию.

Пример нарушения

class Report {
public:
    void generate() { /* создание данных */ }
    void saveToFile(const std::string& path) { /* запись в файл */ }
    void sendEmail(const std::string& to) { /* отправка */ }
};

Здесь Report отвечает и за генерацию, и за сохранение, и за отправку — три причины для изменения.

Рефакторинг с SRP

class Report {
public:
    void generate() { /* создание данных */ }
};

class ReportSaver {
public:
    void save(const Report& report, const std::string& path);
};

class EmailSender {
public:
    void send(const Report& report, const std::string& to);
};

Теперь каждый класс имеет единственную ответственность. Это упрощает модульное тестирование и переиспользование.

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

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

В C++ это часто реализуется через полиморфизм (виртуальные функции) или через шаблоны (CRTP). Для продвинутого уровня рассмотрим обе техники.

Полиморфный подход (виртуальные функции)

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

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

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

Добавление новой фигуры (например, Triangle) не требует изменения существующих классов — только создание нового наследника.

Шаблонный подход (CRTP — Curiously Recurring Template Pattern)

template<typename Derived>
class ShapeBase {
public:
    double area() const { return static_cast<const Derived*>(this)->area_impl(); }
};

class Circle : public ShapeBase<Circle> {
    double r;
public:
    Circle(double r) : r(r) {}
    double area_impl() const { return 3.14 * r * r; }
};

CRTP даёт статический полиморфизм без накладных расходов на виртуальные вызовы, что критично для high-performance C++.

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

Формулировка: Объекты базового класса должны заменяться объектами производных классов без нарушения корректности программы.

Классическое нарушение LSP в C++ — переопределение виртуального метода с ослаблением предусловий или усилением постусловий. Рассмотрим пример с иерархией «Прямоугольник — Квадрат».

Антипаттерн

class Rectangle {
protected:
    double w, h;
public:
    virtual void setWidth(double w) { this->w = w; }
    virtual void setHeight(double h) { this->h = h; }
    double area() const { return w * h; }
};

class Square : public Rectangle {
public:
    void setWidth(double w) override { this->w = w; this->h = w; }
    void setHeight(double h) override { this->w = h; this->h = h; }
};

Если клиент ожидает, что Rectangle может менять ширину и высоту независимо, то Square нарушает это ожидание. Решение — отказаться от наследования, либо использовать интерфейс Shape без мутаторов.

Правильная реализация

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

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

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

Теперь оба класса корректно подставляются без нарушения контракта.

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

Формулировка: Клиенты не должны зависеть от интерфейсов, которые они не используют.

В C++ это означает, что интерфейс (абстрактный класс) должен быть узким и специфичным. Вместо одного «толстого» интерфейса — несколько мелких.

Пример нарушения

class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

Роботу (Robot) не нужно есть и спать, но он вынужден реализовывать эти методы.

Рефакторинг

class Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() = default;
};

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

class Human : public Workable, public Eatable { /* ... */ };
class Robot : public Workable { /* ... */ };

Использование множественного наследования от узких интерфейсов — стандартный подход в C++.

5. Dependency Inversion Principle (DIP

Блоги

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