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

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

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

Начать курс

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

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

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

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

Плохой пример (нарушение SRP):

class Report {
public:
    void generateReport() { /* генерация отчёта */ }
    void saveToFile(const std::string& filename) { /* сохранение в файл */ }
    void printReport() { /* печать отчёта */ }
};

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

Хороший пример (соблюдение SRP):

class Report {
public:
    std::string generate() { /* генерация отчёта */ return "report data"; }
};

class ReportSaver {
public:
    void save(const std::string& data, const std::string& filename) {
        // сохранение в файл
    }
};

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

Теперь каждый класс имеет одну чёткую ответственность, что упрощает тестирование и модификацию.

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

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

Плохой пример:

enum class ShapeType { Circle, Rectangle };

class Shape {
public:
    ShapeType type;
};

class AreaCalculator {
public:
    double calculateArea(const Shape& shape) {
        if (shape.type == ShapeType::Circle) {
            // расчёт площади круга
            return 3.14 * radius * radius;
        } else if (shape.type == ShapeType::Rectangle) {
            // расчёт площади прямоугольника
            return width * height;
        }
        return 0.0;
    }
};

При добавлении новой фигуры (например, треугольника) придётся изменять AreaCalculator.

Хороший пример (через полиморфизм):

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();
    }
};

Теперь для добавления новой фигуры достаточно создать новый класс-наследник, не трогая AreaCalculator.

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

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

Классическое нарушение (прямоугольник-квадрат):

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 = w;  // нарушение LSP!
    }
    void setHeight(int h) override {
        width = h;
        height = h;  // нарушение LSP!
    }
};

Если клиентский код ожидает Rectangle, передача Square сломает логику.

Решение: Сделать общий абстрактный класс Shape или использовать композицию:

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

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; }
};

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

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

Плохой пример:

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

class Robot : public Worker {
public:
    void work() override { /* работает */ }
    void eat() override { throw std::logic_error("Robot can't eat"); }
    void sleep() override { throw std::logic_error("Robot can't sleep"); }
};

Хороший пример (разделение интерфейсов):

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

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

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

class Human : public Workable, public Eatable, public Sleepable {
public:
    void work() override { /* работает */ }
    void eat() override { /* ест */ }
    void sleep() override { /* спит */ }
};

class Robot : public Workable {
public:
    void work() override { /* работает */ }
};

Теперь Robot не содержит пустых или выбрасывающих исключения методов.

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

Суть: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.

Плохой пример:

class MySQLDatabase {
public:
    void save(const std::string& data) {
        // сохранение в MySQL
    }
};                

Блоги

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