Русский
Русский
English
Статистика
Реклама

Dependancy injection

Используем DI в Angular по максимуму концепция частных провайдеров

23.06.2020 16:19:11 | Автор: admin
В Angular очень мощный механизм Dependency Injection. Он позволяет передавать по вашему приложению любые данные, преобразовывать и переопределять их в нужных частях.

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

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

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




Как обычно используется DI в Angular


Я ежедневно провожу ревью Angular-кода на работе и в опенсорсе. Как правило, в большинстве приложений DI сводится к следующей функциональности:

  1. Получить сущности Angular из дерева зависимостей: ChangeDetectorRef, ElementRef и проч.
  2. Получить сервис, чтобы использовать его в компоненте.
  3. Получить какой-нибудь глобальный конфиг по токену, который объявлен где-то наверху. Например, задать токен API_URL в рутовом модуле и получать его из DI в любом месте приложения при необходимости.

Реже встречаются случаи, когда разработчики идут дальше и преобразуют уже существующий глобальный токен в более удобную форму. Хороший пример такого преобразования токен на получение WINDOW из пакета @ng-web-apis/common.

Angular предоставляет токен DOCUMENT, чтобы можно было получить объект страницы из любого места приложения: ваши компоненты не зависят от глобальных объектов, легко тестировать, ничего не сломается при SSR.

Если вам регулярно нужен доступ до объекта WINDOW, можно написать такой токен:

import {DOCUMENT} from '@angular/common';import {inject, InjectionToken} from '@angular/core';export const WINDOW = new InjectionToken<Window>(    'An abstraction over global window object',    {        factory: () => {            const {defaultView} = inject(DOCUMENT);            if (!defaultView) {                throw new Error('Window is not available');            }            return defaultView;        },    },);


Когда кто-то запросит токен WINDOW в первый раз из дерева DI, выполнится фабрика токена он получит объект DOCUMENT у Angular и получит из него ссылку на объект window.

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

Частные провайдеры


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



Давайте посмотрим сразу на солидном примере. Эрин Коглар в своем докладе The Architecture of Components на большой международной конференции Angular Connect показала такой пример:


Если вам неудобно открывать и смотреть видео, то давайте распишу кейс здесь.

Имеем:
Компонент, который отвечает за показ информации по некой сущности организации.
Query-параметр роута, который указывает id организации, с которой мы работаем в текущий момент.
Сервис, который по id возвращает Observable с информацией об организации.

Что хотим сделать
Взять из query-параметров id организации, передать его в метод сервиса, а в ответ получить стрим с информацией об организации. Эту информацию вывести в компоненте.

Рассмотрим три способа добиться желаемого и разберем их.

Как делать не нужно
Иногда я встречаю вот такой стиль работы с данными в компонентах. Пожалуйста, не делайте так:
@Component({   selector: 'organization',   templateUrl: 'organization.template.html',   styleUrls: ['organization.style.less'],   changeDetection: ChangeDetectionStrategy.OnPush,})export class OrganizationComponent implements OnInit {   organization: Organization;   constructor(       private readonly activatedRoute: ActivatedRoute,       private readonly organizationService: OrganizationService,   ) {}   ngOnInit() {       this.activatedRoute.params           .pipe(               switchMap(params => {                   const id = params.get('orgId');                   return this.organizationService.getOrganizationById$(id);               }),           )           .subscribe(organization => {               this.organization = organization;           });   }}

Чтобы использовать полученные данные в шаблоне:
<p *ngIf="organization">   {{organization.name}} from {{organization.city}}</p>


Этот код будет работать, но у него есть ряд проблем:
Неопределенность поля organization: между моментом объявления поля при создании класса и присвоения ему значения пройдет некоторое время. Все это время в данном примере поле будет undefined. Мы либо нарушаем типизацию (такое возможно при отключенном strict у TypeScript), либо предусматриваем это в типе (organization?: Organization) и обрекаем себя на ряд дополнительных проверок.
Такой код тяжелее поддерживать. Завтра нам понадобится вытащить еще один параметр, мы продолжим заполнять ngOnInit, и код начнет постепенно превращаться в кашу с кучей неявных переменных и тяжелым для понимания потоком данных.
При подобном обновлении полей можно столкнуться с проблемами проверки изменений при использовании стратегии OnPush.

Сделаем хорошо
В докладе Эрин из видео, что я прикладывал выше, сделано хорошо. С ее вариантом получается примерно так:

@Component({   selector: 'organization',   templateUrl: 'organization.template.html',   styleUrls: ['organization.style.less'],   changeDetection: ChangeDetectionStrategy.OnPush,})export class OrganizationComponent {   readonly organization$: Observable<Organization> = this.activatedRoute.params.pipe(       switchMap(params => {           const id = params.get('orgId');           return this.organizationService.getOrganizationById$(id);       }),   );   constructor(       private readonly activatedRoute: ActivatedRoute,       private readonly organizationService: OrganizationService,   ) {}}

Чтобы использовать полученные данные в шаблоне:
<p *ngIf="organization$ | async as organization">   {{organization.name}} from {{organization.city}}</p>


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

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

Сделаем еще круче: частные провайдеры


Давайте присмотримся внимательнее к прошлому решению.

На самом деле компонент не зависит от роутера и даже от OrganizationService. Он зависит от organization$. Но такой сущности в нашем дереве внедрения зависимостей нет, поэтому мы вынуждены выполнять преобразования в компоненте.

Но что если выполнить это преобразование перед тем, как данные попадут в компонент? Давайте напишем Provider специально для компонента, в котором и будут происходить необходимые преобразования.

Для удобства мы выносим провайдеры в отдельный файл рядом с компонентом, получая такую структуру файлов:



В файле organization.providers.ts будут находиться Provider для преобразования данных и токен для их получения в компоненте:

export const ORGANIZATION_INFO = new InjectionToken<Observable<Organization>>(   'A stream with current organization information',);По этому токену будет идти стрим с необходимой компоненту информацией:export const ORGANIZATION_PROVIDERS: Provider[] = [   {       provide: ORGANIZATION_INFO,       deps: [ActivatedRoute, OrganizationService],       useFactory: organizationFactory,   },];export function organizationFactory(   {params}: ActivatedRoute,   organizationService: OrganizationService,): Observable<Organization> {   return params.pipe(       switchMap(params => {           const id = params.get('orgId');           return organizationService.getOrganizationById$(id);       }),   );}


Определим массив провайдеров для компонента. Значение для токена ORGANIZATION_INFO получим из фабрики, в которой сделаем необходимое преобразование данных.

Примечание по работе DI deps позволяет взять из дерева DI необходимые сущности и передать их как аргументы в фабрику значения токена. В них можно получить любую сущность из DI, в том числе и с использованием DI-декораторов, например:

{       provide: ACTIVE_TAB,       deps: [           [new Optional(), new Self(), RouterLinkActive],       ],       useFactory: activeTabFactory,}


Объявим providers в компоненте:

@Component({   ..   providers: [ORGANIZATION_PROVIDERS],})


И мы готовы к использованию данных в компоненте:

@Component({   selector: 'organization',   templateUrl: 'organization.template.html',   styleUrls: ['organization.style.less'],   changeDetection: ChangeDetectionStrategy.OnPush,   providers: [ORGANIZATION_PROVIDERS],})export class OrganizationComponent {   constructor(       @Inject(ORGANIZATION_INFO) readonly organization$: Observable<Organization>,   ) {}}

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

Шаблон остается прежним:
<p *ngIf="organization$ | async as organization">   {{organization.name}} from {{organization.city}}</p>


Что нам дает этот подход?

  1. Чистые зависимости: компонент не внедряет в себя и не хранит лишних сущностей. Он работает только с теми данными, которые ему нужны, при этом сам остается чистым и содержит только логику для отображения данных.
  2. Простота тестирования: мы можем легко протестировать сам провайдер, потому что его фабрика обычная функция. Нам легче тестировать компонент: в тестах нам не нужно будет собирать дерево зависимостей и подменять много сущностей мы просто передадим по токену ORGANIZATION_INFO стрим с мокаными данными.
  3. Готовность к изменению и расширению: если компонент будет работать с другим типом данных, мы поменяем лишь одну строчку. Если нужно будет изменить преобразование поменяем фабрику. Если потребуется добавить новых данных, то добавим еще один токен мы можем сложить сколько угодно токенов в наш массив провайдеров.


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

Заключение


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

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

Перевод Практическое руководство по использованию Hilt с Kotlin

09.12.2020 18:14:11 | Автор: admin

Будущих учащихся на курсе "Android Developer. Professional" приглашаем посетить открытый урок на тему "Пишем Gradle Plugin"


А также делимся переводом полезной статьи.


Простой способ использовать внедрение зависимостей в приложениях для Android

Hilt это новая библиотека для внедрения зависимостей, построенная на основеDagger. Она позволяет использовать возможности Dagger в приложениях для Android упрощенным способом. В этом руководстве описаны основные функциональные возможности библиотеки и приведено несколько фрагментов кода, которые помогут вам начать использовать Hilt в своих проектах.

Настройка Hilt

Чтобы настроить Hilt в своем приложении, сначала выполните указания из руководства: Установка Gradle Build.

После установки всех необходимых элементов и подключаемых модулей, чтобы использовать Hilt, задайте своему классуApplicationаннотацию@HiltAndroidApp. Больше ничего делать не нужно, а также не нужно вызывать Hilt напрямую.

Определение и внедрение зависимостей

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

  1. Классы, имеющие зависимости, которые вы собираетесь внедрить.

  2. Классы, которые могут быть внедрены как зависимости.

Они не являются взаимоисключающими: во многих случаях класс одновременно является внедряемым и имеет зависимости.

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

Чтобы в Hilt сделать объект внедряемым, необходимо указать для Hilt способ создания экземпляра этого объекта. Такие инструкции называютсяпривязками.

Есть три способа определения привязки в Hilt.

  1. Добавить к конструктору аннотацию@Inject

  2. Использовать@Bindsв модуле

  3. Использовать@Providesв модуле

Добавление к конструктору аннотации@Inject

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

Использование модуля

Два других способа преобразования объектов во внедряемые в Hilt связаны с использованием модулей.

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

Кроме того, в тестах любой модуль можно заменить другим модулем. Например, это позволяет легко заменять реализации интерфейсов мок-объектами.

Модули устанавливаются вкомпонент Hilt, который указывается с помощью аннотации@InstallIn. Я дам более подробное объяснение ниже.

Вариант 1: использовать@Binds, чтобы создать привязку для интерфейса

Если вы хотите использовать в своем кодеOatMilk, когда требуетсяMilk, создайте абстрактный метод внутри модуля и задайте ему аннотацию@Binds. Обращаю внимание, чтобы этот вариант работал, сам OatMilk должен быть внедряемым. Для этого его конструктору необходимо задать аннотацию@Inject.

Вариант 2: создать функцию-фабрику с помощью@Provides

Когда экземпляр нельзя сконструировать напрямую, можно создать поставщик. Поставщик это функция-фабрика, которая возвращает экземпляр объекта.

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

ОбъектContextявляется внедряемым по умолчанию, если задать ему аннотацию@ApplicationContextлибо@ActivityContext.

Внедрение зависимости

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

  1. Как параметры конструктора

  2. Как поля

Как параметры конструктора

Если пометить конструктор аннотацией @Inject, Hilt внедрит все параметры в соответствии с привязками, которые вы определили для этих типов.

Как поля

Если класс является точкой входа, указанной с использованием аннотации@AndroidEntryPoint (подробнее об этом рассказано в следующем разделе), внедрены будут все поля, помеченные аннотацией@Inject.

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

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

Прочие важные понятия

Точка входа

Помните, я сказал, что во многих случаях класс создается путем внедренияиимеет зависимости, внедренные в него? В некоторых случаях у вас будет класс, который несоздаетсяпутем внедрения зависимости, но имеет зависимости, внедренные в него. Хорошим примером этого являются активности, которые в стандартной ситуации создаются платформой Android, а не библиотекой Hilt.

Эти классы являютсяточками входав график зависимостей Hilt, а Hilt необходимо знать, что у них есть зависимости, требующие внедрения.

Точка входа Android

Большинство ваших точек входа будут так называемымиточками входа Android:

  • Activity

  • Fragment

  • View

  • Service

  • BroadcastReceiver

Если это так, такой точке входа следует задать аннотацию@AndroidEntryPoint.

Другие точки входа

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

ViewModel

ViewModel это особый случай: экземпляры этого класса не создаются напрямую, так как они должны создаваться платформой, при этом он также не является точкой входа Android. Вместо этого с классамиViewModel используют специальную аннотацию@ViewModelInject, которая позволяет Hilt внедрять в них зависимости, когда они создаются с помощью выраженияby viewModels(). Это похоже на то, как@Injectработает для других классов.

Если вам требуется доступ к состоянию, сохраняемому в классе ViewModel, внедритеSavedStateHandleв качестве параметра конструктора, добавив аннотацию@Assisted.

Чтобы использовать@ViewModelInject, вам нужно будет добавить еще несколько зависимостей. Дополнительные сведения см. в статье: Интеграции Hilt и Jetpack.

Компоненты

Каждый модуль устанавливается внутрикомпонента Hilt, который указывается с помощью аннотации@InstallIn(<компонент>). Компонент модуля используется главным образом для предотвращения случайного внедрения зависимости не в том месте. Например, аннотация @InstallIn(ServiceComponent.class) не позволит использовать привязки и поставщики, имеющиеся в соответствующем модуле, внутри активности.

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

Области

По умолчанию у привязок нет областей. В приведенном выше примере это означает, что каждый раз, когда вы внедряетеMilk, вы получаете новый экземплярOatMilk. Если добавить аннотацию@ActivityScoped,область использования привязки будет ограничена пределами ActivityComponent.

Теперь, когда у модуля есть область действия, Hilt будет создавать только одинOatMilkна экземпляр активности. Кроме того, этот экземплярOatMilkбудет привязан к жизненному циклу этой активности он будет создаваться при вызовеonCreate()активности и уничтожаться при вызовеonDestroy()активности.

В этом случае иmilk, иmoreMilkбудут указывать на один и тот же экземплярOatMilk. Однако, если у вас несколько экземпляровLatteActivity, у каждого из них будет собственный экземплярOatMilk.

Соответственно, у других зависимостей, внедренных в эту активность, будет та же область, поэтому они тоже будут использовать тот же экземплярOatMilk:

Область зависит от компонента, в который установлен ваш модуль. Например,@ActivityScopedможно применить только к привязкам, находящимся внутри модуля, который установлен внутриActivityComponent.

Область также определяет жизненный цикл внедренных экземпляров: в данном случае одиночный экземплярMilk, используемыйFridgeиLatteActivity, создается, когдаonCreate()вызывается дляLatteActivity, и уничтожается в егоonDestroy(). Это также означает, что нашMilkне переживет изменение конфигурации, поскольку при этом вызываетсяonDestroy()для активности. Преодолеть это можно путем использования области с более длительным жизненным циклом, например@ActivityRetainedScope.

Список имеющихся областей, компонентов, которым они соответствуют, и жизненных циклов, которым они следуют, приведен в статье:Компоненты Hilt.

Внедрение поставщика

Иногда требуется более прямой контроль над созданием внедренных экземпляров. Например, вам требуется, чтобы один или несколько экземпляров какого-то объекта внедрялись только тогда, когда они нужны, в соответствии с бизнес-логикой. В этом случае можно использоватьdagger.Provider.

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

Фреймворки внедрения зависимостей (такие как Dagger иGuice) обычно применяются в крупномасштабных, сложных проектах. В то же время библиотека Hilt, будучи простой в освоении и настройке, предоставляет все возможности Dagger в пакете, который можно использовать в приложениях любого типа, независимо от размеров кодовой базы.

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


Узнать подробнее о курсе "Android Developer. Professional"

Записаться на открытый урок "Пишем Gradle Plugin"


Прямо сейчас вOTUSстартовалановогодняя распродажа. Скидка распространяется абсолютно на все курсы. Сделайте подарок себе или близким -переходите на сайти забирайте курс со скидкой. А в качестве бонуса предлагаем зарегистрироваться на абсолютнобесплатные демо-уроки:

ЗАБРАТЬ СКИДКУ

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru