Inversion of Control (IoC), Dependency Injection və Dependency Inversion aralarındakı fərq …

Zamiq Əliyev
6 min readOct 8, 2021

--

Bu 3 anlayış arasındakı fərqi başa düşmək üçün əvvəlcə bunların nə olduğunu anlamaq lazımdır. Bu üzdən hər 3 anlayış barədə ayrılıqda yazıb yekunda bunların fərqini qeyd edəcəm. Məqalə biraz uzun olacaq, amma düşünürəm ki, oxuduğunuza dəyəcək.

1. Inversion of control nədir?
Inversion of control bir yazılış tərzidir. Ioc — yazdığımız proyektin daxilində istifadə etdiyimiz obyekt instance’larının idarəsini öz öhdəsinə götürərək,
obyektlər arasındakı asılılıqları ən aza endirmək məqsədi daşıyır. Proyektdəki asılılıqların idarəsini proqramçının əvəzinə framework’ün etməsi olaraq da qeyd oluna bilər.
Framework‘lə işlədiyimiz zaman görürük ki, frameworklər bizim kodumuzun işləməsi üçün lazım olan lib’ləri yaradaraq bir çox işi bizim əvəzimizə özü edir. IOC’a tərif verməli olsaq:
Framework tərəfindən bizim yazdığımız kod parçasının çağrılması, işə düşməsi və daha sonra kontrolun yenidən framework’ə keçməsi hadisəsinin tamamına Inversion Of Control deyilir.
Bunu daha detallı yazım:
Bizim yazdığımız kod hissəsi xaric hər şey framework tərəfindən idarə edilir. Misal üçün, Spring ilə bir not dəftəri proqramı yazmışıq. Spring bir framework olduğu üçün bizim proyektin işləməsi üçün lazım olan qaynaqları özü ayarlayıb idarə edəcək. Yəni proyekti Spring başladacaq və hazır olduqda sizin kod parçasını işlədəcək. Not dəftərinə hər hansısa bir sətir əlavə etdikdə (yəni, məlumat bazasına insert) sizin kod parçası işləyəcək və işləm bitdikdən sonra kontrol yenidən Spring Framework’nə keçəcək. Bütün bu mexanizmə Inversion of Control deyilir.

Inversion of Control’un gətirdiyi avantajlar:

1.1. Bir metodun implimentasiyasından izolə edilmiş bir şəkildə işləməsini təmin edir.
1.2. Fərqli implimentasiyalar arasında rahat keçid imkanı yaradır.
1.3. Asılılıqları minimuma endirdiyi üçün kodu test etməyi/yazmağı asanlaşdırır.

Inversion of control;
— Strategy Pattern
— Service Lacator Pattern
— Factory Pattern
— Dependency Injection
kimi mexanizmlərlə tətbiq edilir.

2. Dependency Injection nədir?
Dependency Injection bir classın digərindən asılı olmasının qarşısını alan mexanizmdir. Dependency Injection tətbiq edərək bir classın digərindən asılı olmasını aradan qaldıra bilərik.
Yuxarıda qeyd etdiyim kimi frameworklər sizə sadəcə business logic ilə məşğul olmaq, digər işlərə vaxt sərf etməmək rahatlığını verir. Bunu da frameworkdən asılı olmadan kod yaza bilməmiz üçün adətən dependency injection tətbiq edərək həyata keçirir.

Dependency Injection tətbiq etməyin avantajları nələrdir?
2.1. Bir-birindən asılı classları daxildə birbaşa istifadə etmək yerinə bu classları çöldən ötürərək aradakı asılılığı minimuma endirir.
2.2. Unit testlərin yazılmasını rahatlaşdırmaqdan əlavə də componentlerin “loosely coupled” olmasını barındırır. Beləcə test edilməsi lazım gələn classlar ayrı-ayrılıqda test edilə bilir.

Bunu misal üzərindən izah edək:

public class Car {
DieselEngine engine;
public void drive() {
String engineStart = engine.start();
}
}
public class DieselEngine {
public String start() {
return “DieselEngine started “;
}
}

Göründüyü kimi Car’ın DieselEngine’ə birbaşa asılılığı var. Car, DieselEngin classındakı bir metodu çağırır. Bu hal asılılığa səbəb olur. Kod böyüdükcə problemlər artıq və asılılıqları aradan qaldıqmaq da başqa bir problem yaradır.

Göstərilən kodda nə kimi problemlər ola bilər:

- Kod əsnək deyil. DieselEngine classı üzərində edilən hər hansısa bir dəyişiklik Car classına da təsir edəcək.
- Unit testi çətinləşdirir. Yalnız bir classı test etmək istədikdə məcbur digər classları da test etməli olacağıq.
- Classlar bir-birinə bağlı olduğu üçün bunları ayrı bir class daxilində istifadə edə bilməyəcəyik.

Bu üzdən Car classını bir interface olaraq, DieselEngine classını isə daha geniş bir şəkildə istifadə edə bilməmiz üçün onun yerinə Engine interface’i kimi qeyd edirik.


public interface Car {
void drive();
}

public interface Engine {
String start();
}

Beləliklə DieselEngin classını Engine’nin bir implimentasiyası olaraq istifadə edə bilərik. Engine interface’ini yaratmaq başqa Engine implimentasiyalarını istifadə edə bilmək imkanını yaradacaq.

public class DieselEngine implements Engine { 
@Override
public String start() {
return “Diesel Engine started.”;
}
}

Ardınca, ElectricEngine və GasolineEngine kimi fərqli Engine implimentasiyalarına da sahib ola bilərik:

public class ElectricEngine implements Engine {

@Override
public String start() {
return “Electric Engine started.”;
}
}
public class GasolineEngine implements Engine {

@Override
public String start() {
return “Gasoline Engine started.”;
}
}

Və Car interface’ini istifadə edən bir AutoCar implimentasiyası yaza bilərik. Bu class DieselEngine əvəzinə Engine classını istifadə edəcək.
Engine classı constructor injection vasitəsilə AutoCar classına inject ediləcək.

public class AutoCar implements Car {

Engine engine;

public AutoCar(Engine engine) {
this.engine = engine;
}

@Override
public void drive() {

String engineStart = engine.start();

}
}

AutoCar classı artıq hər hansısa bir Engine implimentasiyasına bağlı deyil. Birbaşa AutoCar’da asılı class yaratmaq əvəzinə dependency injection istifadə etdiyimiz üçün container və ya framework artıq bu classı yaratmaq və constructor vasitəsilə onu AutoCar classına inject etmək məsuliyyətini daşıyır.

Misal üçün:

Engine engine = new ElectricEngine();
Car car = new AutoCar(engine);
car.drive();

Bu misalda ElectricEngine obyekti Engine interfeysinin bir implimentasiyası olaraq yaradılır və AutoCar obyekti Engine obyektininin hansı implimentasiyası (ElectricEngine,DieselEngine kimi) olmasından asılı olmadan onu istifadə edə bilir. AutoCar classına aşağıdakı kimi setter üsulu tətbiq edə bilərik.

public void setEngine(Engine engine) {
this.engine = engine;
}

setter injection tətbiq edildikdə DieselEngine istifadə misalında olduğu kimi fərqli Engine implimentasiyalarını da istifadə edə biləcək vəziyyətə gəlir.

((AutoCar) car).setEngine(new DieselEngine());
car.drive();

Asılılı aradan qaldırmaq üçün biz bunu özümüz əl ilə etdik, amma tanınmış frameworklər bunu dependency injection tətbiq etməklə özü həll edir. Springi buna misal kimi göstərmək olar.Dependency Injection tətbiq etmək SOLID prinsipləri kimi hər zaman istifadə edilməsi əsas götürülən mexanizmlərdən biridir.

3. Dependency Inversion nədir?
Bir classın və ya metodun onu istifadə edən digər classlara qarşı olan asılılığını minimuma endirməkdir. Bir alt classda edilən hər hansısa dəyişiklik üst classa təsir etməməlidir. Üst səviyyəli classlarda dəyişiklik ediləndə alt səviyyə classlar bu dəyişikliyə uymalıdır, ancaq alt səviyyəli classlarda edilən dəyişiklik üst səviyyəli classlara təsir etməməlidir.
Yaxşı, bu vəziyyətdən çıxış yolu nədir?
-Cavab: Dependency Inversion, yəni üst səviyyəli classlar alt səviyyəli classlara bağlı olmamalı, həll yolu isə isə hər ikisi də interface üzərindən idarə olunmalıdır.

High Level Classes -> Abstraction Layer -> Low Level Classes

Misal üzərindən daha aydın izah edək. Hesab edək ki, bir Notification classımız var və bu class vasitəsilə Email və Sms göndərməliyik.

Email göndərən classımız:

public class Email {
public void sendEmail() {
//Send emeail
}
}

Sms göndərən classımız:

public class SMS {
public void sendSMS() {
//Send sms
}
}

Və bildiriş göndərdiyimiz zaman hər 2 classı işə salan Notification classımız:

public class Notification {
private Email email = new Email();
private SMS sms = new SMS();
public void sender() {
email.sendEmail();
sms.sendSMS();
}
}

Burada diqqətli olmamızı gərəkdirən bir nüans var. Notification classı üst səviyyə class olmasına rəğmən o, alt səviyyəli Email və SMS classlarından asılı haldadır. Email və ya SMS classlarında edilən hər hansısa dəyişiklik avtomatik olaraq Notification classına da təsir edəcəkdir. Tutaq ki, Email və SMS göndərdikdən sonra əlavə olaraq da SMS balans yoxlanmalıdır, bundan ötrü biz Notification classına dəyişiklik etmək məcburiyyətində qalacağıq. Bu zaman biz Dependency Inversion prinsipini pozmuş olacağıq.

Bütün SOLID prinsiplərini nəzərə alsaq buna necə həll yolu tapa bilərik?
Notification classının Email və SMS classından asılılığını aradan qaldırmaq üçün araya interface əlavə etməliyik.

public interface Message {
void sendMessage();
}
public class Email implements Message {
@Override
public void sendMessage() {
sendEmail();
}
private void sendEmail() {
//Send email
}
}
public class SMS implements Message {
@Override
public void sendMessage() {
sendSMS();
}
private void sendSMS() {
//Send sms
}
}

Və son olaraq:

public class Notification {
private List<Message> messages;
public Notification(List<Message> messages) {
this.messages = messages;
}
public void sender() {
for (Message message : messages) {
message.sendMessage();
}
}
}

Artıq Email və SMS classlarını birbaşa deyil Message interface’i vasitəsilə istifadə etdik. Beləliklə artıq biz üst səviyyə classın alt səviyyə classlara olan asılılığını aradan qaldırdıq. Bundan sonra Notification classına dəyişiklik etmədən Message interface’ni implement edən yeni bir classı da (yuxarıda qeyd etdiyim CheckSmsBalance misalı) istifadə edə bilərik.

Dependency Inversion prinsipi SOLID prinsipində yer alan ən sonuncu prinsipdir. Bütün prinsiplər bir biri ilə əlaqəlidir, dolayısıyla kod yazarkən bütün prinsiplərə riayyət
edilməlidir.

Yekun olaraq, yuxarıda yazılanları nəzərə alsaq Dependency Injection və Dependency Inversion arasındakı bəsit oxşar və fərqli cəhətlər aşağıdakılardır:

Hər ikisində oxşar cəhət, classlar arasındakı asılılığı aradan qaldırmaq mexanizmini yerinə yetirməkdir.
Fərqli cəhətləri isə:
I) Dependency Inversionda məqsəd alt classın daxilindəki metodları dəyişən zaman və ya yeni alt metod əlavə edərkən bu dəyişikliyin ana classa yansımamasıdır.
II) Dependency Injectionda məqsəd A classının, daxilində istifadə olunan digər B classından asılı olmamasıdır. Yəni lazım olarsa A’nın içində B classını, lazım gəldikdə C classını,lazım gəldikdə isə D classını ötürərək, bu 2 class arasındakı asılılığı aradan qaldırırıq.

--

--