Почему не Inversify / Angular / NestJS?

При разработке модулей GraphQL мы пробовали разные подходы к безопасной инкапсуляции модулей. Мы создали решение, в котором вам не нужно будет использовать Dependency Injection при запуске, но после того, как вы достигнете определенного масштаба, вы можете использовать нашу отдельную библиотеку DI, чтобы облегчить разделение и сделать границы более строгими, только в момент, когда это действительно имеет смысл и помогает вам. Когда вы дойдете до этого момента, DI поможет вам в основном для простоты имитации поставщиков для тестирования и инкапсуляции для реализации, менее подверженной ошибкам.

На ранних этапах создания GraphQL-модулей у него было внутреннее Inversify для внедрения зависимостей в GraphQL-модулях. Inversify - это не зависящая от платформы библиотека внедрения зависимостей, написанная на JavaScript. К сожалению, Inversify не соответствовал нашим потребностям в модульном DI (внедрение зависимостей) по ряду важных причин, которые мы подробно рассмотрим в этой статье.

Вот почему мы реализовали нашу собственную платформо-независимую библиотеку внедрения зависимостей под названием @graphql-modules/di, которая не зависит от @graphql-modules/core и может использоваться сама по себе.

Он поддерживает поставщиков фабрик, классов и значений, которые могут иметь Symbol, string, number, function и object в provide определении, и он может адресовать конструируемые классы, фабричные функции и постоянные значения как внедренные значения. Он имеет некоторые функции, похожие на Angular's Dependency Injection, упомянутый в их документации. В этой статье мы объясним сходства и различия между этими решениями.

Начнем с принципов, которые мы хотели включить в нашу логику DI, а закончим сравнениями с Angular и NestJS.

Инкапсуляция

Пока мы все еще использовали Inversify, у нас был один GraphQLApp верхний модуль. Обычные модули GraphQL не могут импортировать друг друга, но могут быть импортированы этим GraphQLApp объектом. Также в этих модулях не было инкапсуляции, потому что все поставщики, схемы и преобразователи были объединены без какой-либо инкапсуляции. После этого мы решили обеспечить полный модульный подход и сделать все модульным. Теперь у каждого модуля есть собственный инжектор, действующая схема и контекст, который не зависит от его родительских модулей. Кроме того, каждый модуль создается с использованием контейнеров DI импортированных модулей, содержимого схемы и построителей контекста.

Если вы хотите узнать больше о инкапсуляции, прочтите об этом сообщение в блоге;



Например, если у вас есть DatabaseModule в вашем приложении, который должен использоваться во всем приложении, и он должен принимать настраиваемый поставщик, который будет определен пользователем. . Что вы будете делать в этих реализациях DI, так это создать поставщика, украсив его поставщиком с глобальной областью действия; затем поместите его в ApplicationModule. Однако это нарушит принцип инкапсуляции модульного подхода.

Обобщить; в то время как ваш DatabaseModule импортируется другими модулями на более низком уровне, и этот модуль будет использовать поставщика в своем родительском элементе. Он не должен знать, что его импортирует.

Мы обрабатываем это по-другому в модулях GraphQL, передавая конфигурацию, подчиняясь правилу инкапсуляции;

Три преимущества этого типа DI и модульной системы;

  • AppModule защищен от небезопасного вызова без действительной конфигурации, необходимой для внутреннего процесса (DatabaseModule); спасибо configRequired.
  • DatabaseModule защищен от небезопасного импорта без действительной конфигурации, необходимой для внутреннего процесса (SomeDbProvider); еще раз спасибо configRequired!
  • OtherModuleThatUsesDb защищен от небезопасного вызова или импорта без определения правильно настроенного модуля DatabaseModule.

Иерархия

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

  • Контейнер DI имеет FooProvider
  • Контейнер B DI имеет BarProvider
  • Контейнер C DI имеет BazProvider
  • Контейнер D DI имеет QuxProvider
  • A импортирует B
  • B импортирует D
  • B импортирует C

Например, в нашем DI произойдет следующий случай;

  • FooProvider нельзя внедрить внутри B, пока BarProvider может вводиться с помощью A; потому что у инжектора B есть только его поставщики, а также D и C (BazProvider и QuxProvider).

Как видно из сравнения с Inversify, Inversify может присоединить только один дочерний контейнер DI к родительскому DI. контейнер; и это не соответствовало бы нашему модульному подходу, основанному на иерархии. Например, вы не можете заставить B расширить одновременно D и C, оставив D и C инкапсулированный. Если вы объедините их все в один инжектор, как это делает Angular, D сможет получить доступ к поставщикам C без импорта C.

Провайдеры с ограниченной областью действия

В отличие от клиентского приложения, ваша система DI не должна иметь разные области действия, такие как область действия приложения и область действия сеанса. В нашей реализации мы можем определить поставщиков, которые имеют время жизни в течение одного сетевого запроса GraphQL, а не всей среды выполнения приложения; потому что некоторые провайдеры должны принадлежать одному клиенту. К сожалению, если вы используете существующие библиотеки DI, такие как Inversify, вам нужно найти сложные способы управления поставщиками с заданной областью действия.

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

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



Сравнение с другими реализациями DI фреймворков

Сравнение с Inversify

Чтобы обеспечить истинную инкапсуляцию; у нас должен быть инжектор / контейнер с несколькими дочерними элементами, и эти дети не должны знать друг друга и даже своего родителя. В Inversify у них есть что-то вроде того, что называется Hierarchical DI . В этой функции Inversify терминparent может быть неправильно понят. поскольку инжектор ищет своих собственных поставщиков, затем ищет другой инжектор, который определяется как parent в этой функции. Но нам нужно нечто большее. Мы хотим иметь несколько parent в соответствии с их логикой.

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

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

Сравнение с DI Angular

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

Наша реализация внедрения зависимостей более строгая с точки зрения инкапсуляции, чем в Angular.

Это пример верхнего модуля, написанного на Angular. Предположим, что FooProvider изначально реализован и определен внутри FooModule и используется BarModule. Когда FooProvider токен заменяется на MyFooProvider в области AppModule, BarModule также использует нашу MyFooProvider внутри своей логики. Это явно нарушает инкапсуляцию в модульном подходе, потому что Angular должен отправлять BarModule FooModule FooProvider реализацию вместо той, которая определена вне BarModule; потому что он не должен знать, что определено на более высоком уровне.

Однако модули GraphQL отправят BarModule правильный FooProvider, потому что BarModule импортирует FooModule, а не AppModule. Таким образом, MyFooProvider будет отправлен, если текущий инжектор находится в пределах AppModule.

В модулях GraphQL, если A импортирует B, а B импортирует C, A может получить доступ к области C. Но C не может получить доступ к областям B и A. Это позволяет нам создавать безопасно построенное приложение GraphQL и снижает вероятность ошибок. Вы можете узнать больше о иерархии и инкапсуляции в начале этой статьи.

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

Эта возможность не так важна для приложений Angular; потому что они работают в браузере, а не на сервере. Но все же вы можете заметить аналогичное поведение DI-модулей GraphQL, если вы лениво загружаете модуль Angular, используя свойство маршрутизатора loadChildren.

А библиотека DI Angular не различает области действия приложения и сеанса, потому что ей это не нужно. У приложения Angular есть время жизни, пока window не будет завершено путем его закрытия или обновления страницы; сеанс и среда выполнения приложения могут считаться одинаковыми в клиентском приложении. Вот почему наша реализация DI сложнее, чем в Angular, так как она должна обрабатывать несколько клиентов с помощью одного приложения. Вы можете узнать больше о Провайдерах с заданной областью действия в начале этой статьи.

Сравнение с реализацией DI NestJS

NestJS - это полноценный фреймворк на стороне сервера, модель-представление-контроллер, в котором реализованы все принципы фреймворка MVC Backend. предоставление GraphQL API - это только одна из его различных функций. Наша цель с модулями GraphQL состояла в том, чтобы создать набор инструментов и библиотек, которые отвечают потребностям экосистемы GraphQL и могут (и должны) использоваться пользователями NestJS.

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

По сравнению с целями Nest, GraphQL-Modules - это независимая от платформы библиотека, которую можно использовать даже с простым graphqljs пакетом без сервера; потому что он был разработан для одинаковой работы на всех платформах GraphQL Server, таких как Apollo, Yoga, graphql-express и т. д. Например, вы можете использовать источники данных Apollo с помощью graphql-express ; потому что модули GraphQL передают собственный механизм кеширования загрузчикам данных благодаря нашей независимой логике DI.

Результатом GraphQLModule является построитель контекста и схема, а GraphQLModule NestJS - это модуль NestJS, который предоставляет API с использованием Apollo.

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

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

Работать вместе

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

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

Все сообщения о модулях GraphQL

  1. Модули GraphQL - Масштабные модули GraphQL на основе функций
  2. Почему истинная модульная инкапсуляция так важна в крупномасштабных проектах GraphQL?
  3. Почему мы реализовали собственную библиотеку внедрения зависимостей для GraphQL-модулей?
  4. Провайдеры с ограниченной областью видимости во внедрении зависимостей GraphQL-модулей
  5. Написание проекта GraphQL TypeScript с GraphQL-модулями и GraphQL-Code-Generator
  6. Аутентификация и авторизация в GraphQL (и как могут помочь GraphQL-модули)
  7. Аутентификация с помощью модулей AccountsJS и GraphQL
  8. Управляйте адским циклическим импортом с помощью GraphQL-модулей

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