Просунутий Dependency Injection в Angular 2026: Providers, Tokens та Питання на Співбесідах

Комплексний посібник з просунутих механізмів Dependency Injection в Angular. Providers, InjectionToken, ієрархія інжекторів та найпоширеніші питання на технічних співбесідах.

Advanced Angular Dependency Injection illustration

Dependency Injection (DI) є фундаментом архітектури Angular і одним з найважливіших патернів проєктування, що використовуються у цьому фреймворку. Хоча базові концепції DI відносно прості для освоєння, просунуті техніки вимагають глибшого розуміння механізмів роботи інжекторів, провайдерів та токенів.

У цій статті розглядаються просунуті аспекти системи DI в Angular 19, які є ключовими як для щоденної роботи розробника, так і для технічних співбесід на позиції senior та lead developer.

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

Ієрархія Інжекторів в Angular

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

Angular визначає три основні рівні інжекторів:

  1. Platform Injector - найвищий рівень, спільний для всіх Angular-застосунків на сторінці
  2. Root Injector - рівень застосунку, створюється під час bootstrap
  3. Element Injector - рівень компонента або директиви
typescript
// Приклад ієрархії інжекторів
@Injectable({
  providedIn: 'root' // Реєстрація в Root Injector
})
export class GlobalConfigService {
  readonly apiUrl = 'https://api.example.com';
}

@Injectable()
export class FeatureService {
  // Може бути зареєстрований на рівні модуля або компонента
}

@Component({
  selector: 'app-feature',
  providers: [FeatureService], // Element Injector
  template: `<div>Feature Component</div>`
})
export class FeatureComponent {
  constructor(
    private globalConfig: GlobalConfigService,
    private featureService: FeatureService
  ) {}
}

Процес розв'язання залежностей починається з найближчого інжектора і просувається вгору по ієрархії до знаходження відповідного провайдера або генерування помилки.

Типи Провайдерів та їх Застосування

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

useClass - Базова Реалізація

typescript
// Базове використання useClass
const providers = [
  { provide: LoggerService, useClass: LoggerService },
  // Скорочена форма:
  LoggerService
];

// Заміна реалізації
@NgModule({
  providers: [
    { provide: LoggerService, useClass: AdvancedLoggerService }
  ]
})
export class AppModule {}

useValue - Статичні Значення

typescript
// Конфігурація застосунку як значення
export interface AppConfig {
  apiUrl: string;
  debugMode: boolean;
  maxRetries: number;
}

export const APP_CONFIG: AppConfig = {
  apiUrl: 'https://api.example.com',
  debugMode: false,
  maxRetries: 3
};

// Токен для конфігурації
export const APP_CONFIG_TOKEN = new InjectionToken<AppConfig>('app.config');

// Реєстрація
@NgModule({
  providers: [
    { provide: APP_CONFIG_TOKEN, useValue: APP_CONFIG }
  ]
})
export class AppModule {}

// Використання
@Injectable({ providedIn: 'root' })
export class ApiService {
  constructor(@Inject(APP_CONFIG_TOKEN) private config: AppConfig) {
    console.log(this.config.apiUrl);
  }
}

useFactory - Динамічне Створення Екземплярів

typescript
// Factory із залежностями
export function loggerFactory(
  http: HttpClient,
  config: AppConfig
): LoggerService {
  if (config.debugMode) {
    return new DebugLoggerService(http);
  }
  return new ProductionLoggerService(http);
}

@NgModule({
  providers: [
    {
      provide: LoggerService,
      useFactory: loggerFactory,
      deps: [HttpClient, APP_CONFIG_TOKEN]
    }
  ]
})
export class AppModule {}

useExisting - Псевдоніми Сервісів

typescript
// Створення псевдоніма для існуючого сервісу
@Injectable({ providedIn: 'root' })
export class AuthService {
  isAuthenticated(): boolean { return true; }
}

abstract class AuthChecker {
  abstract isAuthenticated(): boolean;
}

@NgModule({
  providers: [
    { provide: AuthChecker, useExisting: AuthService }
  ]
})
export class AppModule {}

InjectionToken - Безпечна Типізація

InjectionToken забезпечує безпечний спосіб визначення токенів для значень, що не є класами.

typescript
// Визначення токенів з типовими значеннями
export const API_BASE_URL = new InjectionToken<string>('api.baseUrl', {
  providedIn: 'root',
  factory: () => 'https://api.default.com'
});

export const FEATURE_FLAGS = new InjectionToken<Map<string, boolean>>(
  'feature.flags',
  {
    providedIn: 'root',
    factory: () => new Map([
      ['darkMode', true],
      ['newDashboard', false]
    ])
  }
);

// Токен із залежністю від інших сервісів
export const COMPUTED_CONFIG = new InjectionToken<ComputedConfig>(
  'computed.config',
  {
    providedIn: 'root',
    factory: () => {
      const env = inject(EnvironmentService);
      return {
        apiUrl: env.isProd ? 'https://api.prod.com' : 'https://api.dev.com',
        timeout: env.isProd ? 5000 : 30000
      };
    }
  }
);

Функція inject() та Сучасні Патерни

Angular 19 просуває використання функції inject() як переважного методу впровадження залежностей.

typescript
// Традиційне впровадження через конструктор
@Injectable({ providedIn: 'root' })
export class TraditionalService {
  constructor(
    private http: HttpClient,
    private logger: LoggerService
  ) {}
}

// Сучасний підхід з inject()
@Injectable({ providedIn: 'root' })
export class ModernService {
  private http = inject(HttpClient);
  private logger = inject(LoggerService);
  
  // Умовне впровадження
  private optionalService = inject(OptionalService, { optional: true });
}

// inject() у допоміжних функціях
export function createDataFetcher<T>(url: string) {
  const http = inject(HttpClient);
  const errorHandler = inject(ErrorHandlerService);
  
  return {
    fetch: () => http.get<T>(url).pipe(
      catchError(err => {
        errorHandler.handle(err);
        return EMPTY;
      })
    )
  };
}

Модифікатори Впровадження

Angular пропонує низку модифікаторів для контролю процесу розв'язання залежностей.

typescript
@Component({
  selector: 'app-example',
  template: `<div>Example</div>`
})
export class ExampleComponent {
  constructor(
    // @Optional() - не генерує помилку при відсутності провайдера
    @Optional() private optionalService: OptionalService | null,
    
    // @Self() - шукає лише в поточному інжекторі
    @Self() private selfService: SelfService,
    
    // @SkipSelf() - пропускає поточний інжектор
    @SkipSelf() private parentService: ParentService,
    
    // @Host() - шукає до рівня хоста включно
    @Host() private hostService: HostService
  ) {}
}

// Комбінації модифікаторів
@Component({
  selector: 'app-combined',
  template: `<div>Combined</div>`
})
export class CombinedComponent {
  constructor(
    @Optional() @SkipSelf() private parentConfig: ConfigService | null
  ) {
    // Шукає в батьківському, але не генерує помилку при відсутності
  }
}

Multi Providers - Множинні Реалізації

Multi providers дозволяють реєструвати декілька значень під одним токеном.

typescript
// Визначення токена для інтерсепторів
export const HTTP_INTERCEPTORS_TOKEN = new InjectionToken<HttpInterceptor[]>(
  'http.interceptors'
);

// Реєстрація декількох інтерсепторів
@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS_TOKEN,
      useClass: AuthInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS_TOKEN,
      useClass: LoggingInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS_TOKEN,
      useClass: ErrorInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}

// Використання всіх інтерсепторів
@Injectable()
export class HttpService {
  constructor(
    @Inject(HTTP_INTERCEPTORS_TOKEN) 
    private interceptors: HttpInterceptor[]
  ) {
    console.log(`Loaded ${interceptors.length} interceptors`);
  }
}

Провайдери на Рівні Компонента

Реєстрація провайдерів на рівні компонента створює ізольовані екземпляри сервісів.

typescript
@Injectable()
export class FormStateService {
  private formData = signal<Record<string, unknown>>({});
  
  updateField(key: string, value: unknown) {
    this.formData.update(data => ({ ...data, [key]: value }));
  }
  
  getFormData() {
    return this.formData();
  }
  
  reset() {
    this.formData.set({});
  }
}

@Component({
  selector: 'app-user-form',
  providers: [FormStateService], // Новий екземпляр для кожної форми
  template: `
    <form>
      <input (input)="onInput('name', $event)" />
      <input (input)="onInput('email', $event)" />
    </form>
  `
})
export class UserFormComponent {
  private formState = inject(FormStateService);
  
  onInput(field: string, event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.formState.updateField(field, value);
  }
}

Тестування з Dependency Injection

Правильне використання DI значно спрощує тестування застосунків.

typescript
describe('UserService', () => {
  let service: UserService;
  let httpMock: jasmine.SpyObj<HttpClient>;
  
  beforeEach(() => {
    httpMock = jasmine.createSpyObj('HttpClient', ['get', 'post']);
    
    TestBed.configureTestingModule({
      providers: [
        UserService,
        { provide: HttpClient, useValue: httpMock },
        { provide: API_BASE_URL, useValue: 'https://test.api.com' }
      ]
    });
    
    service = TestBed.inject(UserService);
  });
  
  it('should fetch users from correct URL', () => {
    httpMock.get.and.returnValue(of([]));
    
    service.getUsers();
    
    expect(httpMock.get).toHaveBeenCalledWith(
      'https://test.api.com/users'
    );
  });
});

// Тестування з overrideProvider
describe('FeatureComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FeatureComponent]
    }).overrideProvider(FeatureService, {
      useValue: { getData: () => of(mockData) }
    });
  });
});

Готовий до співбесід з Angular?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Поширені Питання на Співбесідах

Нижче наведено типові питання щодо DI, які зустрічаються на технічних співбесідах на позиції Angular developer.

Питання 1: Яка різниця між providedIn: 'root' та реєстрацією в NgModule?

providedIn: 'root' реєструє сервіс як singleton у root інжекторі та підтримує tree-shaking - сервіс буде видалено з бандлу, якщо він не використовується. Реєстрація в NgModule не підтримує автоматичний tree-shaking і вимагає явного імпорту модуля.

Питання 2: Коли використовувати @Self() vs @SkipSelf()?

@Self() обмежує пошук провайдера поточним element інжектором - корисно, коли компонент потребує власного екземпляра сервісу. @SkipSelf() пропускає поточний інжектор і починає пошук з батьківського - використовується, коли компоненту потрібен доступ до екземпляра з вищого рівня ієрархії.

Питання 3: Як реалізувати патерн Strategy за допомогою DI?

typescript
// Інтерфейс стратегії
abstract class PaymentStrategy {
  abstract process(amount: number): Observable<PaymentResult>;
}

// Реалізації
@Injectable()
export class CreditCardStrategy extends PaymentStrategy {
  process(amount: number) {
    return of({ success: true, method: 'credit_card' });
  }
}

@Injectable()
export class PayPalStrategy extends PaymentStrategy {
  process(amount: number) {
    return of({ success: true, method: 'paypal' });
  }
}

// Динамічний вибір стратегії
export const PAYMENT_STRATEGY = new InjectionToken<PaymentStrategy>(
  'payment.strategy'
);

// Конфігурація в модулі
@NgModule({
  providers: [
    {
      provide: PAYMENT_STRATEGY,
      useFactory: (config: AppConfig) => {
        return config.defaultPayment === 'paypal'
          ? new PayPalStrategy()
          : new CreditCardStrategy();
      },
      deps: [APP_CONFIG_TOKEN]
    }
  ]
})
export class PaymentModule {}

Питання 4: Як уникнути circular dependency в DI?

Circular dependencies можна вирішити через:

  • Використання forwardRef() для відкладеного розв'язання посилання
  • Рефакторинг до спільного посередницького сервісу
  • Застосування патерну mediator
  • Використання InjectionToken з factory
typescript
// forwardRef для циклічних залежностей
@Injectable()
export class ServiceA {
  constructor(
    @Inject(forwardRef(() => ServiceB)) 
    private serviceB: ServiceB
  ) {}
}

Найкращі Практики

  1. Віддавати перевагу providedIn: 'root' для синглтонів застосунку - підтримує tree-shaking
  2. Використовувати InjectionToken для значень, що не є класами
  3. Застосовувати функцію inject() замість впровадження через конструктор у нових проєктах
  4. Уникати провайдерів у компонентах, якщо не потрібні ізольовані екземпляри
  5. Документувати складні factory коментарями, що пояснюють логіку створення
  6. Тестувати конфігурацію DI через unit-тести, що перевіряють правильність впровадження

Висновок

Просунуті механізми Dependency Injection в Angular 19 надають потужні інструменти для управління залежностями в застосунках. Розуміння ієрархії інжекторів, різноманіття провайдерів та сучасних патернів з функцією inject() є критично важливим для побудови масштабованих та тестованих застосунків.

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

Щоденний виклик

Чи знайдеш ти помилку в Angular?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 13 вересня 2026 р.

Теги

#angular
#dependency-injection
#typescript
#interview

Поділитися

Пов'язані статті