Skip to content

Bölüm 4 / 13

Servisler ve DI

Servis kavramı ve dependency injection.

11 dakikalık okuma

Bileşen görünümün sahibidir; iş mantığı, veri erişimi ve paylaşılan durum ise servislerde yaşar. Servisi bileşene ulaştıran mekanizma, 5. hatta .NET tarafında kavramını kurduğun dependency injection'dır — Angular bu deseni çerçevenin merkezine koyar. Bu ders servis yazmayı, inject() ile almayı, sağlayıcı (provider) kavramını ve enjektör hiyerarşisini işler; 9. hatta gördüğün parametre özellikleri ve generic'ler burada sahaya iner.

01Ne öğreneceksin

  • Servisin ne olduğu; hangi kodun bileşenden servise taşınacağı
  • @Injectable({ providedIn: "root" }) — tree-shakable singleton
  • inject() ile kurucu enjeksiyonunun karşılaştırması; injection context kuralı
  • Provider türleri: useClass, useValue, useFactory, InjectionToken
  • Hiyerarşik enjektör: root / rota / bileşen düzeyi — ve "singleton'ım neden çoğaldı" tuzağı
  • DI'ın asıl meyvesi: test edilebilirlik ve değiştirilebilirlik

02Servis: bileşenden taşan her şeyin evi

2. derste kural koymuştuk: sunumsal bileşen çizer, akıllı bileşen yönetir. Ama akıllı bileşene de her şey yüklenmez — şu üç sinyalden biri varsa kod servise taşınır:

  • Birden çok bileşen aynı işi yapıyor — veri çekme, biçimleme, hesap (DRY, 4. hat).
  • Durum, bileşenden uzun yaşamalı — bileşen yıkılınca kaybolmaması gereken liste, oturum bilgisi, seçim.
  • İş mantığı görünümden bağımsız test edilmeli — servis, DOM'suz sınanan saf TypeScript sınıfıdır.
import { Injectable } from "@angular/core";

@Injectable({ providedIn: "root" })
export class BasvuruServisi {
  private basvurular: Basvuru[] = [];

  hepsi(): readonly Basvuru[] { return this.basvurular; }
  ekle(b: Basvuru)            { this.basvurular = [...this.basvurular, b]; }
  bul(id: number)             { return this.basvurular.find(x => x.id === id); }
}

@Injectable, 9. hattan tanıdığın dekoratör okumasıyla: "bu sınıfı DI kayıt defterine ekle." providedIn: "root" iki şey söyler: uygulama genelinde tek örnek (singleton — 4. hattın deseni, ama elle değil, konteyner güvencesiyle) ve tree-shakable kayıt — servis hiçbir yerde kullanılmazsa üretim paketine hiç girmez. Modern varsayılan budur; alternatif kayıt yollarını 6. bölümde göreceksin.

03Almak: inject() ve kurucu enjeksiyonu

Servisi kullanacak sınıf onu kurmaz, ister. 5. hattın merkez cümlesi burada da geçerli: new yazan sınıf, bağımlılığının somut tipine ve kuruluş ayrıntısına çivilenir; isteyen sınıf ise yalnız sözleşmeye bağlanır. Modern Angular'da istemenin yolu inject() fonksiyonudur:

import { Component, inject } from "@angular/core";

@Component({ /* … */ })
export class BasvuruListesi {
  private servis = inject(BasvuruServisi);   // alan başlatıcıda — doğru yer

  basvurular = this.servis.hepsi();
  sil(id: number) { this.servis.sil(id); }
}

9. hattın 7. dersinde tanıdığın parametre özellikleri burada devreye girer — eski ve hâlâ çok yaygın yazım, kurucu enjeksiyonudur:

export class BasvuruListesi {
  constructor(private readonly servis: BasvuruServisi) {}
  // deneysel dekoratör dünyası + emitDecoratorMetadata:
  // Angular, parametrenin TİPİNE bakarak doğru servisi verir —
  // 9. hatta "tip silinmesinin bilinçli delindiği istisna" dediğimiz mekanizma
}
inject() (modern tercih)Kurucu enjeksiyonu (sahada yaygın)
Yazım yeriAlan başlatıcı, kurucu gövdesiKurucu parametresi (parametre özelliği)
Tip bilgisiDönüş tipinden — metadata gerekmezemitDecoratorMetadata'ya dayanır
KalıtımdaAlt sınıf kurucuyu tekrarlamazAlt sınıf tüm parametreleri taşıyıp super(...) demek zorunda
Fonksiyonda kullanımOlur — guard/interceptor fonksiyonları (7. ders) buna dayanırOlmaz — sınıf ister
KısıtYalnız injection context içinde çağrılırDoğal olarak bağlam içindedir

Injection context tuzağı: inject() yalnız Angular bir sınıfı kurarken çalışır — alan başlatıcıları ve kurucu gövdesi bu bağlamın içindedir. Bir olay işleyicisinde, setTimeout'ta ya da ngOnInit'te çağırırsan çalışma anında NG0203: inject() must be called from an injection context hatası alırsın. Kural basit: bağımlılıkları sınıfın tepesinde, alan olarak al; metotlar o alanları kullansın. (İleri kaçış yolu runInInjectionContext'tir — varlığını bil, refleksin olmasın.)

04Provider: "bu istendiğinde ne verilecek?"

DI'ın sözlüğünde iki taraf vardır: token (istenen şeyin anahtarı — çoğu zaman sınıfın kendisi) ve provider (o token istendiğinde ne üretileceğinin tarifi). providedIn: "root" aslında "token = bu sınıf, tarif = bu sınıfı kur" demenin kısayoludur. Tarifi değiştirebilirsin:

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    { provide: BasvuruServisi, useClass: SahteBasvuruServisi }, // yerine geçen sınıf
    { provide: API_ADRESI,     useValue: "https://api.kurum.gov.tr" },
    { provide: Gunlukcu,       useFactory: () => uretimMi ? new UzakGunlukcu() : new KonsolGunlukcu() },
  ],
};
TarifNe yaparTipik kullanım
useClassToken istenince başka sınıf kurarSahte/alternatif uygulama; soyut sınıf → somut eşleme
useValueHazır değeri verirYapılandırma sabitleri, test değerleri
useFactoryFonksiyon çalıştırıp sonucunu verirKoşullu kuruluş; başka bağımlılıklarla üretim
useExistingVar olan başka token'ın örneğini verirTakma ad (alias)

Sınıf olmayan değerler (adres, bayrak, yapılandırma nesnesi) için token olarak sınıf kullanamazsın — arayüzler de olmaz, çünkü 9. hattan biliyorsun: arayüz çalışma anında yoktur. Çözüm InjectionToken'dır ve generic'i sayesinde (9. hat 5. ders) tip güvenlidir:

export interface ApiAyar { adres: string; zamanAsimi: number; }

export const API_AYAR = new InjectionToken<ApiAyar>("api.ayar");

// kayıt:  { provide: API_AYAR, useValue: { adres: "…", zamanAsimi: 5000 } }
// kullanım:
private ayar = inject(API_AYAR);   // tipi: ApiAyar — derleyici biliyor

Bu, 9. hattın 9. dersindeki "ortam yapılandırmasını sınırda doğrula, içeriye tiplenmiş ver" ilkesinin Angular kalıbıdır: değer bir kez, uygulama kökünde kaydedilir; her tüketici tiplenmiş biçimde ister.

05Hiyerarşi: enjektör ağacı

Angular'da tek bir DI kaydı yoktur — enjektör ağacı vardır ve bileşen ağacını izler. Bir bileşen servis istediğinde arama kendinden başlar, yukarı doğru çıkar: bileşenin kendi providers'ı → üst bileşenler → rota düzeyi → kök (root). İlk bulunan tarif kazanır.

@Component({
  selector: "app-basvuru-sihirbazi",
  providers: [SihirbazDurumu],   // BU alt ağaca ÖZEL, taze bir örnek
  /* … */
})
export class BasvuruSihirbazi {
  durum = inject(SihirbazDurumu);  // kendi örneğini alır
  // sihirbazın içindeki adım bileşenleri de AYNI örneği alır;
  // sihirbaz kapanınca (ngOnDestroy) örnek de yok edilir
}

Bu bilinçli kullanım değerlidir: çok adımlı bir formun (sihirbaz) durumu, yalnız o alt ağacın yaşadığı sürece var olmalıysa, servisi bileşen düzeyinde sağlarsın — her sihirbaz açılışı temiz durumla başlar, iki sihirbaz yan yana açılsa birbirine karışmaz.

Tuzak da aynı mekanizmadır: "singleton'ım neden çoğaldı?" Bir servis hem providedIn: "root" ile kayıtlıyken bir bileşenin providers dizisine de yazılırsa, o alt ağaç ayrı bir örnek alır: sen sepete ürün eklersin, üst bardaki sayaç boş kalır — çünkü iki ayrı nesne vardır. Belirtisi "servis durumu bazı yerlerde görünmüyor"dur; teşhisi, servisin nerelerde provide edildiğini aramaktır. Kural: paylaşılan durum köke, kapsamlı durum bilinçli olarak alt ağaca; yanlışlıkla iki kez asla.

Kayıt yeriÖrnek ömrüNe zaman
providedIn: "root"Uygulama boyu, tek örnekVarsayılan: paylaşılan servis/durum
Rota providers (7. derste)Rota alt ağacı boyuÖzellik (feature) kapsamı
Bileşen providersBileşen + alt ağacı; bileşenle ölürSihirbaz/panel gibi yerel, tekrarlanabilir durum

06Meyve: test edilebilirlik ve sınır çizgisi

DI'ın gündelik kazancı gevşek bağlılık, asıl meyvesi değiştirilebilirliktir. Bileşen BasvuruServisi'ni istemekle yetindiği için, testte gerçek servisin yerine sahtesi konur — bileşen farkı bilmez:

// 15. hatta ayrıntısıyla; fikir bugünden:
TestBed.configureTestingModule({
  providers: [{ provide: BasvuruServisi, useValue: sahteServis }],
});

Aynı kapı üretimde de işler: HTTP yerine bellekten çalışan demo sürümü, gerçek günlükçü yerine sessiz olanı — hepsi tek satırlık provider değişikliğidir; tüketici kodun hiçbir satırı değişmez. 4. hattın DIP ilkesi ("soyutlamaya bağlan") böylece çerçeve desteğiyle uygulanmış olur.

Sınır çizgisini de netleştir — mülakatta ayırt edici sorudur: Angular DI ile ASP.NET Core DI aynı desendir, farklı ayrıntılardır. 5. hatta üç yaşam süresi görmüştün (singleton / scoped / transient); Angular'da kayıt yeri bu işi görür: root ≈ singleton, bileşen/rota providers ≈ scoped (kapsam = alt ağaç ömrü). Transient'in birebir karşılığı yoktur — her istekte yeni örnek gerekiyorsa factory kalıbına başvurulur. Bir fark daha: .NET'te kapsam HTTP isteği, Angular'da bileşen alt ağacıdır — kavram aynı, eksen farklı.

07Üretim pratiği: servis tasarım kuralları

  • Durumu kapsülle: serviste diziyi private tut, dışarıya readonly görünüm ver (2. bölümdeki örnek). Dışarıdan servis.basvurular.push(...) yapılabiliyorsa 3. dersteki saf pipe / değişiklik algılama sorunlarını sen davet ediyorsun. (Signals bu kalıbı 9. derste daha da netleştirecek.)
  • Servisler servisleri kullanır: inject() servis sınıfının içinde de çalışır — BasvuruServisi, HttpClient'ı (6. ders) ve günlükçüyü enjekte eder. Katmanlar böyle kurulur: bileşen → alan servisi → altyapı servisi.
  • Generic taban gerekirse 9. hattın 5. dersi hazır: class ListeDurumu<T extends { id: number }> gibi bir taban, başvuru/kullanıcı/ilan listelerinin ortak durum mantığını tekilleştirir. Ama sırayı unutma: önce iki somut servis yaz, üçüncü tekrar geldiğinde soyutla (4. hattın kuralı).
  • Bileşende iş mantığı birikiyorsa refactoring sinyalidir: bileşen metodu üç satırı geçen hesap/karar içeriyorsa servise inmelidir. Bileşen "çevirmendir": kullanıcı olayını servis çağrısına, servis durumunu görünüme çevirir.

08Mini alıştırma — Paylaşılan durum ve kapsam deneyi

Yaklaşık 20 dakika · kurulum yok — angular.dev/playground ya da StackBlitz; önceki derslerin listesi üstüne kur

  1. BasvuruServisi'ni yaz: private dizi, hepsi()/ekle()/sil(); providedIn: "root". Liste bileşenini inject(BasvuruServisi) ile buna bağla.
  2. İkinci bir bileşen ekle: OzetCubugu — yalnız servis.hepsi().length gösterir. İki bileşeni kök şablona koy; listeden silince özetin de değiştiğini gör: tek örnek, paylaşılan durum.
  3. Çoğalma tuzağını üret: OzetCubugu'nun providers: [BasvuruServisi] dizisine servisi ekle. Silme yap — özet neden artık değişmiyor? Açıklamayı bir cümleyle yaz, sonra kaydı geri al.
  4. Injection context hatasını üret: inject() çağrısını bir buton işleyicisinin içine taşı; NG0203 hatasını konsolda gör ve alan başlatıcıya geri taşı. Hata mesajını tanı — sahada karşına çıkacak.
  5. InjectionToken dene: SAYFA_BOYU = new InjectionToken<number>("sayfa.boyu"), kökte useValue: 5 ile kaydet, listede enjekte edip ilk N kaydı göster. Değeri yalnız kayıtta değiştirerek davranışın değiştiğini doğrula.
  6. Sahte servis: aynı arayüzü taşıyan SahteBasvuruServisi yaz (sabit 3 kayıt döner); kökte { provide: BasvuruServisi, useClass: SahteBasvuruServisi } kaydıyla uygulamanın tek satır bileşen kodu değişmeden sahteye geçtiğini gör.

09Kendini sına

Dependency injection nedir; Angular'da servis almanın yolları nelerdir? inject() neden tercih ediliyor?

DI, bir sınıfın ihtiyaç duyduğu nesneleri kendisinin kurmayıp (new yok) dışarıdan — bir konteynerden — istemesi desenidir; sınıf somut kuruluş ayrıntısı yerine sözleşmeye bağlanır. Angular'da iki alma yolu vardır: kurucu enjeksiyonu (constructor(private servis: BasvuruServisi) — 9. hattın parametre özelliği; Angular parametrenin tipine bakarak doğru örneği verir, bu da emitDecoratorMetadata'ya dayanır) ve modern inject() fonksiyonu (private servis = inject(BasvuruServisi)). inject() tercih edilir çünkü tip bilgisi dönüş tipinden gelir (metadata mekanizmasına ihtiyaç kalmaz), kalıtımda alt sınıfın kurucu parametrelerini tekrarlaması gerekmez ve sınıf dışındaki fonksiyonlarda da — rota guard'ları, interceptor'lar — çalışır. Tek kuralı injection context'tir: yalnız Angular sınıfı kurarken (alan başlatıcıları, kurucu) çağrılabilir; olay işleyicisinde ya da ngOnInit'te çağrılırsa NG0203 hatası alınır. Bu yüzden kalıp, bağımlılıkları sınıfın tepesinde alan olarak almaktır.

providedIn: "root" ne anlama gelir? Aynı servisi bir bileşenin providers'ına da yazarsan ne olur?

providedIn: "root" servisi kök enjektöre, uygulama genelinde tek örnek (singleton) olarak kaydeder; kayıt tree-shakable'dır — servis hiçbir yerde enjekte edilmezse üretim paketine girmez. Angular'ın enjektörü hiyerarşiktir: bileşen servis istediğinde arama bileşenin kendi providers'ından başlar, bileşen ağacında yukarı çıkar ve kökte biter; ilk bulunan tarif kazanır. Aynı servisi bir bileşenin providers dizisine de yazarsan o bileşen ve altındaki ağaç için ayrı bir örnek kurulur: alt ağaçtaki değişiklikler kökteki örneğe yansımaz — "servisim singleton ama durumu bazı ekranlarda görünmüyor" şikâyetinin klasik sebebi. Bu davranış bilinçli kullanıldığında değerlidir: çok adımlı bir sihirbazın durumu bileşen düzeyinde sağlanırsa her açılış temiz başlar, örnek bileşenle birlikte yok edilir ve yan yana iki sihirbaz karışmaz. Kural: paylaşılan durum köke; kapsamlı durum bilinçli olarak alt ağaca; yanlışlıkla çift kayıt asla.

InjectionToken neden vardır? Bir arayüzü doğrudan token olarak neden kullanamazsın?

DI konteyneri "ne istendiğini" bir anahtarla (token) tanır ve bu anahtar çalışma anında var olan bir değer olmalıdır — sınıflar uygundur çünkü sınıf hem tip hem değerdir (9. hat 7. ders). TypeScript arayüzü ise yalnız derleme anında yaşar, tip silinmesiyle kaybolur; çalışma anında ortada anahtar olarak kullanılacak bir nesne yoktur — bu yüzden provide: ApiAyar şeklinde bir arayüz kaydı mümkün değildir. InjectionToken bu boşluğu doldurur: çalışma anında yaşayan, benzersiz bir anahtar nesnesidir ve generic parametresi (new InjectionToken<ApiAyar>("api.ayar")) sayesinde enjekte edilen değerin tipi derleyicice bilinir. Tipik kullanım, sınıf olmayan değerlerdir: API adresi, sayfa boyutu, yapılandırma nesnesi, ortama göre değişen bayraklar. Kalıp, 9. hattın "yapılandırmayı sınırda bir kez doğrula, içeriye tiplenmiş dağıt" ilkesinin Angular karşılığıdır: değer uygulama kökünde bir kez useValue/useFactory ile kaydedilir, tüketiciler inject(API_AYAR) ile tip güvenli biçimde alır.

DI test edilebilirliği nasıl sağlar? Angular DI ile ASP.NET Core DI'ı karşılaştır.

Bileşen, bağımlılığını token ile istediği için testte o token'a başka bir tarif verilir: { provide: BasvuruServisi, useValue: sahteServis } — bileşen kodu tek satır değişmeden sahte veriyle çalışır; HTTP yok, gerçek servis yok, test hızlı ve yalıtılmış. Aynı kapı üretim esnekliğidir: demo sürümü, alternatif günlükçü, ortama göre factory — hepsi kayıt değişikliğidir. Bu, 4. hattın DIP ilkesinin ("soyutlamaya bağlan, somuta değil") çerçevece desteklenmiş hâlidir. .NET karşılaştırması: desen aynıdır — konteyner, token/tarif eşlemesi, kurucudan isteme; farklar eksendedir. ASP.NET Core'da üç yaşam süresi açıkça adlandırılır (singleton/scoped/transient) ve scoped'ın kapsamı HTTP isteğidir; Angular'da yaşam süresini kayıt yeri belirler — root ≈ singleton, rota/bileşen providers ≈ scoped, kapsam ise istek değil bileşen alt ağacının ömrüdür; transient'in birebir karşılığı yoktur, gerekirse factory kullanılır. Bir fark daha: .NET'te kayıt Program.cs'te merkezîdir, Angular'da providedIn: "root" kaydı servisin kendi üstündedir ve tree-shaking'e yardım eder.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Dependency injection nedir; ne kazandırır? (2) Angular'da servis nasıl tanımlanır ve enjekte edilir? (3) @Injectable dekoratörü ve providedIn: "root" ne anlama gelir? (4) inject() ile kurucu enjeksiyonunun farkları nelerdir? (5) Provider türlerini (useClass, useValue, useFactory) birer örnekle açıklayınız. (6) InjectionToken ne zaman gerekir? (7) Hiyerarşik enjektör nedir; bileşen providers'ının etkisi nedir? (8) Singleton servis nasıl garanti edilir; hangi durumda bozulur? (9) DI test yazmayı nasıl kolaylaştırır? (10) Angular DI ile ASP.NET Core DI'ın yaşam süresi modellerini karşılaştırınız.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Tanım — "İş mantığı ve paylaşılan durum serviste yaşar; bileşen servisi kurmaz, DI'dan ister — 5. hattaki .NET DI'ın aynı deseni." (2) Modern yazım — "inject() kullanırım: tip dönüşten gelir, kalıtım ve fonksiyonlarda çalışır; injection context kuralını bilirim — bağımlılık alan olarak tepede alınır. Kurucu enjeksiyonunu da okurum." (3) Kayıt — "Varsayılan providedIn: root — tree-shakable singleton; sınıf dışı değerlere InjectionToken; useClass/useValue/useFactory tarifleri." (4) Hiyerarşi — "Enjektör ağacı bileşen ağacını izler; bileşen providers bilinçli kapsam açar (sihirbaz durumu), dikkatsiz kullanılırsa 'singleton çoğaldı' tuzağıdır." (5) Kıdem vuruşu — "DI'ın meyvesi değiştirilebilirlik: testte sahte provider, üretimde ortam factory'si — tüketici kod değişmez; DIP'in çerçevedeki hâli."