Головна / Статті

Введення залежностей у TypeScript monorepo: InversifyJS для React та Angular

Введення залежностей у TypeScript monorepo: виберіть контейнер, визначте типобезпечні токени, сформуйте Composition Root та з’єднайте його з React, Vue та Angular.

Видавець: REACTAPP.TOP14 хв читання3191 слів

Джерела: Frontend Dependency Injection: From Context API to Composition Root

Великі кодові бази фронтенду рідко зазнають невдач через один несправний компонент; вони зазнають невдач тому, що бізнес-правила поступово переплітаються з React hooks, HTTP-клієнтами та API для зберігання даних, поки неможливо протестувати чи повторно використовувати щось окремо. Введення залежностей — це механізм низького рівня, який дозволяє тримати ці правила окремими, але лише за умови свідомого проектування структури зв’язків. У цьому посібнику розглядається вибір контейнера для монорепозиторію TypeScript з кількома доменами, визначення типобезпечних токенів, ізоляція кожного домену за допомогою окремого модуля, формування структури в Composition Root та її використання в React, Vue та Angular через мости, складені з десятка рядків кожен.

Чому ізоляцію потрібно визначити ще до моделювання домену

Класичні рекомендації щодо дизайну, заснованого на доменах, радять відкласти деталі реалізації: спочатку створюйте ентитети та сценарії використання, а інструменти обирайте пізніше. На практиці команда, яка розпочинає масштабні зміни в архітектурі, отримує перевагу, вирішуючи заздалегідь одне технічне питання — а саме, як саме буде забезпечуватися ізоляція модулів. Без узгодженого механізму перші кілька доменів формуються спонтанно, і стандарти так і не встановлюються.

Основним принципом є принцип інверсії залежностей: політика високого рівня має залежати від абстракцій, а конкретні деталі — від цих самих абстракцій. Ін’єкція залежностей — це практична техніка, яка його реалізує. Класи запитують абстрактні токени замість конкретних реалізацій, і щось ззовні вирішує, на що саме вони посилаються. У підходах DDD та Clean Architecture це не просто додаткова оптимізація; домен не повинен знати, з якою фреймворком користувацького інтерфейсу, HTTP-клієнтом чи шаром зберігання він працює.

Більшість команд фронтенду спочатку звертаються до React Context (або provide/inject у Vue). Це здається достатнім: оголошується значення біля кореня, і його можна читати будь-де нижче. Однак обмеження проявляються швидко. Немає керування життєвим циклом, немає поняття фабрик чи областей видимості, і кожен споживач значення тепер прив’язаний до середовища виконання React, тож логіка не може працювати чи тестуватися без нього. Шлях від цієї вихідної точки до належного Composition Root — саме про це йдеться у решті цього посібника. Створення власного контейнера рідко є доцільним, тому першим кроком є чесна оцінка існуючих варіантів IoC.

Критерії для контейнера впровадження залежностей у фронтенді

Фреймворки бекенду, такі як NestJS, Spring та .NET, постачаються з механізмом DI як готовим, стандартним рішенням. Код фронтенду має додаткові обмеження: витрати під час виконання, обмеження щодо розміру пакетів та необхідність роботи з кількома фреймворками користувацького інтерфейсу. П’ять критеріїв допомагають зберегти чіткість цілей оцінки:

  1. Агностичність до фреймворку. Контейнер має працювати зі звичайним TypeScript без жодних залежностей від React, Vue чи Angular, а також ефективно функціонувати в браузері, у Node.js для SSR та у React Native.
  2. Модульність. Кожна сфера має можливість надавати заздалегідь налаштований модуль із закритими зв’язками всередині, щоб додаток завантажував лише його. Це допомагає уникнути ситуації, коли файл main.ts містить 500 рядків коду, у яких кожна служба реєструється вручну.
  3. Керування життєвим циклом. Механізми створення синглтонів, тимчасових екземплярів та їх обмеженої реалізації мають бути на першому місці.
  • Тестуваність. Заміна залежності на мок-об’єкт або фейковий екземпляр має займати один рядок, без необхідності перебудови контейнера.
  • Вплив на розмір пакету. Навантаження на продакшн має бути мінімальним як у вигляді неоптимізованого коду, так і оптимізованого з використанням minification та gzip.
  • Кандидати

    InversifyJS

    InversifyJS вже давно є стандартним вибором для великих проектів на TypeScript та є найбільш повним контейнером IoC у цій екосистемі. Він не залежить від конкретної фреймворк-системи та оголошує залежності за допомогою декораторів. Його ContainerModule групує елементи зв’язку в логічні одиниці, а також підтримує режими inSingletonScope, inTransientScope та inRequestScope, дочірні контейнери, завантаження та зняття модулів під час виконання та хуки активації. Недоліком є великий розмір: це був найважчий за розміром варіант, тож називати поточні версії легкими було б оманливо.

    TSyringe

    TSyringe, який підтримується Microsoft, жертвує широтою функціоналу заради зручності. Він автоматично вирішує проблему залежностей конструкторів на основі метаданих TypeScript, пропонує розширений набір декораторів та підтримує звичайні моделі життєвого циклу (Singleton, Transient, ResolutionScoped, ContainerScoped). Для середнього за розміром додатку це привабливий варіант із мінімальними вимогами до налаштувань. Однак у архітектурі з кількома доменами є два недоліки: відсутній концепт модулів першого класу, тому композицію доводиться створювати вручну за допомогою функцій реєстрації або createChildContainer(), а також він залежить від polyfillу reflect-metadata, що значно збільшує об’єм коду.

    Awilix

    Awilix повністю уникає декораторів. Він використовує API ES6 Proxy для прив’язки залежностей до імен параметрів конструктора або до ключів об’єкта аргументів фабрики. Це робить його ідеальним вибором для команд, які не хочуть використовувати декоратори, і це був найменший самостійний контейнер, який було протестовано. Він підтримує режими життєвого циклу SINGLETON, SCOPED та TRANSIENT; модулі організовані за допомогою користувацьких функцій реєстрації.

    Одна проблема, специфічна для фронтенду, заслуговує уваги. У режимі ін’єкції CLASSIC Awilix читає імена параметрів конструктора як рядки, а мініфайери змінюють їхні назви, тому процес розрішення зламується у продакшені. Лише стандартний режим PROXY витримує агресивну обробку коду.

    Вбудований інжектор Angular

    Angular має одну з найсильніших ієрархічних систем DI у розробці фронтенду, яка включає в себе вбудовану ін’єкцію на основі токенів, провайдери з обмеженим діапазоном дії та лежачу завантаження. Якби її можна було використовувати без рунтайму Angular, вона була б серйозним конкурентом для реалізації доменного шару. Однак це неможливо, і саме в цьому проблема.

    React Context

    Context насправді не є контейнером DI; це спосіб уникнення проблеми передачі значень через безліч параметрів. Провайдер розташовується близько до кореня, а компоненти отримують до нього доступ за допомогою useContext(). Будь-яка зміна значення, яке надається, змушує переробити всі компоненти-споживачі, причому немає жодного життєвого циклу, жодного прив’язування через фабрики та жодної ізоляції діапазону дії. Її єдиною справжньою перевагою є те, що вона не додає жодних додаткових об’ємів до пакету.

    Vue provide та inject

    Механізм Vue передає значення вниз по дереву компонентів та дотримується архітектурних обмежень Context: він не є практичною основою для графа доменних сервісів. Проте у нього є одна ергономічна перевага — залежності можна реєструвати глобально через плагін, замість того щоб обгортати дерево в вкладені провайдери. Сучасна функція inject() у Angular ґрунтується на схожій ідеї активного контексту ін’єкції.

    Проста ін’єкція через конструктор

    Підхід без додаткових бібліотек передає кожну залежність через конструктори явно. Він є повністю безпечним з точки зору типів та не має додаткових витрат під час виконання. Недоліком є масштабованість: коли у додатку є приблизно 10–15 доменних сервісів, ручне підключення елементів графа в точці входу перетворюється на великий, крихкий файл, до якого всі бояться торкатися.

    Що показує порівняння

    Розміри бундлів порівнювалися шляхом об’єднання вхідного файлу, який імпортує публічний API кожного пакету, за допомогою команди esbuild --bundle --minify --platform=browser, та стиснення отриманого результату за допомогою gzip -9; це дозволяє відокремити власні витрати контейнера від коду додатку. Було зроблено чотири висновки:

    1. Механізм інжекції з фреймворку належить до частини користувацького інтерфейсу, а не до сфери бізнес-логіки. Використання функції useContext() або атрибута @Injectable() у бібліотеках сфери бізнес-логіки призводить до прив’язки бізнес-правил до одного фреймворку, що порушує перше критерій.
    2. Ручна інжекція підходить лише для невеликих проектів. У великому корпоративному монорепозиторії механізм Composition Root перетворюється на тисячі рядків вручну написаних зв’язків між компонентами.
  • InversifyJS переміг завдяки модульності. Це єдиний кандидат, у якого модуль (ContainerModule) є першокласною одиницею, яку бібліотека може створити, інкапсулювати та експортувати як єдиний цілий.
  • Конфігурація компіляції має значення. InversifyJS та TSyringe потребують у файлі tsconfig.json значень experimentalDecorators: true, emitDecoratorMetadata: true та target: ES2022. InversifyJS v8 більше не потребує окремого polyfill для reflect-metadata. За допомогою інструментів на основі esbuild або SWC (Vite, Next.js, Bun) необхідно переконатися, що етап трансформації підтримує старі метадані декораторів, зазвичай за допомогою плагіну, оскільки esbuild сам по собі не генерує метадані декораторів.
  • Чотири правила, які запобігають перетворенню контейнера на пошуковик сервісів

    Сам по собі контейнер нічого не ізолює; якщо ним недбало користуватися, він перетворюється на глобальну сумку сервісів, до якої може отримати доступ будь-який файл. Тому архітектура ґрунтується на чотирьох правилах:

    • Абстрактні токени DI, створені за допомогою Symbol.for та властивості $.
    • Кожна бібліотека домену експортує лише свій модуль контейнера, а не свої конкретні класи.
    • Єдиний корень композиції складає граф застосунку.
    • Простий міст для кожної платформи користувацького інтерфейсу забезпечує доступ до контейнера для компонентів.

    Безпека типів за допомогою токенів від Symbol.for та $

    Контейнеру потрібні ідентифікатори під час виконання, щоб відповідати абстракціям конкретним реалізаціям. Інтерфейси TypeScript зникають під час компіляції, тому вони не можуть безпосередньо виконувати цю функцію. Тут використовується паттерн, який поєднує кожен інтерфейс у пакеті @my-app/*-contracts із константою тієї самої назви, чия властивість $ містить об’єкт типу Symbol.

    // libs/auth/contracts/src/lib/interfaces/auth.facade.ts
    import type { ServiceIdentifier } from 'inversify';
    
    export interface AuthFacade {
      getAccessToken(): Promise<string | null>;
      login(): Promise<void>;
      logout(): Promise<void>;
    }
    export const AuthFacade = {
      $: Symbol.for('AuthFacade') as ServiceIdentifier<AuthFacade>,
    };
    
    // libs/auth/contracts/src/lib/interfaces/pin.facade.ts
    export interface PinFacade {
      setupPin(pin: string): Promise<void>;
      verifyPin(sub: string, pin: string): Promise<PinVerifyResult>;
      changePin(sub: string, currentPin: string, newPin: string): Promise<PinVerifyResult>;
    }
    export const PinFacade = {
      $: Symbol.for('PinFacade') as ServiceIdentifier<PinFacade>,
    };
    

    У цьому уривку містяться три рішення:

    1. Одна назва для типу та значення. TypeScript зберігає типи та значення у окремих просторах імен, тож AuthFacade може бути водночас інтерфейсом та контейнером токенів. Компілятор вибирає правильне значення з контексту, і нікому не потрібно вигадувати назви на кшталт AuthFacadeToken.
    2. Symbol.for замість Symbol(). Кожен виклик Symbol() повертає абсолютно нове значення, тоді як Symbol.for() шукає ключ у глобальному реєстрі символів часу виконання. Якщо пакет contracts опиняється у двох бундлах, що може трапитися в монорепо або з Module Federation, обидві копії все одно створюють однаковий токен, і пошуки продовжують працювати, замість того щоб таємниче зламатися. Недолік полягає у тому, що ключі реєстру є глобальними рядками, тому вони мають бути унікальними в усьому додатку.
    3. Перетворення ServiceIdentifier<AuthFacade>. Введення $ як ServiceIdentifier<T> від Inversify пов’язує токен із його інтерфейсом на етапі компіляції, тож місця резолюції можуть визначити правильний тип без необхідності використання явних генериків.

    Наступний уривок демонструє обидві ситуації: клас, який отримує фасад через вставку конструктора, та прямий виклик container.get, тип значення якого визначається на основі токена.

    import { inject, injectable } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    
    @injectable()
    export class LoginPageComponent {
      // Strongly-typed injection via token
      constructor(@inject(AuthFacade.$) private readonly auth: AuthFacade) {}
    }
    
    // Resolution site automatically infers return type as AuthFacade
    const facade = container.get(AuthFacade.$); // AuthFacade
    

    Користувач посилається лише на контракт. Він не має жодного уявлення про те, що знаходиться всередині @my-app/auth-core, і саме в цьому суть.

    Один домен, один ContainerModule

    Основна бібліотека кожного домену надає лише одну річ — свій модуль DI. Сценарії використання, ентитети, порти та репозиторії залишаються приватними. Усередині модуля внутрішні сценарії використання прив’язані до самих себе, тоді як публічний фасад — до свого токена контракту.

    // libs/auth/core/src/lib/auth-container.module.ts
    import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    import { CoreAuthFacade } from './facades/core-auth.facade';
    import { LoginUseCase } from './use-cases/login.use-case';
    import { LogoutUseCase } from './use-cases/logout.use-case';
    import { GetAccessTokenUseCase } from './use-cases/get-access-token.use-case';
    
    export const authContainerModule = new ContainerModule((options: ContainerModuleLoadOptions) => {
      // Private use cases (registered to self within domain)
      options.bind(LoginUseCase).toSelf().inSingletonScope();
      options.bind(GetAccessTokenUseCase).toSelf().inSingletonScope();
      options.bind(LogoutUseCase).toSelf().inSingletonScope();
      // Public contract implementation bound to token
      options.bind(AuthFacade.$).to(CoreAuthFacade).inSingletonScope();
    });
    
    // libs/auth/core/src/index.ts - ONLY the ContainerModule is re-exported!
    export * from './lib/auth-container.module';
    

    Інкапсуляція забезпечується двома способами:

    1. Публічний API. Файл index.ts бібліотеки знову експортує лише authContainerModule. Класи на кшталт LoginUseCase чи CoreAuthFacade просто недоступні з інших пакетів.
    2. Правила лінтингу. Правило ESLint @nx/enforce-module-boundaries від Nx перевіряє теги проекту, тож, наприклад, проект із тегом type:core не може імпортувати проект із тегом type:core з іншої доменної структури. Деталі щодо оголошення тегів та обмежень можна знайти у документації Nx щодо меж модулів.

    Межі експорту запобігають випадковим імпортам, а правила лінтингу — навмисному використанню складних шляхів. Разом вони роблять архітектуру чимось, що перевіряється системою CI, а не чимось, що потрібно пам’ятати рецензентам.

    Корень композиції

    Корень композиції — це єдине місце, де створюється граф залежностей, зазвичай apps/my-app/src/composition-root.ts або функція, яка викликається з main.ts. Його керує одне правило:

    Корень композиції — це єдине місце в кодовій базі, де дозволено імпортувати конкретні реалізації, адаптери інфраструктури та основні модулі домену.

    Наведена нижче функція спочатку під’єднує глобальну інфраструктуру (HTTP-клієнт на основі Axios та логгер, створений за допомогою фабрики, щоб він міг приймати налаштовані способи передачі даних), а потім завантажує модулі домену.

    // apps/my-app/src/composition-root.ts
    import { Container } from 'inversify';
    import { HttpClientPort, LoggerPort } from '@my-app/shared-contracts';
    import { AxiosHttpClient, BrowserLogger, ConsoleLogTransport, FileLogTransport } from '@my-app/shared-infrastructure';
    
    import { authContainerModule } from '@my-app/auth-core';
    import { paymentsContainerModule } from '@my-app/payments-core';
    export const initAppContainer = (): Container => {
      const container = new Container();
    
      // 1. Bind global infrastructure adapters
      container.bind(HttpClientPort.$).to(AxiosHttpClient).inSingletonScope();
      container.bind(LoggerPort.$).toDynamicValue(() => new BrowserLogger({
        transports: [
          new ConsoleLogTransport(),
          new FileLogTransport(),
        ],
      })).inSingletonScope();
    
      // 2. Load domain modules
      container.load(authContainerModule);
      container.load(paymentsContainerModule);
    
      return container;
    };
    

    Для повноти картини, адаптер BrowserLogger — це звичайний клас, який реалізує LoggerPort та передає кожне повідомлення до відповідних каналів передачі даних. Зауважте, що він не містить жодних декораторів; оскільки він створюється всередині функції toDynamicValue, контейнеру ніколи не потрібно перевіряти його конструктор.

    // libs/shared/infrastructure/src/lib/logging/browser-logger.ts
    import type { LoggerPort } from '@my-app/shared-contracts';
    import type { LogTransport } from './interfaces/log-transport.interface';
    
    export interface BrowserLoggerOptions {
      transports: LogTransport[];
    }
    
    export class BrowserLogger implements LoggerPort {
      constructor(private readonly options: BrowserLoggerOptions) {}
    
      info(message: string, ...args: unknown[]): void {
        this.options.transports.forEach(t => t.write('INFO', message, args));
      }
    }
    

    Кілька додатків з одного ядра домену

    Оскільки інфраструктура обирається у Composition Root, різні додатки можуть повторно використовувати ті самі пакети домену з різними адаптерами та різним вибором модулів. Додаток React Native може використовувати сховище, підтримуване AsyncStorage, тоді як веб-додаток для адміністрування використовує IndexedDB та завантажує домен аудиту замість домену оплат.

    // apps/mobile/src/composition-root.ts — React Native target
    container.bind(StoragePort.$).to(AsyncStorageAdapter).inSingletonScope();
    container.bind(LoggerPort.$).to(RnLogger).inSingletonScope();
    container.load(authContainerModule);
    container.load(paymentsContainerModule);
    
    // apps/admin/src/composition-root.ts - Web Admin target
    container.bind(StoragePort.$).to(IndexedDbAdapter).inSingletonScope();
    container.bind(LoggerPort.$).to(BrowserLogger).inSingletonScope();
    container.load(authContainerModule);
    container.load(auditContainerModule); // Payments domain excluded entirely
    

    @my-app/auth-core залишається абсолютно ідентичним байт за байтом у версіях для мобільних пристроїв, адміністраторів та десктопу. Він ніколи не дізнається, чи є сховище типу AsyncStorage чи IndexedDB.

    Дозвіл одній доменній системі використовувати іншу

    Припустимо, що payments-core потребує токена доступу, яким керує auth-core. Імпорт @my-app/auth-core заборонений правилами меж. Натомість сценарій оплати використовує токен контракту домену автентифікації, який знаходиться у пакеті contracts та дозволений до використання.

    // libs/payments/core/src/lib/use-cases/create-payment.use-case.ts
    import { inject, injectable } from 'inversify';
    
    // Import from contracts, NOT core — permitted by boundary rules
    import { AuthFacade } from '@my-app/auth-contracts';
    import { PaymentsGatewayPort } from '../ports/payments-gateway.port';
    
    @injectable()
    export class CreatePaymentUseCase {
      constructor(
        @inject(AuthFacade.$) private readonly auth: AuthFacade,
        @inject(PaymentsGatewayPort.$) private readonly gateway: PaymentsGatewayPort,
      ) {}
    
      async execute(amount: number): Promise<void> {
        const token = await this.auth.getAccessToken();
        return this.gateway.processPayment(amount, token);
      }
    }
    

    paymentsContainerModule не прив’язує AuthFacade.$; він лише його використовує. Прив’язка відбувається тоді, коли Composition Root завантажує обидва модулі в один і той самий контейнер. Практичні наслідки: якщо додаток завантажує функції оплати без автентифікації, процес розрішення відбувається з помилкою під час виконання, тому варто проводити тест на кожній меті додатку, щоб переконатися, що кожен публічний токен розрішується один раз.

    З’єднання контейнера з React, Vue та Angular

    Оскільки контейнер нічого не знає про будь-які бібліотеки користувацького інтерфейсу, кожна фреймворк-система отримує невеликий адаптер довжиною приблизно 10–15 рядків.

    React

    Контейнер створюється один раз під час запуску та більше не замінюється, тому значення контексту ніколи не змінюється. Це уникає ланцюгової переробки, яка зазвичай пов’язана з Context: користувачі отримують стабільний посилання. useInjection зберігає результат пошуку для кожного контейнера та токена та кидає зрозумілу помилку, якщо постачальник відсутній.

    // libs/shared/react-di/src/lib/di-context.tsx
    import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
    import type { Container, ServiceIdentifier } from 'inversify';
    
    export interface DIProviderProps {
      container: Container;
      children: ReactNode;
    }
    
    const DIContext = createContext<Container | null>(null);
    
    export const DIProvider = ({ container, children }: DIProviderProps) => (
      <DIContext.Provider value={container}>
        {children}
      </DIContext.Provider>
    );
    
    export const useInjection = <T,>(token: ServiceIdentifier<T>): T => {
      const container = useContext(DIContext);
    
      return useMemo(() => {
        if (!container) {
          throw new Error('useInjection must be used within a DIProvider');
        }
    
        return container.get(token);
      }, [container, token]);
    };
    

    Vue

    Система плагінів Vue та типізований InjectionKey роблять адаптер ще коротшим. Плагін забезпечує контейнер на рівні додатку, а композабл розпізнає з нього токени.

    // libs/shared/vue-di/src/lib/di-plugin.ts
    import type { App, InjectionKey } from 'vue';
    import { inject } from 'vue';
    import type { Container, ServiceIdentifier } from 'inversify';
    
    export const DI_CONTAINER: InjectionKey<Container> = Symbol('DI_CONTAINER');
    
    export const diPlugin = {
      install(app: App, { container }: { container: Container }) {
        app.provide(DI_CONTAINER, container);
      },
    };
    
    export const useInjection = <T>(token: ServiceIdentifier<T>): T => {
      const container = inject(DI_CONTAINER);
    
      if (!container) {
        throw new Error('diPlugin is not installed');
      }
    
      return container.get(token);
    };
    

    Angular

    Власний ієрархічний інжектор Angular може делегувати функції до контейнера домену через постачальників-фабрик. Передача контейнера за допомогою InjectionToken та створення нового контейнера під час кожного запуску запобігають тому, щоб одночасні запити, оброблені на сервері, ділилися станом.

    // apps/angular-app/src/app/domain.providers.ts
    import { InjectionToken, type Provider } from '@angular/core';
    import type { Container } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    import { initAppContainer } from './composition-root';
    
    export const DI_CONTAINER = new InjectionToken<Container>('DI_CONTAINER');
    export const AUTH_FACADE = new InjectionToken<AuthFacade>('AUTH_FACADE');
    
    export const provideDomainContainer = (container: Container): Provider[] => [
      {
        provide: DI_CONTAINER,
        useValue: container,
      },
      {
        provide: AUTH_FACADE,
        useFactory: (c: Container) => c.get(AuthFacade.$),
        deps: [DI_CONTAINER],
      },
    ];
    
    // main.ts - fresh container instance created per bootstrap
    bootstrapApplication(AppComponent, {
      providers: [...provideDomainContainer(initAppContainer())],
    });
    

    Кожен додатковий фасад компонентів Angular потребує власного InjectionToken та постачальника фабрики, тому список зростає разом із публічним API; його генерація з невеликого мапу є варіантом, коли список стає довгим.

    Витрати та коли можна це пропустити

    Налаштування додає близько 21 КБ у стиснутому вигляді та вимагає використання emitDecoratorMetadata під час збірки. Для корпоративного монорепозиторію з десятьма чи більше доменами ці витрати швидко окупуються. Для додатку з двома-трьома екранами така ізоляція є зайвим навантаженням; звичайна ін’єкція через конструктор чи навіть синглтони на рівні модуля підійдуть краще.

    Правила ін’єкції залежностей у фронтенді

    • Тримайте механізми ін’єкції залежностей фреймворку (Context, provide/inject, Angular providers) на периферії UI; ядро домену має залежати лише від токенів контракту.
  • Спочатку оберіть контейнер для модульної структури: домен, який експортує один запечатаний ContainerModule, допомагає зберегти невеликий вхідний точку.
  • Використовуйте токени Symbol.for типу ServiceIdentifier<T>, щоб одночасно уникнути дублювання пакетів та зберегти функціонал інференції типів.
  • Забезпечуйте чіткі межі за допомогою експортів та правил лінтингу, а не лише конвенцій.
  • Нехай саме один файл — кореневий файл композиції — взаємодіє з конкретними реалізаціями; тоді багатоплатформенне будування проекту стає просто заміною зв’язків.
  • Пам’ятайте, що ін’єкція — це лише інструмент. Метою є інверсія: якщо ви можете замінити React на Vue, SQLite на IndexedDB або Axios на Fetch без змін у рівні домену, стратегія виконала свою роль.
  • Пов’язана література

  • Вибір структури папок у React: сім лейаутів та їхні межі функціональності: Порівняйте лейаути типу flat, type-based, feature-based, Atomic Design, DDD, Feature-Sliced Design та monorepo для React та дізнайтеся, на якому рівні ескалації кожен з них перестає працювати.
  • Перенесення Angular HttpClient до серверного бекенду fetch за допомогою withFetch(): Дізнайтеся, чому Angular HttpClient переходить від XMLHttpRequest до fetch, як withFetch() дозволяє реалізувати edge SSR та стрімінг, та як це впливає на ваші інтерцептори.
  • Як Angular Dependency Injection вирішує проблему обслуговування сервісів за допомогою inject(): Дізнайтеся, як Angular DI надає сервіси через функцію inject(), елементи providers та ієрархічні injectors, а також як визначати область дії екземплярів та уникати помилок через відсутність провайдерів.
  • SOLID як питання до змін: рефакторинг сервісу Spring Boot: Навчіться застосовувати кожен принцип SOLID у бекенді Spring Boot, ставлячи запитання про те, що може змінитися, та як уникати надмірної складності, яку часто спричиняє дотримання цих принципів.