Owijając bibliotekę strony trzeciej, dodajesz na niej dodatkową warstwę abstrakcji. Ma to kilka zalet:
Baza kodu staje się bardziej elastyczna na zmiany
Jeśli kiedykolwiek zajdzie potrzeba zastąpienia biblioteki inną biblioteką, wystarczy zmienić implementację w opakowaniu - w jednym miejscu . Możesz zmienić implementację opakowania i nie musisz niczego zmieniać, innymi słowy, masz luźno powiązany system. W przeciwnym razie musiałbyś przejrzeć całą bazę kodu i dokonać modyfikacji wszędzie - co oczywiście nie jest tym, czego chcesz.
Interfejs API opakowania można zdefiniować niezależnie od interfejsu API biblioteki
Różne biblioteki mogą mieć bardzo różne interfejsy API, a jednocześnie żadna z nich może nie być dokładnie tym, czego potrzebujesz. Co się stanie, jeśli jakaś biblioteka potrzebuje tokena do przekazania wraz z każdym połączeniem? Możesz przekazać token w swojej aplikacji, gdziekolwiek chcesz użyć biblioteki, lub możesz go zabezpieczyć gdzieś bardziej centralnie, ale w każdym razie potrzebujesz tokena. Twoja klasa opakowań ponownie upraszcza to wszystko - ponieważ możesz po prostu trzymać token wewnątrz klasy opakowania, nigdy nie wystawiając go na działanie żadnego komponentu w aplikacji i całkowicie odciąć od tego potrzebę. Ogromna zaleta, jeśli kiedykolwiek korzystałeś z biblioteki, która nie podkreśla dobrego projektu API.
Testowanie jednostkowe jest znacznie prostsze
Testy jednostkowe powinny testować tylko jedną rzecz. Jeśli chcesz przetestować jednostkę w klasie, musisz wyśmiewać jej zależności. Staje się to jeszcze ważniejsze, jeśli klasa ta wykonuje połączenia sieciowe lub uzyskuje dostęp do innych zasobów poza twoim oprogramowaniem. Otaczając bibliotekę innej firmy, można łatwo wyśmiewać te połączenia i zwracać dane testowe lub cokolwiek, czego wymaga test jednostkowy. Jeśli nie masz takiej warstwy abstrakcji, staje się to znacznie trudniejsze - i przez większość czasu powoduje to dużo brzydkiego kodu.
Tworzysz luźno powiązany system
Zmiany w opakowaniu nie mają wpływu na inne części oprogramowania - przynajmniej o ile nie zmienisz zachowania opakowania. Wprowadzając warstwę abstrakcji, taką jak to opakowanie, możesz uprościć połączenia z biblioteką i prawie całkowicie usunąć zależność aplikacji od tej biblioteki. Twoje oprogramowanie będzie po prostu używać opakowania i nie będzie miało znaczenia, w jaki sposób opakowanie jest wdrażane ani w jaki sposób robi to, co robi.
Praktyczny przykład
Bądźmy szczerzy. Ludzie mogą dyskutować o zaletach i wadach czegoś takiego przez wiele godzin - dlatego raczej raczej pokazuję wam przykład.
Załóżmy, że masz aplikację na Androida i musisz pobrać obrazy. Istnieje wiele bibliotek, dzięki którym ładowanie i buforowanie obrazów jest dziecinnie proste, na przykład Picasso lub Universal Image Loader .
Możemy teraz zdefiniować interfejs, którego będziemy używać do zawijania dowolnej biblioteki, w której ostatecznie wykorzystamy:
public interface ImageService {
Bitmap load(String url);
}
Jest to interfejs, którego możemy teraz używać w aplikacji za każdym razem, gdy potrzebujemy załadować obraz. Możemy stworzyć implementację tego interfejsu i użyć wstrzykiwania zależności, aby wstrzyknąć instancję tej implementacji wszędzie tam, gdzie używamy ImageService
.
Załóżmy, że początkowo zdecydowaliśmy się na użycie Picassa. Możemy teraz napisać implementację, dla ImageService
której Picasso korzysta wewnętrznie:
public class PicassoImageService implements ImageService {
private final Context mContext;
public PicassoImageService(Context context) {
mContext = context;
}
@Override
public Bitmap load(String url) {
return Picasso.with(mContext).load(url).get();
}
}
Całkiem prosto, jeśli mnie zapytasz. Otaczanie bibliotek nie musi być skomplikowane, aby było przydatne. Interfejs i implementacja mają mniej niż 25 połączonych linii kodu, więc stworzenie tego nie było prawie żadnym wysiłkiem, ale już coś zyskujemy dzięki temu. Zobacz Context
pole we wdrożeniu? Wybrana przez Ciebie struktura wstrzykiwania zależności już zadba o wstrzyknięcie tej zależności, zanim jeszcze skorzystamy z naszej ImageService
, twoja aplikacja nie musi teraz przejmować się sposobem pobierania obrazów i innymi zależnościami, jakie może mieć biblioteka. Twoja aplikacja widzi tylko to, ImageService
a kiedy potrzebuje obrazu, wywołuje go load()
za pomocą adresu URL - prosty i bezpośredni.
Jednak prawdziwa korzyść przychodzi, gdy zaczynamy coś zmieniać. Wyobraź sobie, że musimy teraz zastąpić Picasso uniwersalnym programem ładującym obrazy, ponieważ Picasso nie obsługuje niektórych funkcji, których absolutnie potrzebujemy w tej chwili. Czy musimy teraz przeczesywać naszą bazę kodów i żmudnie zastępować wszystkie wywołania Picassa, a następnie radzić sobie z dziesiątkami błędów kompilacji, ponieważ zapomnieliśmy o kilku wywołaniach Picassa? Nie. Wszystko, co musimy zrobić, to stworzyć nową implementację ImageService
i poinformować naszą strukturę wstrzykiwania zależności, aby od tej pory korzystała z tej implementacji:
public class UniversalImageLoaderImageService implements ImageService {
private final ImageLoader mImageLoader;
public UniversalImageLoaderImageService(Context context) {
DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.defaultDisplayImageOptions(defaultOptions)
.build();
mImageLoader = ImageLoader.getInstance();
mImageLoader.init(config);
}
@Override
public Bitmap load(String url) {
return mImageLoader.loadImageSync(url);
}
}
Jak widać implementacja może być bardzo różna, ale to nie ma znaczenia. Nie musieliśmy zmieniać ani jednego wiersza kodu nigdzie indziej w naszej aplikacji. Używamy zupełnie innej biblioteki, która może mieć zupełnie inne funkcje lub może być używana zupełnie inaczej, ale nasza aplikacja po prostu nie ma znaczenia. Tak jak wcześniej reszta naszej aplikacji widzi ImageService
interfejs ze swoją load()
metodą, ale ta metoda jest zaimplementowana, nie ma już znaczenia.
Przynajmniej dla mnie to wszystko już brzmi całkiem ładnie, ale poczekaj! Jest jeszcze więcej. Wyobraź sobie, że piszesz testy jednostkowe dla klasy, nad którą pracujesz, a ta klasa używa ImageService
. Oczywiście nie możesz pozwolić, aby testy jednostkowe nawiązywały połączenia sieciowe z niektórymi zasobami znajdującymi się na innym serwerze, ale ponieważ teraz używasz tej opcji ImageService
, możesz łatwo pozwolić na load()
zwrócenie wartości statycznej Bitmap
używanej do testów jednostkowych poprzez wdrożenie fałszywego ImageService
:
public class MockImageService implements ImageService {
private final Bitmap mMockBitmap;
public MockImageService(Bitmap mockBitmap) {
mMockBitmap = mockBitmap;
}
@Override
public Bitmap load(String url) {
return mMockBitmap;
}
}
Podsumowując, opakowując biblioteki stron trzecich, baza kodu staje się bardziej elastyczna w stosunku do zmian, ogólnie prostsza, łatwiejsza do testowania i zmniejszasz sprzężenie różnych komponentów w oprogramowaniu - wszystkie rzeczy, które stają się coraz ważniejsze, im dłużej utrzymujesz oprogramowanie.