Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Архитектура JS: Предметно-ориентированное проектирование

При разработке приложений с богатой предметной областью во весь рост встаёт вопрос о том, как правильно организовать код приложения, а если смотреть шире, то какую выбрать архитектуру. О том, какие варианты может предложить вам индустрия мы сейчас и поговорим, но перед тем, как я расскажу про существующие подходы, важно запомнить несколько вещей.

Не существует единственного верного подхода при организации вашего приложения. Известные подходы — всего лишь видение конкретных людей для конкретных ситуаций и конкретных стеков (язык + инструментарий). Любая хорошая архитектура базируется на фундаментальных законах и принципах. Большую часть из них вы уже знаете:

  • Изоляция побочных эффектов;
  • Хорошая абстракция (абстракция данных, композиция, разделение);
  • Сильные барьеры (между абстракциями);
  • Слабые связи (возможность замены/независимого развития).

Из популярного можно выделить следующие словосочетания:

  • The Clean Architecture
  • Onion Architecture
  • Hexagonal Architecture

Все эти архитектуры сводятся так или иначе к тому, что наше приложение представляет из себя набор слоёв (тех самых абстракций), которые связаны друг с другом определённым образом и отвечают за определённые аспекты системы. Лучше всего начать анализ с красивой картинки:

architecture

Начать стоит с того, что: Фреймворк — это не ваше приложение. Остановитесь на секундочку и хорошо обдумайте фразу. В типичных веб-приложениях фреймворк определяет вообще всё. Приложение на 100% переплетается с ним и становится его частью. Программист начинает мыслить в рамках возможностей фреймворка и его ограничений, и в его голове появляются несуществующие причинно-следственные связи.

Да, конечно, сложно (и не нужно) делать абсолютную изоляцию, но и всегда нужно проводить чёткую грань между приложением и тем фреймворком, который вы используете.

Домен

Первым и базовым слоем в приложении является Домен. Это реализация вашей модели предметной области. Чистая бизнес-логика без намёка на инфраструктуру.

Вот что обычно характеризует домен:

  • Чистый код (pure)
  • Plain Old X Object (POXO)
  • Бизнес-логика
  • Валидация

POXO - это обобщённое название, которое в каждом конкретном языке приобретает своё собственное имя. В Java POJO, в Ruby PORO, и так далее. Этой аббревиатурой описывают объекты, которые построены исключительно на возможностях самого языка, без дополнительных абстракций. Так подчёркивается, что домен не использует внешних библиотек, которые влияют на его организацию. Не надо фанатично относиться к этой идее. В некоторых языках сформировались свои правила, и они идут вразрез с общими концепциями.

Персистентность

Реализовать логику только половина дела. В конце концов нужно сохранить наши изменения. Казалось бы, что эта часть должна быть самой простой, но нет. Состояние, его изменение и поддержка целостности настолько сложная история, что придуманы огромные и сложные фреймворки, называемые ORM. Обычно они построены вокруг двух самых распространённых паттернов:

  • ActiveRecord
  • DataMapper

В этом курсе мы их не рассматриваем, но в реальной жизни вам придётся с ними столкнуться.

Репозиторий

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

const film = new Film(name, duration);
repository = new FilmRepository();
repository.save(film);

repository.find(film.id); // film
repository.find(unknownId); // Boom!

Инфраструктура

Именно в эту категорию попадает фреймворк, UI и вообще любая прикладная история. На картинке этот слой находится на самой внешней стороне. Из него происходит отправка электронных писем, смс, и выполняется так называемая логика приложения. Например, перенаправление на определённую страницу после создания какой-то сущности.

Сервисы

Помните диаграмму вариантов использования? Вот именно эти варианты и являются единственным способом изменения состояния вашего приложения. Ваш домен обрастает слоем так называемых сервисов. Каждый сервис представляет собой набор функций, имитирующих бизнес-сценарии, например, "добавить в друзья", "поставить лайк".

class CinemaService {
  createFilm(name, duration) {
    const film = new Film(name, duration);
    this.FilmRepository.save(film);
    return film;
  }
}

Инфраструктурный слой является главным пользователем вашего слоя сервисов. Сервисы могут вызываться в ui, в контроллерах, в асинхронных обработчиках. Слой сервисов настолько важен сам по себе, что Мартин Фаулер описывает его как шаблон проектирования Service Layer

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

Входными данными в функции сервиса не могут быть сущности предметной области. Причина такого правила очень проста. Сервисы – слой поверх предметной области, он инкапсулирует в себе все сценарии. Если сущности окажутся снаружи, то логика становится размазанной между слоями (потекла абстракция), пропадает изоляция. Но так сделать не всегда возможно. Иногда это связано с устройством конкретных фреймворков, которые не дают нормально абстрагировать предметную область от инфраструктуры. В такой ситуации не стоит бороться насмерть за концептуальную чистоту, идите на компромиссы.

То же самое касается выходных данных. В теории, отдавать наружу сущности нельзя по той же причине, по которой нельзя ими оперировать вне сервисов. Так как после возврата крайне просто начать ей оперировать, что сразу повлечёт за собой размазывание логики по слоям. Вместо сущности, как правило, отдают специальный "Data Transfer Object". В отличие от сущности он не содержит поведения и используется исключительно как контейнер для чтения.

DTO - Используется для передачи данных между подсистемами приложения. DTO, в отличие от business object, не должен содержать какого-либо поведения

  • Неизменяемый
  • Просто данные

Опять же, чтобы не усложнять, в тех системах, где нет готовых механизмов для трансляции, возвращают и сущности, но на уровне соглашений используют их исключительно как DTO.

И последнее по списку, но не последнее по важности, не вызывайте сервисы из сервисов. Если появляется общий код, то выносите общую функциональность, но не позволяйте самому сервису начинать мешаться с доменом. Последнее означает то, что если сервисы начинают использовать внутри себя сервисы (тот же или другие), то с большой вероятностью происходит нарушение принципа одного уровня абстракции. Сервисы — слой поверх домена, а это значит, что на одном уровне нельзя использовать и домен, и сервис.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»