- Контекст
- Шта је зависност (у развоју софтвера)
- Интерфејси и апстрактне класе спашавају дан
- Нека неко други одговара за тебе
- Никад ништа није једноставно
- Може то још мало компликованије
- Претпоставимо неке ствари
- Јесмо ли завршили?
- Нисмо још завршили
- Коначно: убризгавање зависности
- Закључак
- Наставак сутра
Урош је питао да укратко испричам зашто се користи Дипенденси инџекшн. Није био на предавању када је Младен увео ову тему те је тако пропустио тај критичан уводни део.
Контекст
Дипенденси инџекшн смо увели у оквиру 4. Теме (Сервисни слој) Модула 4 (Развој веб сервиса и серверске стране апликација). Дипенденси инџекшн механизам је предуслов за увођење сервиса и напредних техника у архитектури наших апликација.
Сви материјали везани за ову тему доступни су на форуму, почевши од “Materijali za Temu 4 se nalaze na sledecem linku.” па до “Projektni zadaci nakon Teme 5 su dati na sledecem linku.”.
Шта је зависност (у развоју софтвера)
Увек када једна класа користи услуге друге класе у имплементацији својих функција, постоји зависност. Класа А, корисник односно клијент, зависи од услуга класе Б, сервисне односно услужне класе.
class A {
private B _b;
public void Foo() {
_b.Bar();
}
}
Наведени пример ће се компајлирати.
Узмимо да имамо Main
методу и унутар ње следеће наредбе:
public static void Main() {
A object_a = new A();
object_a.Foo();
}
Чим програм крене да изврши ред object_a.Foo()
који, јелте, заправо значи извшравање реда _b.Bar();
из дефиниције класе А, програм ће избацити грешку NullReferenceException
, пошто покушава да позове методу над непостојећим објектом.
Први начин да се та грешка исправи јесте да ми унутар наше класе сами инстанцирамо објекат типа класе Б. Ми то можемо урадити било код дефиниције поља private B _b;
или додати конструктор у којем се врши инстанцирање - прављење - одговарајућег објекта.
Пример за први случај:
class A {
private B _b = new B();
public void Foo() {
_b.Bar();
}
}
Пример за други случај:
class A {
private B _b;
public A() {
this._b = new B();
}
public void Foo() {
_b.Bar();
}
}
Хмм, шта може поћи по злу?
Иако ће наш програм овако радити, проблем се прво може јавити када се класа Б из неког разлога промени. Може се десити да Bar()
више не ради оно што ми очекујемо него нешто друго, може се даље десити да се потпис методе промени (да захтева аргументе или аргументе другог типа или другог редоследа, например), а може се десити и да метода Bar()
буде преименована у Baz()
.
Уско и лабаво спрегнути програмчићи
Увек када једна класа у великој мери зависи од друге класе, у нашем примеру класа А од класе Б, кажемо да су те две класе уско спрегнуте. Ако се промени функционалност класе Б а она се користи на више места у нашој апликацији, ми морамо мењати свој изворни код на свим местима где ми приступамо класи Б.
Интерфејси и апстрактне класе спашавају дан
Да би се обезбедили од могуће промене интерфејса класе (интерфејс се овде користи као термин за скуп потписа дате класе, тј. њени конструктори, методи, својства и друго - и то у смислу типова повратних вредности и броја, типова и редоследа аргумената (параметара)), можемо увести праксу да уместо коришћења конкретних класа ми у својим класама користимо интерфејсе или апстрактне класе као зависности.
class A {
private IB _b = new B();
public Foo() {
_b.Bar();
}
}
class A {
private IB _b;
public A() {
_b = new B();
}
public Foo() {
_b.Bar();
}
}
Постојаност интерфејса и апстрактних класа
Интерфејси и апстрактне класе се ретко мењају, из разлога јер свака њихова промена би значила да све класе и сви програми који зависе од њих и који се уздају у њихову непроменљивост, престају да раде. Из тог разлога, произвођачи услужних класа и фрејмворкова посвећују велику пажњу њиховом осмишљавању и једном фиксиране структуре нерадо мењају.
(Не)флексибилност тренутног решења
У нашем примеру смо увели претпоставке за апстракцију али ми и даље сами фиксирамо класу која имплементира интерфејс. То значи да ни даље немамо флексибилност јер код сваке евентуалне промене ми бисмо морали ручно преправљати код класе и поново компајлирати решење - код продукционог софтвера ове радње су временски скупе, тешке за имплементацију, често захтевају привремену обуставу рада целог система и из још и других разлога нису погодна техника.
Нека неко други одговара за тебе
Из наведених разлога било би добро да нашу класу ослободимо одговорности за креирање објекта - односно у ширем смислу уопште ослободимо одговорности за објекат. Желимо да друге компоненте буду задужене за снабдевање наше класе одговарајућим објектима од чијег постојања зависи рад наше класе. То можемо урадити на неколико начина. Два најраспрострањенија начина су додељивање објекта од којег зависимо (објекат од чијег постојања наша класа зависи - зависност) код самог инстанцирања нашег објекта (тј. путем конструктора) или путем својстава (сетера). Још један начин је да објекти од којих зависе други објекти буду инстанцирани глобално, нпр помоћу фектори шеме.
Ја не желим одговорност
Пример за прихватање зависности путем конструктора:
class A {
private IB _b;
public A(IB b) {
_b = b;
}
}
Пример за прихватање завистности путем својстава:
class A {
private IB _b;
public IB B {
get {
return _b;
}
set {
_b = value;
}
}
}
Добро, ја прихватам одговорност
Када смо своју класу променили на наведени начин, можемо је користи овако:
public static void Main() {
IB b = new B();
A a = new A(b); // mogli smo i sa new A(new B());
}
односно:
public static void Main() {
IB b = new B();
A a = new A();
A.B = b; // mogli smo i sa new B();
}
Никад ништа није једноставно
Сличан али мало другачији проблем се јавља када је класа Б сложена класа, у смислу да њено инстанцирање захтева одређена подешавања, када и она за свој рад захтева неке објекте који се прослеђују као аргументи и слично. Можемо да илуструјемо следећим примером:
Прво код
class B {
private C _c;
}
class C {
private D _d;
private E _e;
}
Све док су класе Б и Ц, односно Ц и Д и Е уско спрегнуте, тј саме инстанцирају потребне објекте, наша класа А ће радити без компликација.
class C {
private D _d = new D();
private E _e = new E();
}
class B {
private C _c = new C();
}
class A {
private B _b = new B();
}
Да и речима објаснимо
Позивањем креирања новог објекта типа А, наша класа ће креирати објекат типа Б. Објекат типа Б при креирању ће креирати објекат типа Ц а тај објекат ће опет креирати објекте типа Д и Е. Животни циклус свих тих објеката је везан за објекат типа А али нас то не занима и то прихватамо. Међутим можда су неки од тих типова класе са неким специфичним животним циклусом, можда и неким ресурсима које треба ослободити и тако даље… то већ компликује ситуацију.
Може то још мало компликованије
Шта ако су можда класе Б и Ц (па и Д и Е и тако даље) писане тако да и саме за њихово инстанцирање захтевају да им се добави (достави, обезбеди) зависност, тј објекат од чијег постојања зависи њихов рад?
На примеру наших старих познаника - А, Б, Ц, Д и Е
class C {
private D _d;
private E _e;
public C(D d, E e) {
this._d = d;
this._e = e;
}
}
class B {
private C _c;
public B(C c) {
this._c = c;
}
}
class A {
private B _b;
public A() {
D temp_d = new D();
E temp_e = new E();
C temp_c = new C(temp_d, temp_e);
_b = new B(temp_c);
}
}
Или
class A {
private B _b;
public A() {
_b = new B(new C(new D(), new E()));
}
}
Или
class A {
private B _b;
private C _c;
private D _d;
private E _e;
public A() {
_e = new E();
_d = new D();
_c = new C(_e, _d);
_b = new B(_b);
}
}
У горњим примерима смо користили конкретне класе као зависности. Да су зависности биле типа интерфејса или апстрактне класе, позивалац, тј наша класа А би поред одговорности стварања објеката на себе преузела и одговорност за избор одговарајућих имплементација код прављења објеката - превише слободе и одговорности за нашег малог клијента, тј. класу А.
Шта смо приметили
И да се радило о интерфејсима или апстрактним класама, пример илуструје како зависности ако се не укроте теже да стварају компликован, уско, тесно спрегнут код, који је подложан баговима. Евидентно је такође да о некој флексибилности нема смисла говорити код такве архитектуре.
Претпоставимо неке ствари
Претпоставимо да смо класу А преправили да инстанцирање зависности (објекта од чијег постојања зависи функционалност класе) препусти позиваоцу - путем увођења конструктора са параметром. Такође претпоставимо да класа третира зависност као интерфејс или апстрактну класу (односно прихватиће било који објекат који имплементира интерфејс или апстрактну класу). Тада бисмо, на претходном примеру повезаних класа, имали ситуацију налик овоме:
public static void Main() {
IE _e = new E();
ID _d = new D();
IC _c = new C(_e, _d);
IB _b = new B(_c);
A _a = new A(_b);
}
Само напредно
Напредак у односу на раније фазе јесте двосмеран: прво, пошто користимо интерфејсе, позивалац може уместо класе Е инстанцирати нпр класу Х која имплементира ИЕ, и на тај начин утицати на рад апликације. Друго, одговорност за инстанцирање зависности, како у погледу чина инстанцирања тако и у погледу избора имплементације, изместили смо из наше класе - на тај начин ми смо ослободили нашу класу одговорности и уједно оставили слободу избора корисницима наше класе у погледу достављања и састављања хијерархије зависности.
Апстрактне класе и интерфејси уједно решавају и проблем са тестирањем, где постоји потреба да се функције појединих класа тестирају у строго контролисаним условима. У случају сложених зависности, где су класе строго спрегнуте са конкретним класама, ми немамо начин да све спољне сервисе контролишемо помоћу прављења тзв мокинг објеката. Ипак, то је прича за неку другу прилику.
Јесмо ли завршили?
У овој фази наша класа је довоњно флексибилна ако су у питању релативно једноставне, монолитне архитектуре апликација. У таквим апликацијама можемо обезбедити да на једном месту у програму имамо логику за инстанцирање потребних зависности и на тај начин управљамо функцијама програма и коришћених класа. Када бисмо инстанцирање зависности вршили широм наше апликације на разним местима у коду врло брзо бисмо дошли до тренутка да је апликација поново постала непрегледна.
Нисмо још завршили
Када су у питању вишеслојне апликације - веб апликације су типичан пример - где се програм састоји из слојева и комуникација тих слојева је строго контролисана конвенцијама и референцама - управљање инстанцирањем зависности постаје прво, сложено, друго, пробијамо логичке границе међу слојевима и тиме кршимо захтеве да слојеви буду релативно самосталне целине у једном решењу.
Коначно: убризгавање зависности
За решење потребе централизованог управљања инстанцирањем, добављањем зависности постоји неколико дизајн патерна али се најчешће користи тзв убризгавање зависности. Начелно, то су посебне помоћне библиотеке, које повезујемо са нашом апликацијом, и којима онда, на основу претходног конфигурисања, препуштамо бригу о достављању потребних објеката на свим местима у нашем програму где се појављују зависности. Те помоћне библиотеке уско сарађују са извршним окружењем и правилно прате животни циклус објеката и још низ техничких детаља који су битни у свакодневном раду али су уједно изразито техничке природе те нису, ни у ужем ни у ширем смислу, део нашег пројектног задатка - израде решења за епродавницу или едневник или друштвену мрежу или нешто треће.
Пример у домаћој производњи
На примеру наших класа А, Б, Ц, Д и Е, нека имагинарна библиотека за убризгавање зависности би радила на следећи начин:
DItool.SupplyDependency<IB, B>();
// a ovo znaci sledece:
// Hej, DI alatko,
// kada tokom izvrsavanja programa treba dostaviti objekat koji implementira interfejs IB
// (a u programu taj objekat nije 'rucno' pripremljen i dostavljen),
// ti stupas na scenu!
// Kada se trazi objekat tipa IB, ti napravi (instanciraj) objekat tipa B i 'ubrizgaj' taj objekat
// tamo gde je to potrebno.
// Vazno! : taj objekat je sada tvoja odgovornost! Cuvaj objekat dok je to potrebno
// kada se vise ne koristi, TI si zaduzen za njegovo oslobadjanje!
// isto to i za C, D i E:
DItool.SupplyDependency<IC, C>();
DItool.SupplyDependency<ID, D>();
DItool.SupplyDependency<IE, E>();
Закључак
На овај начин смо поново централизовали управљање зависностима и уједно препустили бригу о њима трећем лицу. Као што смо прво издвојили зависност из наше класе и оставили слободу (и одговорност) кориснику, тј апликацији или другој класи, подметања одговарајућих објеката, тако смо сада ту одговорности комплетно извадили из саме апликације и препустили алатки - машини, контејнеру, библиотеци за убризгавање зависности. Исправка: крајња контрола је и даље код нас, јер ми делегирамо тај посао и одређујемо за које зависности и на који начин, међутим тзв прљави посао даље ради алат а не наш програм.
Наставак сутра
Отприлике смо сада стигли до фазе где можемо претходне кораке илуструвати и на нашој апликацији.