[Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ReactJS] Архитектурный паттерн Dependency Injection в React-приложении
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Расшифровка доклада Сергея Нестерова с конференции FrontendLive 2020.
Привет! Меня зовут Сергей, уже больше двух лет я работаю в группе компаний Тинькофф. Моя команда занимается разработкой системы для анализа качества обслуживания клиентов в Тинькофф, и, как вы, наверное, догадались, мы используем React в своем приложении. Не так давно мы внедрили в свой проект архитектурный паттерн Dependency Injection совместно с IoC-контейнерами. Сделали мы это не просто так: это позволило нам решить ряд проблем, которые тормозили разработку нового функционала. Непосредственно переход на новую архитектуру длился три-четыре месяца. Здесь нужно учитывать, что такого рода задачи являются техническим долгом, которые постоянно отодвигаются на задний план из-за разработки новых бизнес-фич.Сегодня я расскажу про Dependency Injection в React-приложении. Рассмотрим, что из себя представляет этот архитектурный паттерн, как мы к нему пришли и какую проблему он решает. На примерах покажу, как внедрить Dependency Injection в ваш проект, какие есть плюсы и минусы. Начну вот с такой формулы:Frontend + DI ≠ ♥Идея этого доклада родилась из-за того, что архитектурный паттерн Dependency Injection, который, хоть и появился очень давно, к сожалению, до сих пор не очень широко используется в мире фронтенда и не встречается в реальных приложениях. Хотя в последние годы Dependency Injection набирает обороты и если еще не стал трендом фронтенд-разработки, то, как мне кажется, точно им станет в ближайшее время. Кстати, о трендах и технологиях, которые будут лидировать в следующем году, рассказал в своем докладе мой коллега, Филипп Нехаев.Давайте посмотрим, где на сегодняшний день есть Dependency Injection. Он присутствует в таких современных и часто используемых фреймворках, как Angular и Nest.js (используется для написания бэкенда на NodeJS). И если в Angular Dependency Injection идет из коробки, то в React-приложениях и в самом React ничего подобного нет.Цель моего доклада — прийти к такому уравнению: Frontend + DI = ♥ и показать, как можно подружить ваше React-приложение с Dependency Injection. Но перед тем как начать, давайте познакомимся с нашим проектом.Наш технологический стекПогружу вас в наш технологический стек. Мы используем в своем проекте React и MobX. У нас по классической схеме есть какое-то одно глобальное хранилище, которое инициализируется в корне проекта и при помощи React-контекста передается вниз по дереву компонентов. В этом хранилище мы регистрируем все необходимые модели и сервисы, передаем зависимости и потихоньку начинаем строить наше приложение. Мы разрабатываем систему для анализа качества обслуживания клиентов, и у нас основными сущностями являются звонки, чаты и прочие подобные коммуникации, существующие внутри компании. У нас есть таблицы с коммуникациями, в которых можно, например, посмотреть список звонков оператора и, допустим, оставить комментарий к звонку или оценить работу оператора в конкретном разговоре с клиентом. Конечно же, в зависимости от прав доступа к конкретному звонку, могут быть доступны и другие действия. Это выглядит так:
У нас есть карточка звонка с основной информацией и есть различные действия: например, оценить, прослушать эту коммуникацию или оставить комментарий. Это должно выглядеть как таблица с пагинацией, в которой происходит загрузка и отображение 30 коммуникаций на страницу. У нас есть чаты, письма, есть встречи, а можно взять и оценить оператора без какой-либо коммуникации. Само собой, такая таблица — это переиспользуемый компонент, в котором отличаются только отображение карточек и их функционал. Каждый элемент этой таблицы выполняет различные действия, что так или иначе приводит к большому количеству сущностей, и у этих сущностей — большое количество связей. Мы это реализовали и получили решение с существенным количеством недостатков.Очень большое глобальное хранилище. Мы пошли по классической схеме: инициализируем одно глобальное хранилище в корне приложения и начинаем с ним работать. Это приводит к тому, что все объекты состояния грузятся при запуске приложения, а зависимости, которые не влияют на отображение нашей страницы со звонками, все равно подгружаются. То есть объекты в глобальном хранилище, предназначенные для страницы чатов, подгружаются и для страницы со звонками. Большое количество пропсов по дереву компонентов. Нынешний контекст React появился в версии 16.2 или 16.3. Раньше, если и пользовались старым API, то все же склонялись к прокидыванию пропсов внутрь компонентов. Из-за того, что у нас вся логика отличалась на нижнем уровне (на уровне карточек), по дереву компонентов прокидывалось большое количество пропсов и при этом дерево было с глубокой вложенностью — так называемый props hell. Из-за того, что на каждой карточке отличался функционал, у нас получилось еще и большое количество опциональных пропсов, которые прокидываются по этому дереву. Из-за того, что большая доля логики закладывается именно в карточках, у нас получились еще к тому же и сильно связанные сервисы. В конце концов, инициализация сложной зависимости выглядела не сильно привлекательно и не поддавалась рефакторингу. Вдобавок вынести все это добро в независимый модуль стало невозможно, что мешало дальнейшему переиспользованию кода. Столкнувшись с этими проблемами, мы начали искать пути решения и пришли к архитектурному паттерну Dependency Injection. Тут стоит начать немного издалека — с пяти основных принципов проектирования в объектно-ориентированном программировании, обозначаемых аббревиатурой SOLID.SOLIDЧто это за принципы?
- Принцип единственной ответственности.
- Принцип открытости/закрытости.
- Принцип подстановки Барбары Лисков.
- Принцип разделения интерфейса.
- Принцип инверсии зависимостей.
В рамках моего доклада нас интересует только последний принцип — принцип инверсии зависимостей. О чем он говорит?
- Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Чтобы разобраться, что все это значит, давайте взглянем на пример с кодом:
Я буду пользоваться двумя сущностями: сущностью соковыжималки (класс Juicer) и яблока (класс Apple). У класса Juicer есть внешняя зависимость от класса Apple. Что это значит? Это значит, что сейчас у нас очень сильно связаны два класса: Juicer и Apple. У этого решения есть ряд минусов:
- Внешняя зависимость от класса Apple.
- Сложность тестирования. Чтобы протестировать класс Juicer, нам нужно залезть внутрь него и посмотреть, как на самом деле устроен класс Apple.
- Нет возможности для повторного использования. Сейчас наша «соковыжималка» может работать только с «яблоками», и это, наверное, плохо. Хотелось бы получить более универсальную «соковыжималку».
Соблюдая принцип инверсии зависимостей, мы эту связанность между классами убираем, добавляем абстракцию в виде интерфейса IFruit и разворачиваем нашу зависимость так, что теперь «соковыжималка» не полагается на конкретный класс, а зависит от какой-то абстракции. В то же время наш класс Apple — в этом месте как раз произошла инверсия зависимостей — теперь полагается только на интерфейс, то есть на абстракцию. Снова взглянем на пример:
Теперь класс Juicer, во-первых, не инициализирует внутри конструктора необходимую зависимость, а ждет на вход какой-то фрукт с интерфейсом IFruit. Класс Apple имплементирует этот интерфейс и передается извне в конструктор класса Juicer. Мы делаем это для того, чтобы можно было повторно использовать этот класс.
Например, теперь мы можем взять апельсин и снова воспользоваться нашей соковыжималкой! А еще это легче тестировать, потому что теперь мы можем передать нашей соковыжималке не конкретный класс, а просто реализовать какой-то объект, который будет имплементацией нашего интерфейса. Таким образом, мы добились низкой связанности классов друг с другом, то есть теперь мы можем работать с любыми фруктами. Мы воспользовались архитектурным паттерном Dependency Injection. Он позволяет создавать зависимые объекты за пределами класса, которые его будут использовать и передавать его при помощи трех различных методов:
- Constructor injection.
- Property Injection.
- Setter Injection.
Constructor injection. По большому счету, мы передаем внешнюю зависимость через конструктор класса. Это решение считается самым правильным, поскольку позволяет заключить явный контракт: мы видим, что для работы соковыжималки нужен какой-то фрукт. Это легко тестировать, передав объект, имплементированный от абстракции:
Property Injection. Здесь уже нет передачи зависимости через конструктор класса. Мы добавляем в property класса необходимую нам зависимость. Этот метод лучше не использовать, потому что, во-первых, это сокрытие зависимостей — чтобы понять, с чем работает соковыжималка, нужно залезть внутрь нее:
Во-вторых, это сложно тестировать, потому что нужно переопределять поля класса. В-третьих, как я указал в комментарии на примере выше, возможно, тут будет какой-нибудь декоратор. Мы должны закладываться на реализацию какого-то IoC-контейнера, чтобы он понимал, какую зависимость и куда нужно предоставить в этот класс.Setter Injection. В сущности, он похож на Property Injection, но вместо предоставления зависимости напрямую в property класса, у нас есть сеттер, в котором мы передаем необходимую нам зависимость. Этот метод стоит использовать только для опциональных зависимостей. То есть наша соковыжималка должна уметь работать без предоставленной зависимости. Здесь, как и в случае с Property Injection, присутствует сокрытие зависимостей (неявный контракт), и нам нужно смотреть на конкретную реализацию:
Подведем итог:
- Constructor Injection — круто. Берем, используем.
- Property Injection — не используем.
- Setter Injection — используем только для опциональных зависимостей. Inversion of Control-контейнеры.
Выше я упоминал IoC-контейнеры, давайте немного остановимся на них. Что это. IoC-контейнер — это, как правило, библиотека или фреймворк, берущие на себя часть логики вашего приложения и отвечающие за создание инстансов классов: в каком порядке поднимать, какому классу нужна какая зависимость, поднимать ли на каждый запрос необходимой зависимости из контейнера новый инстанс класса или, допустим, брать уже поднятый.Как это выглядит. IoC-контейнер — это своего рода коробка, в которую мы складываем классы — классы яблока и соковыжималки из нашего примера. На выходе мы можем из этого контейнера получить уже готовый инстанс класса, в который будут переданы зависимости, необходимые ему для работы. Готовые решения для работы с ReactУже есть react-simple-di, react-ioc, typescript-ioc, inversifyJS.Для нашего проекта мы выбрали inversifyJS, потому что он не зависит от конкретного фреймворка или библиотеки. Его можно использовать не только с React. Допустим, можно даже не пользоваться Dependency Injection Angular, а воспользоваться inversifyJS.Он хорошо документирован и предоставляет мощные devtools. Это значит, что у него есть много методов для реализации крутых фич и он позволяет удобно дебажить код — у него очень понятные сообщения об ошибках. То есть, когда у вас что-то упало, inversifyJS подскажет, что и где пошло не так:
Рассмотрим наш пример. У нас есть класс Juicer, и у него в конструкторе инициализируется зависимость от класса Apple. При использовании inversifyJS, чтобы сложить в контейнер, мы добавляем injectable-декоратор, который добавляет метаданные о представлении класса.Далее мы добавляем inject-декоратор в конструктор класса и инжектим класс Apple, который нам нужен в качестве зависимости. Далее — инициализируем наш контейнер из inversifyJS, закладываем туда необходимые нам объекты и биндим их по ключам этих классов, ну а потом можем доставать готовые инстансы из этого контейнера.Вы можете сказать: «Это не круто, у нас здесь до сих пор конкретные классы, а хотелось бы работать именно с интерфейсами для уменьшения зависимости!» Ну что ж, давайте переделаем примеры. Вернемся к нашим интерфейсам:
Что мы теперь делаем? Мы имплементируем класс Apple от интерфейса IFruit. В конструктор класса мы передаем @inject по этому интерфейсу и затем регистрируем в контейнере по ключу необходимый нам класс Apple.Что мы получим? Мы получим IFruit is not defined — ошибку ReferenceError.
Почему так произошло? Думаю, вы знаете, что в runtime TypeScript — обычный JavaScript и интерфейсов там нет. В момент, когда у нас запускается приложение, InversifyJS попытается инициализировать зависимость по интерфейсу, который на самом деле не определен.Что мы можем сделать с этой ошибкой, чтобы пользоваться нашими интерфейсами? На самом деле, здесь только одно решение — использовать строковые ключи или токены:
Мы добавляем строковую константу и говорим, что мы хотим получить в конструктор класса зависимость под ключом FruitKey. Далее — в контейнере указываем, что класс Apple теперь будет относиться к этому ключу. Таким образом мы можем использовать интерфейсы, придерживаться архитектурного паттерна Dependency Injection и применять инверсию зависимостей.Reflect-metadataReflect-metadata — это библиотека, которая добавляет метаданные (данные о данных) о классах непосредственно в сам класс. Давайте посмотрим на примере, как это работает:
У нас есть класс Juicer, у него — injectable- и inject-декораторы. Мы хотим понять, как же все-таки inversify-контейнер понимает, что внутрь класса Juicer нужно передать зависимость в виде фрукта. Давайте посмотрим, какие метаданные добавляет reflect-metadata к классу Juice.Воспользуемся командой console.log(Reflect.getMetadataKeys) от нашего класса. Она выведет три ключа:
- design:paramtypes;
- inversify:tagged;
- inversify:paramtypes.
Итак, мы хотим разобраться, как же inversifyJS понимает, что нужно предоставить в конструктор класса зависимость фрукта. Давайте посмотрим значение ключа inversify:tagged:
Снова выполняем console.log(Reflect.getMetadata) по ключу inversify:tagged и видим, что в метаданных класса Juicer присутствует запись о том, что первым параметром в конструктор класса нужно передать зависимость с ключом FruitKey. Именно так inversifyJS и работает: на основе метаданных понимает, какую зависимость и куда передать. Dependency Injection+ReactПерейдем к самому интересному — к внедрению Dependency Injection в React-приложение. Стоит отметить, что в React внедрение в конструктор класса невозможно, потому что React использует конструктор класса по своему назначению. Здесь приходится добавлять обертки, чтобы связать наши компоненты с контейнером. Разобраться, как это работает, вам поможет демо. Вы можете его использовать, оно готово к работе. Просто добавляйте свои страницы и состояния.
import React from 'react';
import { interfaces } from 'inversify';
const context = React.createContext<interfaces.Container | null>(null);
export default context;
Давайте рассмотрим пример. Чтобы хранить контейнеры, конечно же, мы воспользуемся контекстом React. Здесь все достаточно просто: как обычно, мы вызываем функцию React.createContext и передаем ему первоначальное значение null. У inversifyJS есть типы, с помощью которых можно легко и понятно типизировать и при этом получать минимальное количество ошибок.Что нужно для того, чтобы передать контекст в компонент? Нужно реализовать DiProvider — контекст-провайдер, который позволит передавать вниз по дереву созданный нами контекст. Мы реализуем функцию, которая на вход будет принимать два параметра: наш контейнер и дочерние элементы (children) из родительского React-компонента, у которых будет доступ к зависимостям из контейнера:
type Props = {
container: interfaces.Container;
children: ReactNode;
};
export function DiProvider({ container, children }: Props) {
return <Context.Provider value={container}>{children}</Context.Provider>;
}
Дальше нам нужно реализовать High-Order-компонент, который будет помогать передавать контекст вниз. Для этого мы реализуем High-Order-компонент, который назовем, допустим, withProvider, и у него будут два параметра на вход — компонент и контейнер, который мы инициализируем:
export function withProvider<P, C>(
component: JSXElementConstructor<P> & C,
container: interfaces.Container
) {
class ProviderWrap extends Component<Props> {
public static contextType = Context;
public static displayName = `diProvider(${getDisplayName(component)})`;
public constructor(props: Props, context?: interfaces.Container) {
super(props);
this.context = context;
if (this.context) {
container.parent = this.context;
}
}
public render() {
const WrappedComponent = component;
return (
<DiProvider container={container}>
<WrappedComponent {...(this.props as any)} />
</DiProvider>
);
}
}
return ProviderWrap as ComponentClass<Props>;
}
В моем примере довольно много кода. Но большая его часть предназначена для корректной работы Typescript, который будет подсказывать, какие параметры можно передать в получившиеся High-Order-компоненты и отсеивать пропсы, получаемые из нашего контейнера. Мы реализовали функцию, которая оборачивает переданный компонент DiProvider функцией и передает в контекст контейнер, оставляя пропсы этого компонента без изменений.В конструкторе класса мы ищем родительский контекст тех же самых контейнеров, чтобы реализовать иерархическую структуру DI-контейнеров. Если у нас уже есть контекст с контейнером по дереву компонент выше, то мы записываем в parent-поле дочернего контейнера ссылку на него. Как я упоминал ранее, для иерархических контейнеров это дает крутую фичу, к которой вернусь подробнее чуть позже.Теперь перейдем к компоненту, который будет получать из нашего контейнера необходимые данные в виде пропсов. Для этого мы реализуем еще один High-Order-компонент, который будет принимать на вход сам компонент и зависимости, которые он хочет получить в качестве пропсов. Эти зависимости передаются в виде объекта, названия свойств которого будут соответствовать названиям пропсов компонента, а значения — специальным Dependence-классам.В конструктор Dependence-класса передается ключ необходимой зависимости или класс, у которого есть статическое поле с этим ключом. Вторым параметром можно передать объект с опциями. Это нужно для того, чтобы воспользоваться такими фишками inversify, как именованный binding (то есть по имени), tagged binding и функцией трансформации — чтобы из класса достать уже конкретное свойство. Здесь, как и в случае с предыдущим компонентом высшего порядка, много разных типов для того, чтобы TypeScript подсказывал, что не так и какие значения нужно передавать. По большому счету, мы возвращаем исходный компонент с уже переданными в него зависимостями из контейнера, которые мы запросили во втором параметре нашей обертки. Все эти зависимости мы получаем при помощи функции inject. В ней мы проверяем, что у нас есть контекст с DI-контейнером, а затем достаем необходимые зависимости из него при помощи метода resolve, предварительно собрав ключи этих зависимостей. Получившийся результат складываем в новый объект, свойства которого мы вернем в компонент в виде пропсов. Все, что нужно, мы сделали и теперь можем воспользоваться нашими High-Order-компонентами для стандартного компонента React, у которого мы хотим получить зависимость. Мой пример написан на Next.js, чтобы был серверный рендеринг. Да и вообще Next.js легко собрать: то есть npm install, npm run dev — все запустится. Сначала мы оборачиваем pages-компонент в HOC withProvider и передаем туда контейнер, который хотим использовать на уровне нашей страницы.Чтобы получить необходимую зависимость из нашего контейнера, мы должны воспользоваться HOC diInject, передать туда компонент и указать внутри объекта, что мы хотим получить зависимость по такому-то строковому ключу. Например: мы зарегистрировали в контейнере по строковому ключу зависимость ListModel и говорим, что она будет inSingletonScope, потому что мы хотим, чтобы эта зависимость закэшировалась и на каждый get-метод из контейнера мы получали тот же самый инстанс нашей зависимости. Дальше для типизации указываем в Props компонента, что у нас должна быть передана зависимость booksListModel из контейнера, и указываем ее тип. А inversifyJS в React-приложении даст нам поддержку иерархических контейнеров, повторное использование кода, низкую связанность и простоту тестирования.Если последние два пункта исходят из того, что мы придерживаемся архитектурного паттерна Dependency Injection, то первые два — это про плюшки, которые дает inversifyJS.Давайте рассмотрим пример, иллюстрирующий иерархическую структуру наших контейнеров:
У нас SPA-приложение и есть входная точка. При помощи React Router мы перекидываем пользователя на конкретную страницу. В корне нашего проекта добавляем дефолтный контейнер, в который складываем зависимости для работы приложения: это сервисы типа fetch-сервиса, юзер-сервиса для работы с авторизованными данными пользователя и fetch для работы с бэкендом — то есть все те вещи, которые использует каждая страница. Далее, уже на уровне конкретной страницы, регистрируем дочерние контейнеры, в которых мы будем хранить состояние и сервисы конкретной страницы и они не будут переплетаться между собой. Теперь, когда мы загрузим страницу 1, она не будет ничего знать о странице 2.При иерархической структуре нам не нужно указывать общие сервисы, потому что наш дочерний контейнер будет знать о родительском, но при этом родительский ничего не будет знать о дочерних. То есть мы можем пользоваться моделями и сервисами родительского контейнера, тогда как родительский не имеет доступа к дочерним и тем более контейнер конкретной страницы ничего не знает о контейнере других страниц.В этом есть много плюсов, потому что теперь у нас страницы — это такие модули, которые, во-первых, не могут управлять состоянием друг друга, а во-вторых, мы отдаем пользователю только тот js, который ему нужен на этой странице. Поговорим о повторном использовании кода и для этого вернемся к моему примеру:
У нас есть карточка звонка, которую мы хотим переиспользовать. У нее есть различные внешние зависимости: CommentService, CommentModel и так далее.
Чтобы все время не регистрировать эти сервисы и не передавать их в контейнер, мы можем воспользоваться фишкой inversifyJS и взять Container Module, в который мы складываем все необходимые зависимости, а затем на конкретной странице просто подгружаем в этот контейнер необходимый нам модуль. Собственно, на этом все. Теперь мы можем всю нашу страницу разбить на независимые модули и подгружать конкретный модуль в контейнер, только когда он нам нужен.Теперь хочу рассказать про две ключевые фишки inversifyJS, которыми мы пользуемся у себя в проекте. Первая из них — tagged bindings. Для чего они нужны? Давайте вернемся к примеру с соковыжималкой, апельсином и яблоком:
Теперь мы скажем, что соковыжималка должна работать только с цитрусовыми фруктами. Для этого импортируем из inversifyJS декоратор tagged и внутри конструктора класса говорим, что по ключу FruitKey (то есть по ключу фруктов) хотим получить цитрусовый фрукт. Далее — в контейнере регистрируем, что теперь по одному ключу FruitKey у нас два класса — яблоко и апельсин, которым говорим, что яблоко у нас — не цитрусовое, а апельсин — цитрусовый. Чтобы различать эти два вида зависимостей, используем whenTargetTagged.Вторая фишка, о которой я расскажу, — named bindings:
Итак, у нас есть соковыжималка и мы хотим добавить еще один класс с внешней зависимостью в виде соковыжималки, в которой используются яблоки, — класс Store, магазин. Чтобы получить соковыжималку с яблоками, в конструкторе класса мы указываем, что ее необходимо получить по ключу JuicerKey c дополнительным параметром AppleJuicer. Для этого воспользуемся декоратором named из inversifyJS. В контейнере регистрируем наши фрукты и при помощи метода whenAnyAncestorNamed указываем, что у яблок будет дополнительный ключ AppleJuicer, а у апельсинов — OrangeJuicer. Обратите внимание, что в классе Juicer мы берем зависимость по ключу FruitKey и, по большому счету, здесь мы не знаем, что нам придет на вход — апельсин или яблоко. Но при этом в родительской зависимости Store мы можем это определить.Минусы Dependency Injection+ReactКакие есть минусы DI в связке с React? Самый большой и, пожалуй, единственный минус — это использование строковых ключей, которые не сопоставляются с типами. То есть для того, чтобы мы могли работать с абстракциями в виде интерфейсов, нам приходится добавлять строковые ключи, так как в runtime у нас обычный JavaScript, в котором нет интерфейсов:
Если посмотрите на предпоследнюю строчку с кодом, то увидите, что мы биндим Store по ключу StoreKey. Если в моем примере поменять местами Store и, допустим, ту же самую соковыжималку, то в приложении получим ошибку и TypeScript не скажет, что здесь что-то пошло не так.Когда мы берем из контейнера какую-то зависимость или когда биндим какую-то зависимость в контейнер, лучше указывать, какой класс или интерфейс мы хотим получить или зарегистрировать в контейнере.
container.bind<Store>("StoreKey").to(Store);
Это единственный минус, который мы заметили за время внедрения и использования получившейся архитектуры. Вывод Мы получили новую модульную и гибкую архитектуру, которая легко поддается изменениям и легко расширяется. Придерживаясь архитектурного паттерна Dependency Injection, мы уменьшили связанность между компонентами системы и в целом пишем меньше кода. У нас больше нет одного глобального хранилища. Страницы стали полностью независимыми друг от друга и загружают только необходимые для конкретной страницы данные.На этом все. Вы можете перейти по двум ссылкам: первая — это playground для того, чтобы побаловаться с inversifyJS на NodeJS, вторая — пример внедрения в React-приложение. Вы можете забрать себе эти High-Order-компоненты и контейнеры и начать строить свое приложение уже с React и inversifyJS.
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, GTK+, C, Разработка под Linux] Выявляем опечатки в проекте GTK 4 с помощью PVS-Studio
- [Open source, Совершенный код, C, Разработка под Linux] Finding Typos in the GTK 4 Project by PVS-Studio
- [Open source, GTK+, C, Разработка под Linux] Finding Typos in the GTK 4 Project by PVS-Studio
- [Программирование, Проектирование и рефакторинг, API, Google API] Проектирование API: почему для представления отношений в API лучше использовать ссылки, а не ключи (перевод)
- [Разработка веб-сайтов, PHP, Проектирование и рефакторинг, API] Хьюстон, у нас проблема с интерпретацией ошибок
- [Тестирование IT-систем, JavaScript] Не используйте фикстуры в Cypress и юнит-тесты — используйте фабричные функции (перевод)
- [IT-инфраструктура, CRM-системы, Софт] CRM — это не…
- [Веб-дизайн, Разработка веб-сайтов, PHP, 1С-Битрикс] Фреймворки против Битрикс
- [Поисковые технологии, Управление персоналом, Карьера в IT-индустрии, Читальный зал, Удалённая работа] Агрегаторы вакансий для разработчиков: сравниваю 10+ самых популярных
- [Программирование] Дайджест материалов сообщества Deno (01.01 — 31.01)
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_reactjs, #_di, #_react, #_ioc, #_solid, #_typescript, #_javascript, #_inversifyjs, #_blog_kompanii_tinkoff (
Блог компании TINKOFF
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
), #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 20:38
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Расшифровка доклада Сергея Нестерова с конференции FrontendLive 2020. Привет! Меня зовут Сергей, уже больше двух лет я работаю в группе компаний Тинькофф. Моя команда занимается разработкой системы для анализа качества обслуживания клиентов в Тинькофф, и, как вы, наверное, догадались, мы используем React в своем приложении. Не так давно мы внедрили в свой проект архитектурный паттерн Dependency Injection совместно с IoC-контейнерами. Сделали мы это не просто так: это позволило нам решить ряд проблем, которые тормозили разработку нового функционала. Непосредственно переход на новую архитектуру длился три-четыре месяца. Здесь нужно учитывать, что такого рода задачи являются техническим долгом, которые постоянно отодвигаются на задний план из-за разработки новых бизнес-фич.Сегодня я расскажу про Dependency Injection в React-приложении. Рассмотрим, что из себя представляет этот архитектурный паттерн, как мы к нему пришли и какую проблему он решает. На примерах покажу, как внедрить Dependency Injection в ваш проект, какие есть плюсы и минусы. Начну вот с такой формулы:Frontend + DI ≠ ♥Идея этого доклада родилась из-за того, что архитектурный паттерн Dependency Injection, который, хоть и появился очень давно, к сожалению, до сих пор не очень широко используется в мире фронтенда и не встречается в реальных приложениях. Хотя в последние годы Dependency Injection набирает обороты и если еще не стал трендом фронтенд-разработки, то, как мне кажется, точно им станет в ближайшее время. Кстати, о трендах и технологиях, которые будут лидировать в следующем году, рассказал в своем докладе мой коллега, Филипп Нехаев.Давайте посмотрим, где на сегодняшний день есть Dependency Injection. Он присутствует в таких современных и часто используемых фреймворках, как Angular и Nest.js (используется для написания бэкенда на NodeJS). И если в Angular Dependency Injection идет из коробки, то в React-приложениях и в самом React ничего подобного нет.Цель моего доклада — прийти к такому уравнению: Frontend + DI = ♥ и показать, как можно подружить ваше React-приложение с Dependency Injection. Но перед тем как начать, давайте познакомимся с нашим проектом.Наш технологический стекПогружу вас в наш технологический стек. Мы используем в своем проекте React и MobX. У нас по классической схеме есть какое-то одно глобальное хранилище, которое инициализируется в корне проекта и при помощи React-контекста передается вниз по дереву компонентов. В этом хранилище мы регистрируем все необходимые модели и сервисы, передаем зависимости и потихоньку начинаем строить наше приложение. Мы разрабатываем систему для анализа качества обслуживания клиентов, и у нас основными сущностями являются звонки, чаты и прочие подобные коммуникации, существующие внутри компании. У нас есть таблицы с коммуникациями, в которых можно, например, посмотреть список звонков оператора и, допустим, оставить комментарий к звонку или оценить работу оператора в конкретном разговоре с клиентом. Конечно же, в зависимости от прав доступа к конкретному звонку, могут быть доступны и другие действия. Это выглядит так: У нас есть карточка звонка с основной информацией и есть различные действия: например, оценить, прослушать эту коммуникацию или оставить комментарий. Это должно выглядеть как таблица с пагинацией, в которой происходит загрузка и отображение 30 коммуникаций на страницу. У нас есть чаты, письма, есть встречи, а можно взять и оценить оператора без какой-либо коммуникации. Само собой, такая таблица — это переиспользуемый компонент, в котором отличаются только отображение карточек и их функционал. Каждый элемент этой таблицы выполняет различные действия, что так или иначе приводит к большому количеству сущностей, и у этих сущностей — большое количество связей. Мы это реализовали и получили решение с существенным количеством недостатков.Очень большое глобальное хранилище. Мы пошли по классической схеме: инициализируем одно глобальное хранилище в корне приложения и начинаем с ним работать. Это приводит к тому, что все объекты состояния грузятся при запуске приложения, а зависимости, которые не влияют на отображение нашей страницы со звонками, все равно подгружаются. То есть объекты в глобальном хранилище, предназначенные для страницы чатов, подгружаются и для страницы со звонками. Большое количество пропсов по дереву компонентов. Нынешний контекст React появился в версии 16.2 или 16.3. Раньше, если и пользовались старым API, то все же склонялись к прокидыванию пропсов внутрь компонентов. Из-за того, что у нас вся логика отличалась на нижнем уровне (на уровне карточек), по дереву компонентов прокидывалось большое количество пропсов и при этом дерево было с глубокой вложенностью — так называемый props hell. Из-за того, что на каждой карточке отличался функционал, у нас получилось еще и большое количество опциональных пропсов, которые прокидываются по этому дереву. Из-за того, что большая доля логики закладывается именно в карточках, у нас получились еще к тому же и сильно связанные сервисы. В конце концов, инициализация сложной зависимости выглядела не сильно привлекательно и не поддавалась рефакторингу. Вдобавок вынести все это добро в независимый модуль стало невозможно, что мешало дальнейшему переиспользованию кода. Столкнувшись с этими проблемами, мы начали искать пути решения и пришли к архитектурному паттерну Dependency Injection. Тут стоит начать немного издалека — с пяти основных принципов проектирования в объектно-ориентированном программировании, обозначаемых аббревиатурой SOLID.SOLIDЧто это за принципы?
Я буду пользоваться двумя сущностями: сущностью соковыжималки (класс Juicer) и яблока (класс Apple). У класса Juicer есть внешняя зависимость от класса Apple. Что это значит? Это значит, что сейчас у нас очень сильно связаны два класса: Juicer и Apple. У этого решения есть ряд минусов:
Теперь класс Juicer, во-первых, не инициализирует внутри конструктора необходимую зависимость, а ждет на вход какой-то фрукт с интерфейсом IFruit. Класс Apple имплементирует этот интерфейс и передается извне в конструктор класса Juicer. Мы делаем это для того, чтобы можно было повторно использовать этот класс. Например, теперь мы можем взять апельсин и снова воспользоваться нашей соковыжималкой! А еще это легче тестировать, потому что теперь мы можем передать нашей соковыжималке не конкретный класс, а просто реализовать какой-то объект, который будет имплементацией нашего интерфейса. Таким образом, мы добились низкой связанности классов друг с другом, то есть теперь мы можем работать с любыми фруктами. Мы воспользовались архитектурным паттерном Dependency Injection. Он позволяет создавать зависимые объекты за пределами класса, которые его будут использовать и передавать его при помощи трех различных методов:
Property Injection. Здесь уже нет передачи зависимости через конструктор класса. Мы добавляем в property класса необходимую нам зависимость. Этот метод лучше не использовать, потому что, во-первых, это сокрытие зависимостей — чтобы понять, с чем работает соковыжималка, нужно залезть внутрь нее: Во-вторых, это сложно тестировать, потому что нужно переопределять поля класса. В-третьих, как я указал в комментарии на примере выше, возможно, тут будет какой-нибудь декоратор. Мы должны закладываться на реализацию какого-то IoC-контейнера, чтобы он понимал, какую зависимость и куда нужно предоставить в этот класс.Setter Injection. В сущности, он похож на Property Injection, но вместо предоставления зависимости напрямую в property класса, у нас есть сеттер, в котором мы передаем необходимую нам зависимость. Этот метод стоит использовать только для опциональных зависимостей. То есть наша соковыжималка должна уметь работать без предоставленной зависимости. Здесь, как и в случае с Property Injection, присутствует сокрытие зависимостей (неявный контракт), и нам нужно смотреть на конкретную реализацию: Подведем итог:
Рассмотрим наш пример. У нас есть класс Juicer, и у него в конструкторе инициализируется зависимость от класса Apple. При использовании inversifyJS, чтобы сложить в контейнер, мы добавляем injectable-декоратор, который добавляет метаданные о представлении класса.Далее мы добавляем inject-декоратор в конструктор класса и инжектим класс Apple, который нам нужен в качестве зависимости. Далее — инициализируем наш контейнер из inversifyJS, закладываем туда необходимые нам объекты и биндим их по ключам этих классов, ну а потом можем доставать готовые инстансы из этого контейнера.Вы можете сказать: «Это не круто, у нас здесь до сих пор конкретные классы, а хотелось бы работать именно с интерфейсами для уменьшения зависимости!» Ну что ж, давайте переделаем примеры. Вернемся к нашим интерфейсам: Что мы теперь делаем? Мы имплементируем класс Apple от интерфейса IFruit. В конструктор класса мы передаем @inject по этому интерфейсу и затем регистрируем в контейнере по ключу необходимый нам класс Apple.Что мы получим? Мы получим IFruit is not defined — ошибку ReferenceError. Почему так произошло? Думаю, вы знаете, что в runtime TypeScript — обычный JavaScript и интерфейсов там нет. В момент, когда у нас запускается приложение, InversifyJS попытается инициализировать зависимость по интерфейсу, который на самом деле не определен.Что мы можем сделать с этой ошибкой, чтобы пользоваться нашими интерфейсами? На самом деле, здесь только одно решение — использовать строковые ключи или токены: Мы добавляем строковую константу и говорим, что мы хотим получить в конструктор класса зависимость под ключом FruitKey. Далее — в контейнере указываем, что класс Apple теперь будет относиться к этому ключу. Таким образом мы можем использовать интерфейсы, придерживаться архитектурного паттерна Dependency Injection и применять инверсию зависимостей.Reflect-metadataReflect-metadata — это библиотека, которая добавляет метаданные (данные о данных) о классах непосредственно в сам класс. Давайте посмотрим на примере, как это работает: У нас есть класс Juicer, у него — injectable- и inject-декораторы. Мы хотим понять, как же все-таки inversify-контейнер понимает, что внутрь класса Juicer нужно передать зависимость в виде фрукта. Давайте посмотрим, какие метаданные добавляет reflect-metadata к классу Juice.Воспользуемся командой console.log(Reflect.getMetadataKeys) от нашего класса. Она выведет три ключа:
Снова выполняем console.log(Reflect.getMetadata) по ключу inversify:tagged и видим, что в метаданных класса Juicer присутствует запись о том, что первым параметром в конструктор класса нужно передать зависимость с ключом FruitKey. Именно так inversifyJS и работает: на основе метаданных понимает, какую зависимость и куда передать. Dependency Injection+ReactПерейдем к самому интересному — к внедрению Dependency Injection в React-приложение. Стоит отметить, что в React внедрение в конструктор класса невозможно, потому что React использует конструктор класса по своему назначению. Здесь приходится добавлять обертки, чтобы связать наши компоненты с контейнером. Разобраться, как это работает, вам поможет демо. Вы можете его использовать, оно готово к работе. Просто добавляйте свои страницы и состояния. import React from 'react';
import { interfaces } from 'inversify'; const context = React.createContext<interfaces.Container | null>(null); export default context; type Props = {
container: interfaces.Container; children: ReactNode; }; export function DiProvider({ container, children }: Props) { return <Context.Provider value={container}>{children}</Context.Provider>; } export function withProvider<P, C>(
component: JSXElementConstructor<P> & C, container: interfaces.Container ) { class ProviderWrap extends Component<Props> { public static contextType = Context; public static displayName = `diProvider(${getDisplayName(component)})`; public constructor(props: Props, context?: interfaces.Container) { super(props); this.context = context; if (this.context) { container.parent = this.context; } } public render() { const WrappedComponent = component; return ( <DiProvider container={container}> <WrappedComponent {...(this.props as any)} /> </DiProvider> ); } } return ProviderWrap as ComponentClass<Props>; } У нас SPA-приложение и есть входная точка. При помощи React Router мы перекидываем пользователя на конкретную страницу. В корне нашего проекта добавляем дефолтный контейнер, в который складываем зависимости для работы приложения: это сервисы типа fetch-сервиса, юзер-сервиса для работы с авторизованными данными пользователя и fetch для работы с бэкендом — то есть все те вещи, которые использует каждая страница. Далее, уже на уровне конкретной страницы, регистрируем дочерние контейнеры, в которых мы будем хранить состояние и сервисы конкретной страницы и они не будут переплетаться между собой. Теперь, когда мы загрузим страницу 1, она не будет ничего знать о странице 2.При иерархической структуре нам не нужно указывать общие сервисы, потому что наш дочерний контейнер будет знать о родительском, но при этом родительский ничего не будет знать о дочерних. То есть мы можем пользоваться моделями и сервисами родительского контейнера, тогда как родительский не имеет доступа к дочерним и тем более контейнер конкретной страницы ничего не знает о контейнере других страниц.В этом есть много плюсов, потому что теперь у нас страницы — это такие модули, которые, во-первых, не могут управлять состоянием друг друга, а во-вторых, мы отдаем пользователю только тот js, который ему нужен на этой странице. Поговорим о повторном использовании кода и для этого вернемся к моему примеру: У нас есть карточка звонка, которую мы хотим переиспользовать. У нее есть различные внешние зависимости: CommentService, CommentModel и так далее. Чтобы все время не регистрировать эти сервисы и не передавать их в контейнер, мы можем воспользоваться фишкой inversifyJS и взять Container Module, в который мы складываем все необходимые зависимости, а затем на конкретной странице просто подгружаем в этот контейнер необходимый нам модуль. Собственно, на этом все. Теперь мы можем всю нашу страницу разбить на независимые модули и подгружать конкретный модуль в контейнер, только когда он нам нужен.Теперь хочу рассказать про две ключевые фишки inversifyJS, которыми мы пользуемся у себя в проекте. Первая из них — tagged bindings. Для чего они нужны? Давайте вернемся к примеру с соковыжималкой, апельсином и яблоком: Теперь мы скажем, что соковыжималка должна работать только с цитрусовыми фруктами. Для этого импортируем из inversifyJS декоратор tagged и внутри конструктора класса говорим, что по ключу FruitKey (то есть по ключу фруктов) хотим получить цитрусовый фрукт. Далее — в контейнере регистрируем, что теперь по одному ключу FruitKey у нас два класса — яблоко и апельсин, которым говорим, что яблоко у нас — не цитрусовое, а апельсин — цитрусовый. Чтобы различать эти два вида зависимостей, используем whenTargetTagged.Вторая фишка, о которой я расскажу, — named bindings: Итак, у нас есть соковыжималка и мы хотим добавить еще один класс с внешней зависимостью в виде соковыжималки, в которой используются яблоки, — класс Store, магазин. Чтобы получить соковыжималку с яблоками, в конструкторе класса мы указываем, что ее необходимо получить по ключу JuicerKey c дополнительным параметром AppleJuicer. Для этого воспользуемся декоратором named из inversifyJS. В контейнере регистрируем наши фрукты и при помощи метода whenAnyAncestorNamed указываем, что у яблок будет дополнительный ключ AppleJuicer, а у апельсинов — OrangeJuicer. Обратите внимание, что в классе Juicer мы берем зависимость по ключу FruitKey и, по большому счету, здесь мы не знаем, что нам придет на вход — апельсин или яблоко. Но при этом в родительской зависимости Store мы можем это определить.Минусы Dependency Injection+ReactКакие есть минусы DI в связке с React? Самый большой и, пожалуй, единственный минус — это использование строковых ключей, которые не сопоставляются с типами. То есть для того, чтобы мы могли работать с абстракциями в виде интерфейсов, нам приходится добавлять строковые ключи, так как в runtime у нас обычный JavaScript, в котором нет интерфейсов: Если посмотрите на предпоследнюю строчку с кодом, то увидите, что мы биндим Store по ключу StoreKey. Если в моем примере поменять местами Store и, допустим, ту же самую соковыжималку, то в приложении получим ошибку и TypeScript не скажет, что здесь что-то пошло не так.Когда мы берем из контейнера какую-то зависимость или когда биндим какую-то зависимость в контейнер, лучше указывать, какой класс или интерфейс мы хотим получить или зарегистрировать в контейнере. container.bind<Store>("StoreKey").to(Store);
=========== Источник: habr.com =========== Похожие новости:
Блог компании TINKOFF ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ), #_javascript, #_proektirovanie_i_refaktoring ( Проектирование и рефакторинг ), #_reactjs |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 20:38
Часовой пояс: UTC + 5