Angular Dependency Injection

Nahit Ferhat Ektaş
7 min readDec 23, 2023

--

Dependency Injection Angular’dan bağımsız olarak yazılım geliştirme süreçlerinde kullanılan bir tasarım desenidir. Bu tasarım deseni, SOLID yazılım prensiplerinin sonuncusu olan Dependency Inversion (bağımlılıkların tersine çevrilmesi) prensibine dayanır.

Dependency Inversion(Bağımlılıkların tersine çevrilmesi) prensibine göre; üst seviyeli sınıflar alt seviyeli sınıflara bağımlı olmamalıdır. Bağımlılıklar abstract(soyut) sınıflar üzerinden olmalıdır.

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: ``,
})
export class AppComponent implements OnInit {
userService!: UserService;

constructor() {
this.userService = new UserService();
}
ngOnInit(): void {
const userList = this.userService.getUser();
console.log(userList);
}
}

Yukarıdaki örnekte, AppComponent sınıfında UserService sınıfından bir nesne örneği oluşturduk ve getUser() fonksiyonunu çağırdık. Yani aslında AppComponent sınıfı UserService sınıfına bağlı(Tightly Coupled) olmuş oldu. UserService sınıfında yapacağımız her değişiklik artık AppComponent sınıfınıda etkileyecektir. Bu durum, Dependency Inversion prensibine aykırıdır.

Dependency Inversion prensibine göre, sınıflar arasındaki bağımlılık gevşek(Loosely Coupled) olmalıdır.

Yukarıdaki örnekte olduğu gibi UserService sınıfından bir instance oluşturmak yerine, UserService sınıfının abstract(soyut) halini kullanarak işlemlerimizi yapmamız gerekiyor. İşte bu noktada Inversion of Control(IOC) yapısı devreye giriyor. IOC Container’ lar Dependency Injection tasarım desenini kullanan framework’lere göre değişkenlik gösterse de, genel anlamda abstract(soyut) sınıfları çözümlemek için kullanılır.

Biz bu yazıda Angular da yer alan IOC Container’dan bahsedeceğiz.

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
providers: [{ provide: 'userServiceDependency', useClass: UserService }],
template: ``,
})
export class AppComponent implements OnInit {
constructor(
@Inject('userServiceDependency') private userService: UserService
) {}
ngOnInit(): void {
const userList = this.userService.getUser();
console.log(userList);
}
}

Yukarıdaki örnekte, bir önceki örnekte olduğu gibi bir instance yaratmak yerine constructor da kullanmak istediğimiz sınıfı parametre olarak geçtik.

Tabi bunu yapmadan önce IOC Container yani providers’ a provide ve useClass olmak üzere iki tane parametre tanımladık.

Provide ile tanımladığımız alan bizim Injection Token alanımız, useClass ile tanımladığımız alan ise, injection token’a karşılık gelen sınıfımızdır.

Injection Token: Inject edilecek sınıfların benzersiz bir şekilde tanımlanmasını sağlayan değerlerdir.

Providers alanında tanımladığımız provider ve useClass değerleri ile, Angular IOC Container’a şunu demiş oluyoruz; eğer sana provider da tanımladığım benzersiz değer ile gelirsem bana useClass içerisinde tanımladığım sınıftan bir nesne örneği geri döndür. IOC Container’a kullanacağımız dependency’nin bilgisini verdik, henüz bir injection işlemi gerçekleştirmedik.

Angular’ da injection işlemi 2 şekilde yapılabiliyor.

1- Constructor’ a providers da tanımladığımız Injection Token’ı parametre olarak geçerek.

2- Angular 14 ile birlikte gelen inject() fonksiyonuna Injection Token’ı göndererek.

Biz bu örnekte constructor ile injection işlemlerimizi gerçekleştireceğiz.

constructor(
@Inject('userServiceDependency') private userService: UserService
) {}

Yukarıdaki kod satırında @Inject(‘userServiceDependency’) Injection Token ile IOC Container’dan UserService için bir nesne örneği talep ettik.

Angular da bu işlem Injector adı verilen abstract bir sınıf ile ele alınır. Injector sınıfı, talep edilen Injection Token’a karşılık yaratılmış bir nesne örneği yoksa, yeni bir nesne örneği yaratıp kayıt eder ve geri döner. Eğer daha önceden kayıtlı bir nesne örneği var ise, mevcut olanı geri döndürür. Bunun nedenide Angular Dependency Injection mekanizmasının Singleton olarak çalışmasıdır.

Eğer Injection Token ile oluşturmak istediğimiz sınıfın isimleri aynı ise aşağıdaki örnekte olduğu gibi, daha kolay bir şekilde de kullanabiliriz.

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],

//providers: [{ provide: UserService, useClass: UserService }] yerine
providers: [UserService],

template: ``,
})
export class AppComponent implements OnInit {

//constructor(
//@Inject('userServiceDependency') private userService: UserService
//) {} yerine
constructor(private userService: UserService) {}

ngOnInit(): void {
const userList = this.userService.getUser();
console.log(userList);
}
}

Injectable Service Yapısı

Angular uygulamamızda herhangi bir sınıfı component veya directive’lere inject edebiliyoruz. Buna ek olarak sınıflarımızı Injectable dekoratörü ile işaretleyebiliyoruz.

Injectable ile işaretlediğimiz sınıflar ile injection işlemlerimizi daha esnek ve daha pratik olarak gerçekleştirebiliyoruz.

Öncelikle, Injection ile işaretlemediğimiz bir sınıf örneği üzerinden ilerleyelim.

export class UserService {
constructor() {}

getUser() {
return ['user-1', 'user-2'];
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
providers: [UserService],
template: ``,
})
export class AppComponent implements OnInit {
constructor(private userService: UserService) {}
ngOnInit(): void {
const userList = this.userService.getUser();
console.log(userList);
}
}

Yukarıdaki örnekte, UserService sınıfımı AppComponent sınıfına inject ederek sorunsuz bir şekilde işleme devam edebiliriz.

Peki UserService içerisinde de bir dependency’ e ihtiyaç duysaydık ne olacaktı? Örneğin; oluşturduğumuz sınıf içerisinde HttpClient nesnesini inject ederek uzak API’lere istek atmak istediğimizi düşünelim. HttpClient sınıfını bir şekilde Dependency Injection mekanizması içerisine dahil etmemiz gerekiyor.

Angular da oluşturduğumuz sınıflarda bir injection işlemi gerçekleştirmek istiyorsak, ilgili sınıfı @Injectable olarak işaretlememiz gerekiyor.

@Injectable()
export class UserService {
constructor(private httpClient: HttpClient) {}

getUser() {
return ['user-1', 'user-2'];
}
}

Yukarıdaki örnekte, UserService sınıfını Injectable ile işaretledikten sonra constructor da HttpClient parametresi ile yeni bir instance yaratabildik. Injectable ile işaretlenen sınıflar, ihtiyaç duyulan component veya directive’lerde kendilerine tanımlanan dependency’ler ile birlikte işleme alınırlar.

Injectable ile işaretlenen sınıfların bir diğer faydası ise,

Injectable dekoratörünün içerisine providedIn:’root’ parametresini geçersek, kullanacağımız component veya module içerisinde providers’a eklememize gerek kalmaz.

@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private httpClient: HttpClient) {}

getUser() {
return ['user-1', 'user-2'];
}
}

Yukarıdaki UserService sınıfımızı kullanmak istediğimizde tek yapmamız gereken, inject edeceğimiz sınıfın constructor’ına parametre olarak geçmek olacaktır.

Angular providedIn: ‘root’ kullanıldığında, providers tanımı derleme zamanında root level da yapılır ve inject edildiği yerlerde tek bir nesne örneği ile çalışır.

Dependency Providers Türleri

Bu bölüme kadar dependency olarak useClass’ı kullandık.

providers: [{ provide: 'userServiceDependency', useClass: UserService }],

useClass ile oluşturduğumuz sınıfları injection token ile birlikte inject ederek nesne örneği talebinde bulunabiliyorduk.

Angular da useClass ile birlikte toplam 4 tane provider etme türü vardır.

  • useClass
  • useExisting
  • useValue
  • useFactory

Class providers: useClass

Yukarıdaki örneklerde de sıkça kullandığımız provide türüdür. Eğer bir sınıf provide edilecekse useClass’dan yararlanılır.

Alias providers: useExisting

Provide edilen bir sınıfa farklı bir injection token ile ulaşmamızı sağlar.

providers: [
{ provide: UserService, useClass: UserService },
{ provide: NewUserService, useExisting: UserService },
],

Yukarıdaki örnekte, önce UserService injection token ile UserService sınıfı nesnesi oluşturuldu. Daha sonrasında NewUserService injection token ile mevcut UserService sınıfının nesne örneğine erişim sağlamış olduk.

Value providers: useValue

Value Providers ile string, boolean, number gibi değerlerin provide edebiliriz.

const USER_CONFIG = {
isAdmin: true,
profileImage: './assets/images/profile/',
});

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
providers: [{provide:'USER_CONFIG', useValue: USER_CONFIG }],
template: ``,
})
export class AppComponent implements OnInit {
constructor(@Inject('USER_CONFIG') private userConfig:any) {}
ngOnInit(): void {
console.log(this.userConfig);
}
}

Yukarıdaki örnekte useValue ile oluşturulan USER_CONFIG değeri, ilgili component’e inject edildiğinde USER_CONFIG de yer alan bilgileri getirecektir(isAdmin ve profileImage)

Factory providers: useFactory

useFactory providers ile dinamik değerlere göre provide işlemi gerçekleştirebiliriz.

Örnek üzerinden ilerleyecek olursak,

@Injectable()
export class UserService {
getUser() {
return ['user-1', 'user-2'];
}
}

@Injectable()
export class AdminService {
getUser() {
return ['admin-user-1', 'admin-user-2'];
}
}

Öncelikle UserService ve AdminService adında, ayrı değerler return eden iki farklı sınıf oluşturduk.

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, HttpClientModule],
providers: [
{ provide: 'ADMIN_USER', useValue: false },
{
provide: 'users',
useFactory: (ADMIN_USER: boolean) =>
ADMIN_USER ? new AdminService() : new UserService(),
deps: ['ADMIN_USER'],
},
],
template: ``,
})
export class AppComponent implements OnInit {
constructor(@Inject('users') private user: UserService) {}
ngOnInit(): void {
const userList = this.user.getUser();
console.log(userList);
}
}

Oluşturduğumuz sınıflar(UserService,AdminService), useFactory ile belirlediğimiz koşula göre component’imize inject edilecektir.

{ provide: 'ADMIN_USER', useValue: false },

ValueProviders’ın koşuluna göre, hangi sınıftan nesne örneği üretileceğine karar veriyoruz.

useValue:true ise,

Injection token olarak oluşturduğumuz ‘users’ ile AdminService den dönen değerler console da gösterilecek.

useValue:false ise,

UserService de yer alan değerler console da gösterilecek.

Böylelikle Factory providers ile dinamik olarak nesne örneği yaratmış olduk.

Dependency Injection Çözümleme Sırası ve Kuralları Nelerdir?

Angular component’imizi ilk çalıştırdığında, constructor’da yer alan injector’lar providers da tanımlı mı kontrol eder ve eğer tanımlı ise kullanır. Eğer tanımlı değilse, component’in veya module’nin parent elementlerini kontrol eder. Eğer burada da tanımlı bir providers bulamazsa root level da tanımlanıp tanımlanmadığına bakar. Eğer hala tanımlı bir providers bulamadıysa hata döndürür.

Angular da çözümleme kuralları @Optional(), @Self(), @SkipSelf() ve @host() dekoratörleri ile değiştirilebilir.

@Optional()

Optional dekoratörü ile ilgili servisin inject işlemi opsiyonel olarak tanımlanır. Angular bu dekoratörü gördüğünde, providers’ı çözümleyemese bile hata döndürmek yerine null değerini döndürücektir.

constructor(@Optional() private userService: UserService) {}

@Self()

Self dekoratörü ile Angular ilgili providers tanımını yalnızca injection işleminin yapıldığı component de arayacaktır.

export class AppComponent implements OnInit {
constructor(@Self() private userService: UserService) {}
ngOnInit(): void {
const userList = this.userService.getUser();
console.log(userList);
}
}

UserService sınıfının providers’ı yalnızca AppComponent içerisinde aranacaktır. Eğer bulunamazsa hata dönecektir. Hata almamak için @Optional() ile kullanılabilir.

constructor(@Self() @Optional() private userService: UserService) {}

@SkipSelf()

SkipSelf dekoratörü ile Angular ilgili providers tanımını inject edilen component de değil bir üst component de arayacaktır.

export class AppComponent implements OnInit {
constructor(@SkipSelf() private userService: UserService) {}
ngOnInit(): void {
const userList = this.userService.getUser();
console.log(userList);
}
}

UserService providers, AppComponent de değil, bağlı bulunduğu bir üst component de aranmaya başlayacaktır.

@Host()

Host dekoratörü ile providers arama işleminin hangi seviyede biteceğini tanımlayabiliyoruz. Üst component’ler de providers tanımlı olsa bile, @host dekoratörünü tanımladığımız seviye bu component’lerin altında ise Angular geçerli providers aramayı durdurur.

@Component({
selector: 'app-user',
standalone: true,
imports: [CommonModule],
template: ``,
})
export class UserComponent implements OnInit {
constructor(private userService: UserService) {}
ngOnInit(): void {
console.log(this.userService.getUser());
}

Yukarıdaki örnekte UserComponent oluşturduk, bu component aşağıda oluşturduğumuz örneğin child component’i olacak. UserService sınıfının providers işlemini aşağıdaki component de yapacağız.

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, UserComponent],
providers: [UserService],
template: `<app-user></app-user>`,
})
export class AppComponent {
constructor(@Host() private userService: UserService) {}
}

UserComponent de inject edilen UserService sınıfının çözümlenmesi öncelikle kendi içinde yapılmaya çalışılacaktır. UserComponent’in içinde bir providers tanımlamadığımız için, parent component’i olan AppComponent’in providers’ına bakarak çözümleme yapmaya çalışacaktır. Eğer AppComponent de yaptığımız gibi @Host() dekoratörünü tanımlarsak çözümleme yapılmaya çalışacak son component’in AppComponent olduğunu söylemiş oluruz, aksi durumda normal çözümleme sırası devam edecektir.

Angular uygulamalarımızda Dependency Injection yapısını yoğun bir şekilde kullanıyoruz. Genel kullanım contructor da injection işlemi yaparak devam etsede, Angular Dependency Injection yapısı ile kullanıcılara ciddi oranda esneklik sağlıyor.

--

--