C++MiddleSystem design
Что такое принципы SOLID и как они применяются в C++?
SOLID в C++ реализуется через виртуальные функции (OCP, LSP), шаблоны и концепты (ISP, DIP), а также строгое разделение заголовков (SRP). Концепты C++20 дают статическую проверку интерфейсных контрактов.
Принципы SOLID в C++
S — Single Responsibility Principle
Класс должен иметь единственную причину для изменения.
#include <string>
#include <fstream>
// Нарушение: класс и хранит данные, и сериализует, и логирует
class UserBad {
public:
std::string name;
void saveToFile(const std::string& path); // SRP-нарушение
void log(); // SRP-нарушение
};
// Соблюдение: разделение ответственности
struct User { std::string name; int id; };
class UserRepository {
public:
void save(const User& u, const std::string& path) {
std::ofstream f(path);
f << u.id << " " << u.name << "\n";
}
};
O — Open/Closed Principle
Открыт для расширения, закрыт для изменения.
#include <memory>
#include <vector>
struct Shape {
virtual double area() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
double r;
explicit Circle(double r) : r(r) {}
double area() const override { return 3.14159 * r * r; }
};
struct Rectangle : Shape {
double w, h;
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
};
double totalArea(const std::vector<std::unique_ptr<Shape>>& shapes) {
double sum = 0;
for (const auto& s : shapes) sum += s->area();
return sum;
// Новую фигуру добавляем без изменения этой функции
}
L — Liskov Substitution Principle
// Нарушение: Rectangle vs Square — классический антипаттерн
struct Rectangle {
virtual void setWidth(int w) { w_ = w; }
virtual void setHeight(int h) { h_ = h; }
int area() const { return w_ * h_; }
protected:
int w_ = 0, h_ = 0;
};
// Square переопределяет setWidth/setHeight — нарушает LSP
// Решение: не наследовать Square от Rectangle
// Используйте независимые типы или концепты
I — Interface Segregation Principle
// Нарушение: один жирный интерфейс
struct IWorker {
virtual void work() = 0;
virtual void eat() = 0; // роботы не едят!
virtual void sleep() = 0;
};
// Соблюдение: мелкие интерфейсы
struct IWorkable { virtual void work() = 0; virtual ~IWorkable() = default; };
struct IFeedable { virtual void eat() = 0; virtual ~IFeedable() = default; };
struct Robot : IWorkable { void work() override { /* ... */ } };
struct Employee : IWorkable, IFeedable {
void work() override { /* ... */ }
void eat() override { /* ... */ }
};
D — Dependency Inversion Principle + C++20 Concepts
#include <concepts>
// Абстракция через concept (статическая DIP)
template<typename T>
concept Logger = requires(T t, std::string_view msg) {
{ t.log(msg) } -> std::same_as<void>;
};
struct ConsoleLogger {
void log(std::string_view msg) {
std::puts(msg.data());
}
};
struct FileLogger {
void log(std::string_view msg) { /* write to file */ }
};
template<Logger L>
class Service {
L& logger_;
public:
explicit Service(L& l) : logger_(l) {}
void doWork() {
logger_.log("Starting work");
}
};
// Использование
ConsoleLogger cl;
Service service(cl); // CTAD, нет виртуальных вызовов
Подводные камни
- Виртуальное наследование для реализации ISP даёт накладные расходы — vtable на каждый интерфейс. C++20 concepts дают нулевую стоимость через статический полиморфизм.
- LSP-нарушения в иерархиях наследования часто обнаруживаются не в compile-time, а в runtime. Концепты и статические проверки (
static_assert) позволяют сдвинуть ошибки влево. - Злоупотребление DIP через интерфейсы создаёт избыточную косвенность — в performance-sensitive C++ коде предпочитайте шаблоны и concepts над виртуальными функциями.
- SRP в C++ осложняется тем, что разбиение на мелкие классы увеличивает количество заголовков и время компиляции — баланс между чистотой и build performance важен.
- Open/Closed через виртуальные функции требует stable ABI — добавление нового чисто виртуального метода в базовый класс ломает всех наследников.
- Шаблонная DIP (concepts) не поддерживает runtime-подмену реализации — для мокирования в тестах всё равно нужны виртуальные функции или type erasure (std::function, std::any).
Common mistakes
- Объяснять SOLID в C++ только по синтаксису, без жизненного цикла и стоимости.
- Игнорировать ошибки, null/empty состояния, порядок инициализации или режим сборки.
- Давать пример, который работает в демо, но ломается при изменении владельца ресурса.
- Показывать сырой указатель без объяснения владельца и момента освобождения.
What the interviewer is testing
- Кандидат формулирует точную модель для SOLID в C++, а не только определение.
- Пример компилируем, безопасен по lifetime и соответствует версии технологии.
- Названы trade-off, ограничения и диагностируемые симптомы ошибки.