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