Что такое DDD (Domain-Driven Design) и каковы принципы его применения в Go?

Ответ

Domain-Driven Design (DDD) — это подход к разработке программного обеспечения, который фокусируется на предметной области (домене) и бизнес-логике. Главная цель — смоделировать сложный домен в коде, используя терминологию и концепции, понятные как разработчикам, так и экспертам в предметной области.

DDD особенно полезен в больших и сложных проектах, где простая CRUD-архитектура не справляется.

Ключевые концепции DDD

  1. Ubiquitous Language (Единый язык): Создание общего языка, который используется всеми участниками проекта (разработчиками, менеджерами, экспертами). Этот язык отражается в коде: в названиях классов, методов, модулей.

  2. Bounded Context (Ограниченный контекст): Четкое определение границ, в рамках которых модель и Единый язык имеют смысл. Например, в контексте "Продажи" Товар может иметь цену и скидку, а в контексте "Склад" — вес и местоположение.

  3. Layers (Слои): Классическая слоистая архитектура в DDD:

    • Domain Layer: Сердце приложения. Содержит бизнес-логику, сущности, агрегаты. Не зависит от других слоев.
    • Application Layer: Оркестрирует выполнение бизнес-сценариев (use cases), делегируя работу объектам домена.
    • Infrastructure Layer: Технические детали: работа с базами данных, файловой системой, внешними API. Реализует интерфейсы, определенные в доменном и прикладном слоях.
    • Presentation/UI Layer: Взаимодействие с пользователем (API-эндпоинты, веб-интерфейс).

Применение в Go (Тактические паттерны)

Поскольку в Go нет классов, паттерны DDD реализуются с помощью структур и интерфейсов.

  • Entity (Сущность): Объект с уникальным идентификатором и жизненным циклом. В Go это структура с полем ID.

    type User struct {
        ID    uuid.UUID
        Email string
        // ... другие поля
    }
  • Value Object (Объект-значение): Неизменяемый объект без идентификатора, характеризуемый своими атрибутами (например, Money, DateRange).

    type Money struct {
        Amount   decimal.Decimal
        Currency string
    }
  • Aggregate (Агрегат): Кластер из связанных сущностей и объектов-значений, который рассматривается как единое целое. У агрегата есть корень (Aggregate Root), который является единственной точкой входа для изменения агрегата.

  • Repository (Репозиторий): Интерфейс для доступа к агрегатам, имитирующий коллекцию объектов в памяти. Он скрывает детали хранения (SQL, NoSQL).

    // В доменном слое (domain/user.go)
    package domain
    
    type UserRepository interface {
        FindByID(ctx context.Context, id uuid.UUID) (*User, error)
        Save(ctx context.Context, user *User) error
    }
    
    // В инфраструктурном слое (infrastructure/postgres_user_repo.go)
    package infrastructure
    
    type PostgresUserRepo struct { db *sql.DB }
    
    func (r *PostgresUserRepo) FindByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
        // ... логика запроса к PostgreSQL
    }

Преимущества и недостатки в Go

  • Преимущества: Четкая структура, тестируемость, масштабируемость, бизнес-логика изолирована от технических деталей.
  • Недостатки: Может быть избыточным для простых CRUD-приложений. Требует дисциплины и глубокого понимания предметной области.

Ответ 18+ 🔞

Давай разберем эту вашу DDD, как есть, без соплей. Сидишь ты такой, пишешь очередной микросервис, а там бизнес-логика такая, что волосы дыбом. CRUD'ом уже не отделаешься, понимаешь? Тут-то и всплывает этот ваш Domain-Driven Design, или, как я его называю, «Дай Денег Домой» — потому что без него в больших проектах просто пиздец.

Слушай сюда, суть в чём. Всё крутится вокруг предметной области — этого вашего «домена». Не вокруг баз данных, не вокруг фреймворков, а вокруг того, чем бизнес, блядь, дышит. Продажи, склад, доставка — вот это вот всё. И главная фишка — говорить на одном языке с заказчиком. Не «создай запись в таблице users», а «зарегистрируй нового клиента». Этот язык потом прямо в код ложится, и все всё понимают. Красота, ёпта!

Основные киты, на которых всё держится

  1. Единый язык (Ubiquitous Language). Это когда ты, твой тимлид и этот менеджер с галстуком, который вечно «просит кнопочку», говорите одними и теми же словами. «Заказ», «Товар», «Резервирование». И в коде у тебя не OrderDTO, а domain.Order. Прям вот так, без подвохов.

  2. Ограниченный контекст (Bounded Context). Вот это, блядь, самое важное! Чтобы не было пиздеца, когда один «Пользователь» в двадцати сервисах означает разное. В контексте «Доставки» у «Пользователя» есть адрес, а в контексте «Оплаты» — номер карты. Разделили, разграничили — и жить стало проще. Каждый контекст — это как бы отдельная вселенная со своими правилами.

  3. Слои (Layers). Архитектура, мать её. Всё по полочкам:

    • Домен (Domain Layer): Святая святых. Тут живёт чистая бизнес-логика, мозг проекта. Никаких баз данных, никаких HTTP — только правила предметной области. Не зависит ни от кого, гордый и независимый слой.
    • Приложение (Application Layer): Дирижёр. Получает команду (типа «Оформить заказ»), бегает по репозиториям, достаёт агрегаты, вызывает у них методы — оркестрирует процесс. Но саму логику не содержит, блядь, это важно!
    • Инфраструктура (Infrastructure Layer): Чернорабочий. Всё, что связано с внешним миром: PostgreSQL, Redis, Kafka, отправка email. Реализует интерфейсы, которые ему сверху спустили.
    • Представление (Presentation Layer): Лицо проекта. REST API, GraphQL, gRPC — всё, что общается с пользователем или другими сервисами.

Как это в Go приткнуть? (Тактические штуки)

В Го классов нет, но кто сказал, что мы не справимся? Всё на структурах и интерфейсах.

  • Сущность (Entity): Объект, у которого есть ID, и он меняется со временем. Как человек — сегодня имя одно, завтра женился, фамилию поменял, а ID в паспорте тот же.

    type Customer struct {
        ID   uuid.UUID // Вот он, король, уникальный идентификатор
        Name string
        // ... остальные поля
    }
  • Объект-значение (Value Object): Неизменяемая хуйня без ID. Характеризуется своими полями. Два объекта с одинаковыми полями — неотличимы. Как деньги: 100 рублей есть 100 рублей, хоть в кошельке, хоть на карте.

    type Address struct {
        Street string
        City   string
        ZIP    string
    }
    // Методы для сравнения по полям, а не по ссылке!
  • Агрегат (Aggregate): Вот тут начинается магия. Это группа связанных сущностей, которую мы трогаем как единое целое. У агрегата есть корень (Aggregate Root) — главная сущность, через которую идёт всё общение. Например, Заказ (корень) и его Позиции. Меняешь позиции только через методы заказа. Так сохраняется целостность, блядь!

    type Order struct { // Агрегат, корень
        ID         uuid.UUID
        CustomerID uuid.UUID
        Items      []OrderItem // Сущности внутри агрегата
        Status     OrderStatus
    }
    func (o *Order) AddItem(productID uuid.UUID, quantity int) error {
        // Вся валидация и бизнес-правила тут!
        if o.Status != StatusDraft {
            return domain.ErrOrderAlreadyConfirmed
        }
        // ... добавляем позицию
    }
  • Репозиторий (Repository): Абстракция над хранилищем. Работаешь с ним, как с коллекцией в памяти: «Дай мне заказ с ID 123», «Сохрани этот заказ». А уж как он там, в PostgreSQL или в MongoDB, сохраняется — тебя, в доменном слое, не ебёт. Это забота инфраструктуры.

    // ДОМЕННЫЙ СЛОЙ (domain/order_repository.go) - объявляем контракт
    package domain
    
    type OrderRepository interface {
        FindByID(ctx context.Context, id uuid.UUID) (*Order, error)
        Save(ctx context.Context, order *Order) error
    }
    
    // ИНФРАСТРУКТУРНЫЙ СЛОЙ (infra/postgres_order_repo.go) - реализуем
    package infrastructure
    
    type PostgresOrderRepo struct {
        db *sql.DB
    }
    
    func (r *PostgresOrderRepo) FindByID(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
        // Тут уже твой любимый SQL, сканирование строк и прочая жесть
        const query = `SELECT id, customer_id, status FROM orders WHERE id = $1`
        row := r.db.QueryRowContext(ctx, query, id)
        // ... маппим в domain.Order
    }
    // Application Layer будет зависеть только от интерфейса domain.OrderRepository

Итог: зачем это всё?

Плюсы, блядь:

  • Чёткость. Код отражает бизнес, а не таблицы в БД. Новый разработчик входит в проект и сразу понимает, что происходит.
  • Тестируемость. Доменную логику можно тестировать в полной изоляции, мокая репозитории. Это охуенно.
  • Защита от дурака. Агрегаты сами следят за своими инвариантами. Нельзя просто так взять и изменить статус заказа, минуя все проверки.
  • Масштабируемость. Ограниченные контексты позволяют раскидывать систему на независимые сервисы (микросервисы, если угодно).

Минусы, ёпта:

  • Оверкилл. Для простой админки, где надо CRUD над пятью таблицами — это стрельба из пушки по воробьям. Только время потратишь.
  • Сложность. Надо думать, блядь. Глубоко вникать в домен. Не все к этому готовы, многие привыкли «давайте просто сделаем эндпоинт».
  • Бойлерплейт. Кода становится больше. Но это, как говорится, плата за надёжность.

Вывод: Если проект — это «Hello, World», забей хуй. Если же там реальная бизнес-логика, сложные процессы и жить этому проекту больше полугода — DDD твой друг, товарищ и брат. Просто не превращай его в религию, а используй как инструмент. Всё, чих-пых!