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

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

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

Начать курс

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

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

Что такое SOLID и зачем это нужно?

SOLID — это набор правил, которые помогают разработчику избежать "плохого" кода. Соблюдение этих принципов ведет к снижению связанности (coupling) и повышению связности (cohesion) модулей. В C++, где мощные возможности шаблонов и множественное наследование могут легко привести к хаосу, SOLID служит надежным компасом. Каждая буква акронима расшифровывается следующим образом:

  • S — Single Responsibility Principle (Принцип единственной ответственности)
  • O — Open-Closed Principle (Принцип открытости/закрытости)
  • L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
  • I — Interface Segregation Principle (Принцип разделения интерфейса)
  • D — Dependency Inversion Principle (Принцип инверсии зависимостей)

Принцип единственной ответственности (Single Responsibility)

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

Практика в C++: Нарушение этого принципа часто приводит к "божественным объектам", которые делают всё сразу.

// ❌ Плохо: класс нарушает SRP
class Report {
public:
    void generateReport() { /* генерация данных */ }
    void saveToFile(const std::string& filename) { /* запись в файл */ }
    void printReport() { /* отправка на принтер */ }
};

// ✅ Хорошо: разделяем ответственность
class ReportData {
public:
    std::string generate() { return "Report data"; }
};

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

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

В хорошем примере каждый класс отвечает за свою задачу. Это упрощает тестирование и модификацию кода.

Принцип открытости/закрытости (Open-Closed)

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

Практика в C++: Достигается с помощью абстракции и полиморфизма.

// ❌ Плохо: чтобы добавить новую фигуру, нужно менять код
class AreaCalculator {
public:
    double calculateArea(std::string shapeType, double param) {
        if (shapeType == "circle") {
            return 3.14 * param * param;
        } else if (shapeType == "square") {
            return param * param;
        }
        return 0.0;
    }
};

// ✅ Хорошо: используем полиморфизм
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

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

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

class AreaCalculatorOCP {
public:
    double calculate(const Shape& shape) {
        return shape.area();
    }
};

Теперь для добавления новой фигуры (например, треугольника) не нужно менять AreaCalculatorOCP — достаточно создать новый класс, наследующий от Shape.

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

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

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

// ❌ Плохо: квадрат нарушает LSP
class Rectangle {
public:
    virtual void setWidth(int w) { width = w; }
    virtual void setHeight(int h) { height = h; }
    int area() const { return width * height; }
private:
    int width = 0, height = 0;
};

class Square : public Rectangle {
public:
    void setWidth(int w) override {
        Rectangle::setWidth(w);
        Rectangle::setHeight(w); // нарушает ожидания
    }
    void setHeight(int h) override {
        Rectangle::setWidth(h);
        Rectangle::setHeight(h);
    }
};

// ✅ Хорошо: используем общий интерфейс или отдельные классы
class ShapeLSP {
public:
    virtual int area() const = 0;
};

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

class SquareLSP : public ShapeLSP {
    int side;
public:
    SquareLSP(int s) : side(s) {}
    int area() const override { return side * side; }
};

Здесь SquareLSP и RectangleLSP не наследуют друг от друга, а реализуют общий интерфейс, что исключает неожиданное поведение.

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

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

Практика в C++: В C++ интерфейсы реализуются через абстрактные классы.

// ❌ Плохо: "толстый" интерфейс
class MultiFunctionPrinter {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

// Класс для простого принтера вынужден реализовывать ненужные методы
class SimplePrinter : public MultiFunctionPrinter {
public:
    void print() override { /* печать */ }
    void scan() override { throw std::logic_error("Not supported"); }
    void fax() override { throw std::logic_error("Not supported"); }
};

// ✅ Хорошо: разделяем интерфейсы
class IPrinter {
public:                

Блоги

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