was successfully added to your cart.

Корзина

Плавненько от MVC к DDD Light. Часть 1. Диета жирной Модели.

Предисловие

В процессе технического аудита (LVL UP) я часто сталкиваюсь с этой проблемой, которая делает код нечитабельным куском дерьма софтины… нет, все-таки, дерьма.

Если ваш проект больше маленького (от 3-х месяцев разработки и с дальнейшей поддержкой), вам будет это полезно:

В чем цимус

Я делю понятие fat (толстая) / smart (умная) / rich (богатая) Model и понятие obese (жирная) Model. Вот об этой разнице и поговорим.

Вкратце, Модель (Model) – это объект, который объединяет данные и логику какой-то сущности, которыми мы манипулируем в рамках нашего приложения.

Например, модель Пользователь. У нее могут быть Пароль и Email (данные), а еще Пользователь устанавливает себе новый пароль (логика).

Fat (толстая) / Smart (умная) / Rich (богатая) Model (далее Rich Model или RM) – утрировано, это когда все модификации свойств модели происходят через вызов ее же методов.

// Rich model
class User {
  // Props
  email: string
  password: string
  
  // Logic
  setNewPassword(newPassword: string): void {
    if (newPassword === this.pasword) {
      	throw new Error("New password must not be the same")
    }
    this.password = newPassword
  }
}

Slim (тонка) / Anemic (анемичная) Model – утрировано, это когда все модификации модели происходят через прямое присваивание ее свойств в других местах кода.

// Slim model
class User {
  // Props
  email: string
  password: string
}

class UserService {
  setNewPassword(user: User, newPassword: string): void {
    if (newPassword === user.pasword) {
      	throw new Error("New password must not be the same")
    }
    user.password = newPassword
  }
}

Я всегда ЗА Rich Model, потому что она отлично соответствует принципам Tell Don’t Ask и Law of Demeter, которые я считаю основополагающими для сохранения принципов SOLID.

С таким определением Модели нет никаких проблем, но часто разработчики начинают связывать ее с Базой Данных (БД). Такая Модель превращается в Active Record и уже здесь начинают появляться проблемы (хотя, конечно, жить можно).

А еще хуже, когда Модель становиться Obese (жирной) – это когда туда начинают еще добавлять логику приложения.

Например, добавляют метод sendRegistrationConfirmation, который отправляет email об успешной регистрации (и это не шутка, я часто такое видел).

Следующая стадия ожирения наступает, когда Модель начинает отвечать за другие Модели. Пример:

У Пользователя есть Визитка. При изменении ФИО у Пользователя, нужно поменять ФИО и в Визитке. Пользователю добавляют метод changeFIO внутри которого подтягивают сущность Визитки и устанавливают у нее новое ФИО. То есть, теперь Пользователь модифицирует и сущности Визитки.

Это может быть нормально, если Визитка является «дочерней» / «подконтрольной» сущностью по отношению к Пользователю.

Вишенкой же на торте становиться то, что при изменении ФИО в Визитке, надо поменять ФИО Пользователя. И вот тут Визитке добавляют метод changeFIO, который меняет и ФИО Пользователя…

Любу циклическую / двунаправленную логику очень тяжело поддерживать, понимать и гибко изменять.

SOLID? Не, не слышали

Короче, по итогу наша Модель (1) содержит данные, (2) свою бизнес-логику, (3) логику приложения, (4) отвечает за работу с БД (Active Record), (5) так еще и двунаправленную логику других Моделей.

Это тяжело писать, понимать, поддерживать, рефакторить и практически невозможно в нормальные сроки покрывать тестами.

Просто… пиздец… И самое смешное – это очень распространенный пиздец.

Так что делать?

Нам нужно вынести по максимуму из Модели, начнем:

Выносим логику приложения

Сначала убираем всю логику приложения (отправки мейлов / нотификаций, формирование jwt, запросы в сторонние сервисы) в Контроллер / Сервисы.

Да, это плохой вариант, но это первый шаг, который нужно сделать к светлому будущему.

Выносим БД

Чтобы отвязать БД от Моделей (уйти от паттерна Active Record), нам нужен Data Mapper (DM).

Паттерн Data Mapper берет 2 сущности и мапит их друг к другу. Например сущность Пользователя из вашего приложения и сущность Пользователя из БД / стороннего API (например Auth0) / чего угодно.

Именно DM позволяет качественно абстрагировать работу с источником данных (Data Access Layer – DAL).

Вариантов DM много, но вот самые распространенные:

  1. DAO – вариант, но гавенный
  2. Repository – а вот это уже тема (а если добавить Query Object (QO) – то просто агонь 🔥)

Сама концепция утверждает, что в Repository можно использовать любое количество DAO, но в DAO нельзя использовать Repository, поэтому Repository находиться на более высоком слое абстракции, что дает ему преимущество.

Где посмотреть примеры Repository:

  1. Библиотека DDDL для JS и TypeScript
    1. Интерфейс Repository
    2. Пример обобщенного класса для knex
    3. Пример использования с knex
    4. Пример обобщенного класса для Objection
    5. Пример использования с Objection
  2. Еще для JS / TS: TypeORM, MirkoORM
  3. Для PHP Doctrine
  4. Для C# EF

Я вернусь к вопросу описания Repository в отдельной статьей с примерами.

Подписывайтесь на телегу, чтобы не пропустить анонсы новостей.

P.S.

История с Репозиторием опциональная, в отличии от «выноса логики приложения» и «однонаправленной иерархии».

Однонаправленная иерархия

Чтобы поправить ситуацию с циклическими / двунаправленными зависимостями без СМС и регистрации надо…

Догадались?

Правильно! Сделать их однонаправленными, построив иерархию.

Суть заключается в том, что только методы User могут менять Profile напрямую и Visit Card через Profile. При этом ни Visit Card, ни Profile не имеют права менять User.

В MVC очень редко достаточно полноценно рассматривается этот вопрос (даже не знаю названия концепции из MVC, которая устанавливала правила построения таких иерархий).

А вот в DDD (Light) об этом написано много. И в частности, чтобы понять как правильно строить подобные деревья зависимостей сущностей надо изучить паттерны Entity (сущность) и Aggregate (агрегат).

Я уже почти дописал статьи на тему этих патернов и скоро выпущу их + делаю курс, где пошагово все расскажу. А пока смотрите вот это:

  1. Интерфейсы Entity
  2. Интерфейсы Aggregate
  3. Примеры Аггрегатов
    1. Папка с Агрегатом Пользователя

Подписывайтесь на телегу, чтобы не пропустить анонсы новостей.

По итогу

Теперь в Модели у нас лежит только ее свойства, логика модификации этих свойств и при этом все Модели построены в однонаправленные иерархические деревья. Это чисто и приятно. Вся логика приложения и работа с БД ушла на другие слои. На такую сущность приятно смотреть и работать с ней.

Если что-то непонятно по статье, пишите в телегу (не стесняйтесь).

Теперь ждем статью «Плавненько от MVC к DDD Light. Часть 2. Диета жирного Контроллера.»

Гораздо больше контента и развлечений в Telegram-канале