Разделение забот не должно быть скучным

Программное обеспечение постоянно меняется, и один из аспектов, влияющих на определение качества кода, заключается в том, насколько легко его изменить. Но почему это так?

… Если вы боитесь что-то изменить, это явно плохо спроектировано.

- Мартин Фаулер

Разделение проблем и ответственности

«Соберите воедино то, что меняется по одним и тем же причинам. Разделяйте те вещи, которые меняются по разным причинам ».

Будь то вещи, функции, классы или модули, они могут применяться к принципу единой ответственности и разделения ответственности. Разработка программного обеспечения на основе этих принципов начинается с архитектуры.

Архитектура

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

Вы заметили, что эти три обязанности не относятся к одной и той же категории? Это связано с тем, что они принадлежат к разным слоям, которые, в свою очередь, можно разделить на концепции. Согласно приведенному выше примеру, «сохранить пользователя в базе данных» относится к концепции «пользователя», а также к уровню, который взаимодействует с базой данных.

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

Уровень домена

На этом уровне мы можем определять единицы, которые играют роль сущностей и бизнес-правил и имеют прямое отношение к нашему домену. Например, в приложении пользователей и команд у нас, вероятно, будет сущность User, сущность Team и JoinTeamPolicy, чтобы ответить, может ли пользователь присоединиться к данной команде или нет.

Это наиболее изолированный и важный уровень нашего программного обеспечения, и он может использоваться на уровне приложений для определения вариантов использования.

Уровень приложения

Уровень приложения определяет фактическое поведение нашего приложения, таким образом, отвечая за взаимодействие между элементами уровня домена. Например, у нас может быть JoinTeam вариант использования, который получает экземпляры User и Team и передает их JoinTeamPolicy; если пользователь может присоединиться, он делегирует ответственность за сохранение уровня инфраструктуры.

Уровень приложения также может использоваться как адаптер для уровня инфраструктуры. Допустим, наше приложение может отправлять электронные письма; класс, отвечающий за связь напрямую с почтовым сервером (назовем его MailChimpService), принадлежит уровню инфраструктуры, но тот, который фактически отправляет электронные письма (EmailService), принадлежит уровню приложения и использует MailChimpService внутренне. Следовательно, остальная часть нашего приложения не знает подробностей о конкретных реализациях - она ​​знает только то, что EmailService может отправлять электронные письма.

Уровень инфраструктуры

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

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

Идея здесь в том, что интерфейс репозитория принадлежит уровню домена, а реализация, в свою очередь, принадлежит уровню инфраструктуры - т.е. домен знает только о методах и параметрах, принимаемых репозиторием. Это делает оба уровня более гибкими, даже в том, что касается тестирования! Поскольку JavaScript не реализует концепцию интерфейсов, мы можем представить свой собственный и на основе этого создать конкретную реализацию на уровне инфраструктуры.

Слой входных интерфейсов

Этот уровень содержит все точки входа нашего приложения, такие как контроллеры, интерфейс командной строки, веб-сокеты, графические пользовательские интерфейсы (в случае настольных приложений) и так далее.

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

NodeJS и разделение проблем

Хорошо, после всей этой теории, как это работает в приложении Node? По правде говоря, некоторые шаблоны, используемые в многоуровневых архитектурах, очень хорошо подходят для мира JavaScript!

NodeJS и уровень домена

Уровень домена на Node может состоять из простых классов ES6. Существует множество модулей ES5 и ES6 + для помощи в создании сущностей предметной области, например: Структура, Состояние амперсанда, tcomb и ObjectModel.

Давайте посмотрим на простой пример с использованием Structure:

Обратите внимание, что в нашем списке нет Backbone.Model или таких модулей, как Sequelize и Mongoose, поскольку они предназначены для использования на уровне инфраструктуры. общаться с внешним миром. Следовательно, остальной части нашей кодовой базы даже не нужно знать об их существовании.

NodeJS и прикладной уровень

Варианты использования относятся к уровню приложения и, в отличие от обещаний, они могут иметь результаты, выходящие за рамки успеха и неудачи. Хорошим шаблоном Node для таких случаев является эмиттер событий. Чтобы использовать его, мы должны расширить класс EventEmitter и генерировать событие для каждого возможного результата, тем самым скрывая тот факт, что наш репозиторий внутренне использует обещание:

Таким образом, наша точка входа может выполнить вариант использования и добавить слушателя для каждого результата, вот так:

NodeJS и уровень инфраструктуры

Реализация уровня инфраструктуры не должна вызывать затруднений, но будьте осторожны, чтобы его логика не просочилась на вышележащие уровни!

Например, мы можем использовать модели Sequelize для реализации репозитория, который взаимодействует с базой данных SQL, и дать ему имена методов, которые не предполагают существование SQL нижний слой - такой как общий метод add в нашем последнем примере.

Мы можем создать экземпляр SequelizeUsersRepository и передать его зависимым элементам как переменную usersRepository, а зависимые элементы могут просто взаимодействовать с его интерфейсом.

То же самое можно сказать и о базах данных NoSQL, почтовых службах, механизмах очередей, внешних API и т. Д.

NodeJS и уровень входных интерфейсов

Есть много возможностей для реализации этого уровня в приложениях Node. Для HTTP-запросов наиболее часто используется модуль Express, но вы также можете использовать Hapi или Restify. Окончательный выбор сводится к деталям реализации, хотя изменения этого уровня не должны влиять на другие. Если переход с Express на Hapi каким-то образом подразумевает внесение соответствующих изменений, это признак взаимосвязи, и вам следует обратить пристальное внимание на то, чтобы исправить это.

Соединение слоев

Обсуждение одного уровня напрямую с другим может быть плохим решением и вызвать связь между ними. В объектно-ориентированном программировании обычным решением этой проблемы является внедрение зависимостей (DI). Этот метод заключается в том, чтобы зависимости класса принимались в качестве аргументов в его конструкторе, вместо того, чтобы требовать зависимости и создавать их экземпляры внутри самого класса - следовательно, создается так называемая инверсия управления.

Использование этого метода позволяет нам очень кратко изолировать зависимости класса, делая его более гибким и легким для тестирования, поскольку устранение зависимостей становится тривиальной задачей.

Для приложений Node есть хороший модуль DI под названием Awilix, который позволяет нам использовать DI, не привязывая наш код к самому модулю DI, поэтому нам не хочется использовать этот странный механизм внедрения зависимостей от Angular 1. Автор Awilix имеет серию сообщений, объясняющих внедрение зависимостей с помощью Node, которые стоит прочитать, а также введение о том, как использовать Awilix . Кстати, если вы планируете использовать Express или Koa, вам также следует взглянуть на Авиликс-Экспресс или Авиликс-Коа .

Практический пример

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

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

Дополнительная информация

Если вы хотите узнать больше о многоуровневых архитектурах и о том, как разделить проблемы, взгляните на следующие ссылки: