Odwracanie, wstrzykiwanie – pora rzucić okiem na nie. Część 2
Poprzednio odwracałem sterowanie (lub kontrolę, jak kto woli :)). Dzisiaj pora odwrócić zależność. Zasada odwracania zależności (Dependency Inversion Principle) to ostatnia (licząc wg porządku liter w nazwie) z zestawu zasad SOLID. O co więc chodzi z tą zależnością i na czym tak naprawdę polega jej odwracanie? Najlepiej będzie zademonstrować to na przykładzie.
Oglądaliście Seksmisję (to już 30 lat od jej premiery)? Był w niej taki alert: „Weź pigułkę”. Załóżmy, że mamy właśnie taką aplikację, która o określonej porze ma poinformować jej użytkownika o wzięciu pigułki. Oto jak by ona wyglądała:
class Program { static void Main(string[] args) { var reminder = new Reminder(); reminder.Await(); } } class Reminder { public void Await() { var textToVoice = new TextToVoice(); while (true) { if (Abort()) break; if (ItsTime()) textToVoice.Say("Weź pigułkę"); } } private bool Abort() { //... } private bool ItsTime() { //... } } class TextToVoice { public void Say(string sentence) { //... } }
To co daje się zauważyć w powyższym programie to uzależnienie klasy Reminder od klasy TextToVoice. Ktoś może zaprotestować, że dlaczego zaraz „uzależnienie„. Otóż inaczej tego nazwać nie można – aby klasa Reminder działała poprawnie, wymaga istnienia klasy TextToVoice, inaczej nie będzie mogła funkcjonować – czyż to nie jest zależność? Jedno bez drugiego działać nie może – zatem jedno od drugiego zwyczajnie zależy.
Co jeszcze uderza w tym kodzie? Aby się tego dowiedzieć, załóżmy, że użytkownikami programu mogą być też osoby niesłyszące. Tym należałoby przypomnienie pokazywać, zamiast je wypowiadać. Hm? Czyli trzeba program dostosować choćby tak:
class Program { static void Main(string[] args) { var reminder = new Reminder(); reminder.Await(true); } } class Reminder { public void Await(bool display) { TextToDisplay textToDisplay = null; TextToVoice textToVoice = null; string message = "Weź pigułkę"; if (display) textToDisplay = new TextToDisplay(); else textToVoice = new TextToVoice(); while (true) { if (Abort()) break; if (ItsTime()) if (display) textToDisplay.Display(message); else textToVoice.Say(message); } } } class TextToDisplay { public void Display(string sentence) { //... } }
Kod się znacząco rozbudował, co ujawniło, że oprócz tego, że zależy on od używanych klas, to zależy od nich także logika jego działania. Obecnie musi on uwzględniać czy ma mówić czy wyświetlać informację i w zależności od tego inaczej działać. Bez dwóch zdań poprzednio logika programu także zależała od używanej klasy, ale trudniej było to pokazać, kiedy program używa dwóch klas tę zależność logiki widać jak na dłoni. Oczywiście obecnie program zależy już od dwóch klas, co jest oczywiste i czego należało się spodziewać.
Nasuwa się więc wniosek, że dodanie kolejnego sposobu komunikowania o potrzebie wzięcia pigułki (np. poprzez wibrację lub błyśnięcie światłem) jeszcze bardziej wpłynie na logikę programu. A przecież tak naprawdę logika takiego przypominacza powinna opierać się na następującym scenariuszu:
- czy już czas, aby powiadomić użytkownika
- tak – powiadom go
- nie – wykonaj ponownie sprawdzenie z punktu 1
To wszystko co przypominacz powinien robić. Aby jednak mogło tak być, powinien przestać interesować się używanymi mechanizmami powiadamiania – powinien po prostu powiadamiać. Sposób, w jaki będzie się to działo nie jest zaś jego sprawą. Czyli potrzebny mu jest tak naprawdę nie konkretny, a uniwersalny mechanizm powiadamiania. Taki mechanizm to właśnie coś, co w programowaniu nazywamy abstrakcją. Podążając tym tropem należałoby zdefiniować klasę abstrakcyjną (tu Notifier) służącą do dowolnego powiadamiania, czyli:
class Reminder { public void Await(bool display) { Notifier notifier = null; string notification = "Weź pigułkę"; if (display) notifier = new TextToDisplay(); else notifier = new TextToVoice(); while (true) { if (Abort()) break; if (ItsTime()) notifier.Notify(notification); } } } class Notifier { public abstract void Notify(string notification); } class TextToVoice : Notifier { public override void Notify(string notification) { Say(notification); } private void Say(string sentence) { //... } } class TextToDisplay : Notifier { public override void Notify(string notification) { Display(notification); } private void Display(string sentence) { //... } }
Jak widać sytuacja się zmieniła. Co prawda Reminder nadal od czegoś zależy (co nie dziwi – jest usługobiorcą, korzysta z usług, musi od nich zależeć), ale obecnie jest to klasa abstrakcyjna (abstrakcja), a nie konkretny mechanizm. Ogólność (abstrakcyjność) uzależniającej klasy sprawia, że zależność ta nie jest już przeszkodą, bo nie niesie ze sobą żadnych kosztów. Pod klasą tą może zostać obecnie ukryty dowolny mechanizm (nawet wystukujący przypomnienie alfabetem Morse’a) i jaki by on nie był (i jaki by się kiedyś nie pojawił), nie będzie to miało wpływu na klasę korzystającą z klasy abstrakcyjnej.
Najbardziej istotną zmianą, jaką wniósł powyższy kod jest jednak to, że odwróciła się zależność pomiędzy klasą korzystającą z klas specjalizowanych i klasami specjalizowanymi – to one zamiast nadal uzależniać, zaczęły być zależne od klasy abstrakcyjnej (obecnie po niej dziedziczą). To jest właśnie owo odwrócenie, które występuje w nazwie zasady.
Jak widać abstrakcja (klasa abstrakcyjna) stała się pomostem pomiędzy dotychczas sztywno połączonymi klasami, a zależność Reminder od TextToVoice i TextToDisplay została dzięki niej odwrócona. Teraz to usługodawcy (klasy usługowe) zależą (pośrednio, przez abstrakcję) od usługobiorcy. I to właśnie jest zasada odwracania zależności:
A: Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obie grupy modułów powinny zależeć od abstrakcji.
B: Abstrakcje nie powinny zależeć od szczegółowych rozwiązań. To szczegółowe rozwiązania powinny zależeć od abstrakcji.
Oczywiście użycie klasy abstrakcyjnej, choć uelastycznia całość oprogramowania, nie jest szczytem tej elastyczności. Wymaga bowiem, aby klasy, które implementują stosowne mechanizmy, dziedziczyły z klasy abstrakcyjnej. Rzecz w tym, że może nie być takiej możliwości (bo już dziedziczą po czymś innym), a poza tym – tak naprawdę w klasie abstrakcyjne nie ma nic, co byłoby im potrzebne do działania. Wszak nie ma tam żadnego kodu, który byłby wspólny dla obu mechanizmów, jedyne co jest wspólne, to interfejs. I dlatego właśnie najlepszym, bo najbardziej elastycznym rozwiązaniem, będzie sprawienie, by klasy konkretnych mechanizmów implementowały po prostu odpowiedni interfejs. Diagram zatem powinien wyglądać tak:
Na koniec warto zwrócić uwagę na postać interfejsu. Co ją determinuje? Czy postać ta wynika z potrzeb specjalizowanych mechanizmów (usługodawców)? Nie, jest ona odpowiedzią na potrzeby usługobiorcy. To on, a nie usługodawcy, jest tutaj „panem sytuacji„, to on ma swoją potrzebę (chce powiadamiać), a usługodawcy mają mu taką możliwość zapewnić (nasz klient – nasz pan). Właśnie dzięki takiemu podejściu usługobiorca jest całkowicie uniezależniony od usługodawców, natomiast oni zależą od niego (muszą się do niego dostosować). Aby to dostosowanie zobrazować zostawiłem dotychczasowe metody obu mechanizmów i dodałem tę uniwersalną, narzucaną przez abstrakcję. Dopiero w jej ramach wywoływane są metody specjalizowane – umiejętności konkretnych klas. To jeszcze jeden przejaw odwrócenia zależności.
Odwracajmy zatem zależność, abyśmy nigdy nie znaleźli się w takiej sytuacji 😉 :
Oczywiście obecna postać klasy jest daleka od doskonałości, w dodatku nadal zależy od klas, od których chciałem ją uniezależnić. Tę niedogodność (którą obecnie celowo pozostawiam) wyeliminuję już wkrótce, poruszając ostatni temat tego cyklu – wstrzykiwanie zależności.