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, ограничения и диагностируемые симптомы ошибки.

Sources

Related topics