Это первая часть из пяти частей.

Часть 0 была введением, теперь давайте перейдем к нашим первым реальным вопросам о нашем REST API, написанном на JavaScript под Node.js с сервером Koa, базой данных Mongodb и Mongoose ODM.

Как организовать код, назвать файлы и написать простой, читаемый, интуитивно понятный код, руководствуясь хорошими руководящими принципами кодирования?

Структура папки

Я говорил об использовании подхода Domain-Driven Design (DDD) или, по крайней мере, вдохновлялся им. Это означает, что сначала наш код будет организован на 4 уровня:

  • уровень домена, содержащий наши классы сущностей и связанную с ними бизнес-логику,
  • слой приложения, содержащий наши сервисы приложения: он будет как можно более тонким,
  • уровень инфраструктуры, содержащий конкретные реализации, необходимые нашему приложению (доступ к данным, шифрование, ведение журнала… вы называете это),
  • Уровень представления, содержащий код нашего сервера, маршруты и контроллеры для интерфейса HTTP, а также наши команды для интерфейса CLI.

Структура папок будет отражать этот подход:

config/
dist/
docs/
logs/
src/
  app/
    content/
    listeners/
    user/
    utils/
    app.js
  domain/
  infra/
    database/
    encryption/
    logger/
    utils/
  interfaces/
    http/
      api/
      auth/
      cache/
      errors/
      logger/
      routes/
      server.js
    console/
      commands/
      cli.js
  container.js
  index.js
test/

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

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

index.js будет иметь следующий простой код (обратите внимание, как мы используем контейнер, чтобы получить то, что хотим):

src / index.js

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

app.js (служба app из нашего контейнера) будет иметь следующий простой код (здесь мы снова используем внедрение зависимостей):

src / app / app.js

Наша служба db на данном этапе может быть чем угодно. Наш сервер тоже. Им просто нужно реализовать определенные методы и иметь определенные свойства (например, logger). В этом вся прелесть.

Теперь зачем нам внедрять наш контейнер в метод start () службы server? Поскольку нашему серверному коду, чтобы получить все остальные модульные службы, потребуется тот же контейнер, из которого мы получили нашу службу app (подробнее об этом в Части 2 ), и мы хотим импортировать контейнер только в одном месте и желательно на самом высоком уровне.

Именование файлов (и классов)

В типичном модуле приложения я рекомендую следующее:

user/
  create.js
  get.js
  remove.js
  search.js
  update.js

Что будет отображаться в следующие классы в нашем модуле user:

create.js: class CreateUser {}
get.js: class GetUser {}
remove.js: class RemoveUser {}
search.js: class SearchUser {}
update.js: class UpdateUser {}

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

Но это не означает, что мы никогда не создадим (упрощенный) класс ManageUser, и для этого есть веские причины, как мы увидим чуть позже.

Да, получается много файлов, но на самом деле это нормально. Подойдет и для модульного тестирования.

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

Стиль кодирования

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

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

Вот несколько правил Eslint, которые я рекомендую:

"indent": ["error", 2, { "SwitchCase": 1 } ],
"linebreak-style": ["error", "unix"],
"comma-spacing": ["error", { "before": false, "after": true }],
"space-before-function-paren": ["error", "always"],
"no-multi-spaces": 2,
"no-trailing-spaces": 2,
"quotes": ["error", "single"],
"semi": ["error", "never"],
"one-var": ["error", "never"],
"no-return-await": 2

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

При написании JavaScript часто возникают следующие вопросы:

1. Следует написать:

{ one, two }

or

{
  one,
  two
}

но если мы выберем последнее, я должен написать:

{ one }

or

{
  one
}

и в конечном итоге реальный вопрос заключается в том, сколько свойств объекта перед вставкой разрывов строк?

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

2. Следует ли мне вставлять разрыв строки после стрелки (= ›) или при написании тернарных операций (?:). Опять же, я считаю это очень ситуативным, но вы можете следовать определенному правилу и для них.

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

Как минимизировать зависимости, внедрив сервисный контейнер для внедрения зависимостей?

Мы воспользуемся превосходной библиотекой Awilix и создадим контейнер служб, отвечающий за создание экземпляров наших классов (с помощью метода asClass ()), или предоставим уже созданные экземпляры служб как есть (с помощью asValue ( ) метод). Таким образом, у нас будет механизм для внедрения необходимых зависимостей приложения без их жесткого кодирования на уровне нашего приложения.

Наши службы приложений будут разрешаться каждый раз, тогда как другие службы будут кэшироваться (с помощью метода singleton ()).

Таким образом, следующий код container.js довольно прост:

src / container.js

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

container.cradle[serviceName]

Когда и как использовать события?

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

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

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

Для этого мы создадим событие в нашей службе C reateUser, которое может выглядеть примерно так:

src / app / user / create.js

Воспользовавшись преимуществом класса Node.js EventEmitter, от которого теперь наследуется наша служба (и фактически мы сделаем все наши службы такими).

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

Вот почему мы реализуем класс прослушивателя событий, UserListener, который будет приписывать новый значок массиву значков спонсора (конечно, в нашей модели домена объект User будет необходимо иметь свойство массива значков):

src / app / listeners / userListener.js

И, наконец, в нашем контроллере мы можем написать такой код:

Когда наше событие будет запущено, оно будет прослушано и соответствующим образом отреагировано.

Обратите внимание, что мы должны привязать функцию обработчика событий к ее экземпляру, чтобы убедиться, что последний работает правильно с правильным контекстом this. Это связано с тем, как работает JavaScript. В Части 5 мы увидим, как использовать декоратор @ autobind в нашей функции класса слушателя, чтобы избавиться от этой bind () вызовите и не только напишите более чистый код, но и получите большую гибкость в процессе.

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

Но что-то не так, не правда ли?

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

Один из способов справиться с этой проблемой - написать такой универсальный класс (о, здесь возвращается - хотя и гораздо более простая - версия ранее ужасной службы ManageUser):

Затем мы просто используем ManageUser.createUser (…) и т. Д. В нашем контроллере (см. Часть 2).

Конечно, вызовы bind () по-прежнему ужасны. Нам действительно нужно будет избавиться от них тем или иным способом в Части 5.

Теперь нужно ли нам использовать события каждый раз, когда нам нужно передавать данные из одной службы приложения в другую? Ответ - нет. Если работа с сущностью имеет последствия для объединенной сущности, есть другие способы распространить эффект, либо на уровне ORM (имеется в виду на уровне инфраструктуры, мы увидим это в части 2). или просто внедрив репозиторий объединенной сущности в службу нашей основной сущности.

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

Теперь вы, возможно, слышали о источнике событий g, который представляет собой гораздо более изощренный способ (и с которым легко ошибиться) использования событий. Часто это связано с подходом командного запроса / чтения (CQRS), которого мы здесь не придерживаемся, поэтому мы оставим это в стороне в этой серии.

В части 2 мы рассмотрим, как реализовать шаблон репозитория, отделить классы домена от моделей баз данных и добавить некоторую автоматическую логику сохранения в соответствии с доменом. Мы также поговорим о том, как настроить HTTP-сервер, написать правильные маршруты API и эффективно управлять контроллерами. Продолжение следует!