SOLIDny tydzień – D jak Dependency Inversion
W końcu przyszła pora na ostatnią literę ze zbioru SOLID czyli D – Dependency Inversion Pronciple (DIP) czyli reguła odwrócenia zależności. Łatwa w zrozumieniu. Prosta w implementacji. Idealna na weekend.
W skrócie
Odwrócenie zależności może niekoniecznie brzmi jak coś oczywistego. Zwłaszcza dla osób, które nie posługują się jeszcze biegle „żargonem” programistycznym. Jednak pod tym określeniem kryje się prosta zasada. Mianowicie chodzi o to, żeby klasy wyższego poziomu nie były zależne od implementacji klas niższego poziomu i żeby najważniejsze elementy komunikowały się przez abstrakcje.
Czyli, że co?
Czym są te klasy wyższego i niższego poziomu? Klasy wyższego poziomu są to zazwyczaj klasy bliższe logice biznesowej, jakimś zachowaniom nie związanym z częścią techniczną ale z funkcjami dostępnymi dla użytkownika – im bliżej użytkownika tym wyższy poziom. W takim razie klasami niższego poziomu będą np. serwisy to wysyłania wiadomości przez konkretny kanał, integrujące się z innymi usługami albo zapisujące do bazy. Skupiają się one na zagadnieniach technicznych.
Przenosząc regułę do świata rzeczywistego można dać przykład z aparatem i kartą pamięci (akurat mam go przed oczami, stąd takie skojarzenie). Jeżeli aparat łamałby regułę DIP to oznaczałoby, że może być do niego włożony konkretny tylko model karty pamięci konkretnego producenta – aparat przyjmuje model X producenta Y i koniec. Chcesz użyć innego, weź inny aparat. Od razu brzmi jak jakiś absurd. Aparat nie jest zależny od konkretnej „implementacji” karty. Korzysta z przyjętego interfejsu jakim jest standard kart SD. Dlatego każda karta, która spełnia ten standard może zostać użyta i nie musimy niczego modyfikować w aparacie żeby zmienić ją na inny egzemplarz innego producenta.
W naszym przykładzie aparat jest klasą wyższego rzędu bo wykonuje „logikę biznesową” jaką jest robienie zdjęć. Karta pamięci jest zaś klasą niższego rzędu bo jest technicznym sposobem przechowania danych. Aparat chce dane po prostu jakoś przechować, nie obchodzi go jak. Za to karta robi to w konkretny sposób.
Klasycznie
Tak samo powinno to wyglądać w przypadku klas w Twojej aplikacji. Te wykonujące funkcjonalności bliższe użytkownikowi nie powinny bezpośrednio zależeć od konkretnych implementacji klas bliższych technicznym zagadnieniom.
Skoro tak mówimy o zapisie to weźmy sobie za przykład klasę modyfikującą zdjęcie i klasę zapisującą je na dysku:
class HddImageSaver { public void SaveImage(Image image) { // zapisywanie w katalogu na dysku } } class ImageManipulator { private HddImageSaver _imageSaver; public ImageManipulator(HddImageSaver imageSaver) { _imageSaver = imageSaver; } public void Resize(Image image, float x, float y) { // zmiana rozmiaru zdjęcia _imageSaver.SaveImage(image); } }
W powyższym przykładzie klasa odpowiadająca za funkcjonalność bliższą użytkownikowi zależy od konkretnej implementacji szczegółów technicznych jakimi jest zapis w konkretny sposób czyli na dysk.
Takie rozwiązanie będzie działać, ale ma dwie podstawowe wady:
- nie pozwala przetestować jednostkowo klasy manipulującej zdjęciem – jesteśmy zawsze zależni od metody zapisującej na dysk więc chcąc testować funkcję Resize() mielibyśmy zawsze efekt uboczny w postaci faktycznego zapisu na dysk dla każdego testu
- Nie możemy w prosty sposób zmienić sposobu zapisu zdjęcia – chcąc zamienić katalog na dysku na bazę danych musimy zmodyfikować klasę ImageManipulator. A jak wiesz z tekstu poświęconego zasadzie Otwarte/Zamknięte taka sytuacja nie powinna mieć miejsca.
Rozdzielmy je!
Rozwiązaniem jest dodanie interfejsu, przez który obie klasy będą „rozmawiać”:
interface IImageSaver { void SaveImage(Image image); } class HddImageSaver : IImageSaver { public void SaveImage(Image image) { // zapisywanie w katalogu na dysku } } class ImageManipulator { private IImageSaver _imageSaver; public ImageManipulator(IImageSaver imageSaver) { _imageSaver = imageSaver; } public void Resize(Image image, float x, float y) { // zmiana rozmiaru zdjęcia _imageSaver.SaveImage(image); } }
Dzięki temu bez problemu możemy żonglować implementacjami bo klasy wyższego poziomu kompletnie nie obchodzi jaka implementacja jest aktualnie użyta.
Oczywiście jak to zawsze w życiu bywa ważne jest zachowanie zdrowego rozsądku i trzymanie się zasady YAGNI. Bo nie wszystkie klasy wymagają interfejsu. Jeżeli jakaś klasa jest tylko elementem innej klasy i zawsze będą musiały być użyte w tych konkretnie implementacjach to nie ma sensu robić interfejsu dla samego faktu zrobienia interfejsu, który byłby i tak implementowany na pewno tylko raz.
Leave a Comment