Mert Özen Her Satırda Daha İleri
Frontend Angular — #10

Angular Service ve Dependency Injection: providedIn, Injector Ağacı ve Provider'lar

Mert Özen 31 Eki 2026 7 dk 1 görüntülenme
Angular Service ve Dependency Injection: providedIn, Injector Ağacı ve Provider'lar

Service neden var, providedIn: 'root' ile singleton, constructor injection, component seviyesinde providers ve hiyerarşik injector, useClass/useValue/useFactory ve InjectionToken ile config sağlamak.

Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.

Component Her İşi Kendisi Yapmamalı

Bir e-ticaret sayfası düşün. Ortada ürün listesi var, sağ üst köşede de "Sepet: 3 ürün" yazan küçük bir rozet. Kullanıcı listeden bir ürünü sepete eklediğinde rozetteki sayı artmalı. İki component de AppComponent'in altında, yani kardeşler.

Daha önce gördüğümüz @Input ve @Output ile bunu çözebilirsin: liste bir event fırlatır, parent onu yakalar, sayıyı rozete input olarak geçer. İki kardeş için idare eder. Sepeti bir de ödeme sayfası, bir de ürün detay sayfası bilmek isteyince veri, component ağacında yukarı aşağı taşınan bir kova suya döner.

Apartmandaki merkezi kalorifer kazanı gibi bir çözüm lazım. Her daire kendi kazanını kurmaz; bodrumda tek bir kazan var, her daire boruyu bağlar ve ısınır. Angular'da o kazanın adı service, boruları bağlayan tesisatçının adı da dependency injection.

İlk Service'imiz

Proje klasöründe şu komutu çalıştır:

ng generate service cart

Kısaltması ng g s cart. CLI, src/app altına cart.service.ts ve test dosyasını koyar. Bu sefer app.module.ts güncellenmez; sebebini birazdan göreceksin. Oluşan dosyayı şöyle dolduralım:

import { Injectable } from '@angular/core';

export interface CartItem {
  name: string;
  price: number;
}

@Injectable({
  providedIn: 'root'
})
export class CartService {
  private items: CartItem[] = [];

  add(item: CartItem): void {
    this.items.push(item);
  }

  clear(): void {
    this.items = [];
  }

  get count(): number {
    return this.items.length;
  }

  get total(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

Service dediğimiz şey sıradan bir TypeScript class'ı. Template'i yok, selector'ı yok. Sepetteki ürünleri tutuyor, ekleme ve temizleme metodları sunuyor, sayıyı ve toplamı hesaplıyor. items dizisi private, çünkü dışarıdaki kimsenin sepete service'i atlayarak dokunmasını istemiyoruz.

Farkı yaratan, üstteki @Injectable decorator'ı. providedIn: 'root' ayarı Angular'a "bu service'ten uygulama genelinde bir tane oluştur, isteyen herkese onu ver" der. Service'i kendisi kaydettiği için hiçbir module'ün providers listesine eklemene gerek yok. CLI'nin app.module.ts'e dokunmamasının sebebi bu.

Constructor Injection

Şimdi ürün listesi service'i kullansın:

import { Component } from '@angular/core';
import { CartItem, CartService } from '../cart.service';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {
  products: CartItem[] = [
    { name: 'Mekanik Klavye', price: 1450 },
    { name: 'Kablosuz Mouse', price: 380 }
  ];

  constructor(private cartService: CartService) { }

  addToCart(product: CartItem): void {
    this.cartService.add(product);
  }
}

Hiçbir yerde new CartService() yazmadık. Constructor'a sadece "bana CartService tipinde bir şey lazım" dedik. Angular component'i oluştururken parametrenin tipine bakar, elindeki CartService nesnesini bulur ve içeri verir. Buna constructor injection denir.

Parametrenin başındaki private bir TypeScript kısayolu. Hem parametreyi tanımlar hem de onu class'ın bir property'si yapar; böylece class'ın her yerinde this.cartService diye kullanabilirsin.

Duvardaki prize benziyor. Fişi takarsın, elektrik gelir. Santrali sen kurmazsın, kablonun nereden geldiğini de bilmen gerekmez. Yarın elektrik başka bir santralden gelse bile senin fişin aynı kalır.

Lifecycle hook'ları konuşurken "constructor'ı bağımlılıkları almak için kullan" demiştik. Kastettiğimiz tam olarak buydu.

Singleton: İki Component, Tek Sepet

Rozet component'i de aynı service'i istesin:

import { Component } from '@angular/core';
import { CartService } from '../cart.service';

@Component({
  selector: 'app-cart-badge',
  template: `<span>Sepet: {{ cartService.count }} ürün</span>`
})
export class CartBadgeComponent {
  constructor(public cartService: CartService) { }
}

Listede "Sepete Ekle" butonuna bastığında rozetteki sayı artar. İki component birbirinden habersiz, aralarında hiçbir input ya da output yok. Ama ikisi de aynı CartService nesnesini aldığı için aynı sepete bakıyorlar.

providedIn: 'root' ile sağlanan service bir singleton'dır: uygulama boyunca tek bir kopyası yaşar. İlk isteyen component geldiğinde oluşturulur, sonraki herkes aynı nesneyi alır.

Bu sefer private yerine public yazdık, çünkü template doğrudan cartService.count okuyor. Template sadece public üyelere erişebilir; private bıraksaydın Angular 12'nin strict template kontrolü derleme hatası verirdi.

Hiyerarşik Injector: Herkese Ayrı Kopya

Her zaman tek kopya istemeyiz. Ekranda üç kronometre olduğunu düşün; birini başlatınca diğerleri de saymaya başlamasın. Bu kez service'i component seviyesinde sağlıyoruz:

import { Injectable, OnDestroy } from '@angular/core';

@Injectable()
export class StopwatchService implements OnDestroy {
  seconds = 0;
  private timerId = 0;

  start(): void {
    this.stop();
    this.timerId = window.setInterval(() => this.seconds++, 1000);
  }

  stop(): void {
    window.clearInterval(this.timerId);
  }

  ngOnDestroy(): void {
    this.stop();
  }
}
@Component({
  selector: 'app-stopwatch',
  template: `
    <p>{{ stopwatch.seconds }} sn</p>
    <button (click)="stopwatch.start()">Başlat</button>
    <button (click)="stopwatch.stop()">Durdur</button>
  `,
  providers: [StopwatchService]
})
export class StopwatchComponent {
  constructor(public stopwatch: StopwatchService) { }
}

Service'teki @Injectable() bu kez boş, providedIn yok. Onun yerine component'in decorator'ına providers: [StopwatchService] ekledik. Artık her <app-stopwatch> kendi service kopyasını alır. Component yok edilince kopyası da gider ve daha önce gördüğümüz ngOnDestroy, service'in içinde de çalışıp interval'i temizler.

Bunu mümkün kılan, Angular'ın injector'larının bir ağaç olması. Bir component bir bağımlılık istediğinde Angular önce o component'in kendi injector'ına bakar. Bulamazsa parent'ına çıkar, sonra onun parent'ına, en sonunda module ve root seviyesine. Bir şirkette sarf malzemesi aramak gibi: önce kendi çekmecene bakarsın, yoksa departmanın dolabına, orada da yoksa merkez depoya.

Bir service'i @NgModule içindeki providers listesine yazarak module seviyesinde de sağlayabilirsin. providedIn: 'root' gelmeden önce bu yöntem standarttı ve eski projelerde sık görürsün. Lazy loading ile yüklenen module'lerde bu davranışın nasıl değiştiğini ilerleyen yazılarda detaylandıracağız.

Provider Tarifleri: useClass, useValue, useFactory

providers: [StopwatchService] yazmak aslında şunun kısaltmasıdır: { provide: StopwatchService, useClass: StopwatchService }. provide "kim bunu isterse" anlamına gelir, useClass da "ona bundan bir nesne ver". İkisinin aynı olması şart değil:

@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string): void {
    console.log(message);
  }
}

@Injectable()
export class SilentLoggerService extends LoggerService {
  log(message: string): void { }
}
// app.module.ts
providers: [
  { provide: LoggerService, useClass: SilentLoggerService }
]

Uygulamadaki her component hâlâ constructor'da LoggerService istiyor ama artık eline SilentLoggerService geçiyor. Hiçbir component'in koduna dokunmadan davranışı değiştirdik. Dependency injection'ın asıl gücü burada.

Hangi sınıfın verileceğine çalışma anında karar vermek istersen useFactory kullanırsın:

import { environment } from '../environments/environment';

providers: [
  {
    provide: LoggerService,
    useFactory: () => environment.production ? new SilentLoggerService() : new LoggerService()
  }
]

Factory bir fonksiyondur ve döndürdüğü nesne, bağımlılığı isteyen herkese verilir. Burada production build'de sessiz logger'ı, geliştirmede normal logger'ı seçiyoruz. Factory'nin kendisi başka bir service'e ihtiyaç duyarsa onu deps dizisiyle listelersin.

useValue ise hazır bir değer verir: { provide: LoggerService, useValue: { log: () => {} } }. En çok testlerde, gerçek service yerine sahte bir nesne koymak için işe yarar.

InjectionToken ile Config Değeri Sağlamak

Bazen sağlamak istediğin şey bir class değil, düz bir ayar nesnesidir: uygulama adı, sayfa başına ürün sayısı gibi. TypeScript interface'leri derleme sonrasında yok olduğu için Angular onları anahtar olarak kullanamaz. Çözüm InjectionToken:

import { InjectionToken } from '@angular/core';

export interface AppConfig {
  appName: string;
  pageSize: number;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// app.module.ts
providers: [
  { provide: APP_CONFIG, useValue: { appName: 'Mağaza', pageSize: 20 } }
]

// product-list.component.ts
constructor(@Inject(APP_CONFIG) private config: AppConfig) { }

Token, çalışma anında da var olan benzersiz bir anahtardır. String verdiğimiz 'app.config' sadece hata mesajlarında görünen bir açıklama. Değeri alırken parametrenin tipi yetmez, çünkü AppConfig bir interface; o yüzden @Inject(APP_CONFIG) ile hangi anahtarı istediğimizi açıkça söylüyoruz. Inject da @angular/core içinden gelir.

Bu sana tanıdık gelebilir. Daha önce gördüğümüz Türkçe locale ayarındaki { provide: LOCALE_ID, useValue: 'tr' } satırı tam olarak buydu: LOCALE_ID, Angular'ın kendi tanımladığı bir InjectionToken.

Küçük Bir Deneme

Bir FavoritesService oluştur. İçinde favori ürün adlarını tutan bir dizi, toggle(name: string) metodu ve favori sayısını veren bir getter olsun. Ürün kartlarına bir kalp butonu koy, sayfanın üst kısmına da "Favoriler: 2" yazan küçük bir component ekle. İkisi de aynı service'i constructor'dan alsın.

Çalıştığını gördükten sonra providedIn: 'root' satırını sil ve service'i her ürün kartının providers listesine ekle. Önce console'daki NullInjectorError hatasını oku. Sonra aynı satırı sayaç component'inin providers listesine de ekle ve sayacın bu kez neden hep sıfırda kaldığını injector ağacı üzerinden kendine açıkla.

Rozetteki sayı güncelleniyor, çünkü Angular her kontrol turunda template'i yeniden okuyor. Peki sepet değiştiği an bir bildirim göstermek ya da sepeti sunucuya kaydetmek istesek? Component'in "sepet değiştiğinde bana haber ver" diyebilmesi gerekir. Bir sonraki yazıda RxJS'e, observable'lara ve service'te BehaviorSubject ile state tutmaya giriyoruz.