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) — Принцип инверсии зависимостей