Domain-Driven Design в C++: архитектура для сложных проектов

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

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

Начать курс

Почему DDD — это не про скучные диаграммы, а про живую архитектуру

Когда проект на C++ перерастает размеры «игрушечного» приложения, разработчики часто сталкиваются с хаосом: бизнес-логика размазана по контроллерам, база данных диктует структуру классов, а новички боятся трогать legacy-код. Domain-Driven Design (DDD) — это не набор абстрактных правил, а практический инструмент, который помогает обуздать сложность. В этой статье мы разберём, как внедрить DDD в C++, используя конкретные примеры кода, и почему это спасёт ваш проект от «большого комка грязи».

Что такое DDD и зачем он C++ разработчику?

Domain-Driven Design (предметно-ориентированное проектирование) — это подход, где основное внимание уделяется модели предметной области, а не техническим деталям. В C++ это особенно актуально: язык даёт мощные инструменты (шаблоны, RAII, умные указатели), но без правильной архитектуры проект превращается в монолит, где изменение одной фичи ломает всё.

DDD предлагает два уровня:

  • Стратегическое моделирование: разделение системы на контексты (Bounded Contexts) и настройка их взаимодействия.
  • Тактическое моделирование: строительные блоки — Entity, Value Object, Aggregate, Repository, Domain Service.

Мы сосредоточимся на тактическом моделировании, так как именно оно даёт конкретные паттерны для кода на C++.

Слоистая архитектура: фундамент DDD в C++

DDD предполагает чёткое разделение на слои. Типичная структура для C++ проекта выглядит так:

  • Domain Layer (ядро): сущности, value object, агрегаты, доменные сервисы. Никаких зависимостей от фреймворков или внешних библиотек.
  • Application Layer: сценарии использования (use cases), оркестрация. Зависит только от Domain.
  • Infrastructure Layer: реализация репозиториев (SQL, Redis, файлы), работа с внешними API. Реализует интерфейсы из Domain.
  • Presentation Layer: пользовательский интерфейс (консоль, REST API, GUI).

Это не жёсткое правило, а рекомендация. Главное — зависимости направлены внутрь: внешние слои могут зависеть от внутренних, но не наоборот.

Пример: как не надо делать

// ❌ Плохо: бизнес-логика смешана с SQL
class UserController {
    void createUser(const std::string& name) {
        // SQL запрос прямо здесь
        db.execute("INSERT INTO users (name) VALUES (?)", name);
        // Логика проверки тоже здесь
        if (name.empty()) throw std::runtime_error("Name required");
    }
};

Пример: DDD-стиль

// ✅ Хорошо: чёткое разделение
class User : public Entity<User, int> {
    std::string name_;
public:
    explicit User(int id, std::string name) : Entity(id), name_(std::move(name)) {
        if (name_.empty()) throw std::invalid_argument("User name cannot be empty");
    }
    const std::string& name() const { return name_; }
};

// Интерфейс репозитория (Domain)
class IUserRepository {
public:
    virtual ~IUserRepository() = default;
    virtual void save(const User& user) = 0;
};

// Реализация (Infrastructure)
class SqlUserRepository : public IUserRepository {
    void save(const User& user) override {
        // Только SQL, без бизнес-логики
    }
};

Строительные блоки DDD на C++

Entity (Сущность)

Это объект с уникальным идентификатором, который сохраняется во времени. В C++ удобно сделать базовый шаблонный класс:

template<typename T, typename IdType>
class Entity {
    IdType id_;
public:
    explicit Entity(IdType id) : id_(std::move(id)) {}
    IdType id() const { return id_; }
    bool operator==(const Entity& other) const { return id_ == other.id_; }
    bool operator!=(const Entity& other) const { return !(*this == other); }
};

class Order : public Entity<Order, std::string> {
    // Бизнес-логика заказа
};

Value Object (Объект-значение)

Неизменяемые объекты, которые определяются своими атрибутами, а не идентификатором. Классический пример — деньги:

class Money {
    long long cents_; // Всегда в копейках, чтобы избежать проблем с float
public:
    Money(long long cents) : cents_(cents) {}
    Money operator+(const Money& other) const {
        return Money(cents_ + other.cents_);
    }
    bool operator==(const Money& other) const {
        return cents_ == other.cents_;
    }
    // Без сеттеров — только конструирование
};

Совет: используйте constexpr и noexcept где возможно — это улучшит производительность и безопасность.

Aggregate (Агрегат)

Группа сущностей и value object, которые рассматриваются как единое целое. У агрегата есть корень (Aggregate Root), через который происходит доступ к внутренним объектам.

class OrderAggregate {
    std::string orderId_;
    std::vector<OrderLine> lines_; // Value Object
    Money total_;
public:
    void addLine(const OrderLine& line) {
        // Бизнес-правило: нельзя добавить товар с нулевой ценой
        if (line.price() == Money(0)) {
            throw std::invalid_argument("Price must be positive");
        }
        lines_.push_back(line);
        total_ = total_ + line.totalPrice();
    }
    // Никаких геттеров для lines_ — только методы, выражающие бизнес-намерения
};

Repository (Репозиторий) и Domain Service

Репозиторий — это коллекция объектов для доступа к данным, но с точки зрения предметной области. Не путайте с DAO (Data Access Object) — репозиторий оперирует доменными объектами, а не таблицами.

// Интерфейс репозитория (Domain)
class IOrderRepository {
public:
    virtual std::optional<OrderAggregate> findById(const std::string& id) = 0;
    virtual void save(const OrderAggregate& order) = 0;
};

// Domain Service — когда логика не помещается в один агрегат
class PricingService {
public:
    Money calculateDiscount(const OrderAggregate& order, const Customer& customer) {
        // Бизнес-правила скидок
        if (customer.isPremium() && order.total() > Money(10000)) {
            return Money(order.total().cents() * 10 / 100); // 10% скидка
        }
        return Money(0);
    }
};

Реальный пример: система управления заказами

Соберём всё вместе. Представьте, что мы пишем микросервис для интернет-магазина на C++.

Шаг 1. Определяем Bounded Context

У нас есть контекст «Заказы» и контекст «Каталог товаров». Мы сосредоточимся на первом.

Шаг 2. Моделируем ядро

// Value                

Блоги

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