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 singletoninject()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 yeri | Alan başlatıcı, kurucu gövdesi | Kurucu parametresi (parametre özelliği) |
| Tip bilgisi | Dönüş tipinden — metadata gerekmez | emitDecoratorMetadata'ya dayanır |
| Kalıtımda | Alt sınıf kurucuyu tekrarlamaz | Alt sınıf tüm parametreleri taşıyıp super(...) demek zorunda |
| Fonksiyonda kullanım | Olur — guard/interceptor fonksiyonları (7. ders) buna dayanır | Olmaz — sınıf ister |
| Kısıt | Yalnız injection context içinde çağrılır | Doğ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() },
],
};
| Tarif | Ne yapar | Tipik kullanım |
|---|---|---|
useClass | Token istenince başka sınıf kurar | Sahte/alternatif uygulama; soyut sınıf → somut eşleme |
useValue | Hazır değeri verir | Yapılandırma sabitleri, test değerleri |
useFactory | Fonksiyon çalıştırıp sonucunu verir | Koşullu kuruluş; başka bağımlılıklarla üretim |
useExisting | Var olan başka token'ın örneğini verir | Takma 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 örnek | Varsayılan: paylaşılan servis/durum |
Rota providers (7. derste) | Rota alt ağacı boyu | Özellik (feature) kapsamı |
Bileşen providers | Bileşen + alt ağacı; bileşenle ölür | Sihirbaz/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
privatetut, dışarıyareadonlygörünüm ver (2. bölümdeki örnek). Dışarıdanservis.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
BasvuruServisi'ni yaz:privatedizi,hepsi()/ekle()/sil();providedIn: "root". Liste bileşeniniinject(BasvuruServisi)ile buna bağla.- İkinci bir bileşen ekle:
OzetCubugu— yalnızservis.hepsi().lengthgösterir. İki bileşeni kök şablona koy; listeden silince özetin de değiştiğini gör: tek örnek, paylaşılan durum. - Çoğalma tuzağını üret:
OzetCubugu'nunproviders: [BasvuruServisi]dizisine servisi ekle. Silme yap — özet neden artık değişmiyor? Açıklamayı bir cümleyle yaz, sonra kaydı geri al. - Injection context hatasını üret:
inject()çağrısını bir buton işleyicisinin içine taşı;NG0203hatasını konsolda gör ve alan başlatıcıya geri taşı. Hata mesajını tanı — sahada karşına çıkacak. InjectionTokendene:SAYFA_BOYU = new InjectionToken<number>("sayfa.boyu"), kökteuseValue: 5ile 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.- Sahte servis: aynı arayüzü taşıyan
SahteBasvuruServisiyaz (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."
