Większość projektów, w które jestem zaangażowany, wykorzystuje kilka komponentów typu open source. Zgodnie z ogólną zasadą, czy dobrym pomysłem jest zawsze unikanie wiązania wszystkich składników kodu do bibliotek stron trzecich, a zamiast tego przechodzenie przez enkapsulujące opakowanie, aby uniknąć bólu związanego ze zmianami?
Na przykład większość naszych projektów PHP bezpośrednio używa log4php jako struktury rejestrowania, tzn. Są one tworzone za pomocą \ Logger :: getLogger (), używają -> info () lub -> warn () itp. W przyszłości może się jednak pojawić hipotetyczna struktura rejestrowania, która jest w pewien sposób lepsza. Na obecnym etapie wszystkie projekty ściśle powiązane z podpisami metody log4php musiałyby się zmieniać w kilkudziesięciu miejscach, aby dopasować je do nowych podpisów. Miałoby to oczywiście duży wpływ na bazę kodów, a każda zmiana jest potencjalnym problemem.
Aby przygotować przyszłe nowe bazy kodu z tego rodzaju scenariusza, często rozważam (a czasem implementuję) klasę opakowania, która zawiera funkcje rejestrowania i ułatwia, choć nie jest niezawodny, zmianę sposobu, w jaki rejestrowanie działa w przyszłości przy minimalnej zmianie ; kod wywołuje opakowanie, opakowanie przekazuje wywołanie do środowiska rejestrowania du Jour .
Pamiętając o tym, że istnieją inne bardziej skomplikowane przykłady z innymi bibliotekami, czy jestem nadmiernie inżynieryjny, czy jest to rozsądne środki ostrożności w większości przypadków?
EDYCJA: Więcej uwag - użycie wstrzykiwania zależności i dublowania testów praktycznie wymaga, aby i tak wyodrębnić większość interfejsów API („Chcę sprawdzić, czy mój kod wykonuje i aktualizuje jego stan, ale nie pisać komentarza do dziennika / uzyskać dostęp do prawdziwej bazy danych”). Czy to nie decyduje?
źródło
Odpowiedzi:
Jeśli używasz tylko niewielkiego podzbioru interfejsu API innej firmy, warto napisać opakowanie - pomaga to w enkapsulacji i ukrywaniu informacji, dzięki czemu nie narażasz potencjalnie dużego API na własny kod. Może także pomóc upewnić się, że każda funkcja, której nie chcesz używać, jest „ukryta”.
Innym dobrym powodem dla opakowania jest to, że spodziewasz się zmienić bibliotekę strony trzeciej. Jeśli jest to infrastruktura, o której wiesz, że się nie zmienisz, nie pisz dla niej opakowania.
źródło
Nie wiedząc, jakie super wspaniałe nowe funkcje będzie miał ten rzekomo ulepszony rejestrator, jak napisałbyś opakowanie? Najbardziej logicznym wyborem jest, aby otoki utworzyły jakąś klasę logger i miały metody takie jak
->info()
lub->warn()
. Innymi słowy, zasadniczo identyczny z obecnym API.Zamiast kodu na przyszłość, którego nigdy nie będę musiał zmieniać lub który może wymagać nieuniknionego przepisania, wolę kod „na przeszłość”. Oznacza to, że w rzadkich przypadkach, kiedy zmieniają znacząco składnik, to kiedy piszę otoki aby był on zgodny z ostatniego kodu. Jednak każdy nowy kod korzysta z nowego interfejsu API i zmieniam stary kod, aby użyć go za każdym razem, gdy i tak dokonuję zmiany w tym samym pliku lub na ile pozwala na to harmonogram. Po kilku miesiącach mogę usunąć opakowanie, a zmiana jest stopniowa i solidna.
Innymi słowy, opakowania naprawdę mają sens tylko wtedy, gdy znasz już wszystkie interfejsy API, które musisz zawinąć. Dobrym przykładem jest sytuacja, w której aplikacja musi obsługiwać wiele różnych sterowników baz danych, systemów operacyjnych lub wersji PHP.
źródło
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:
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: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 naszejImageService
, 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 goload()
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: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 opcjiImageService
, możesz łatwo pozwolić naload()
zwrócenie wartości statycznejBitmap
używanej do testów jednostkowych poprzez wdrożenie fałszywegoImageService
: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.
źródło
Myślę, że owijanie bibliotek stron trzecich na wypadek, gdyby jutro pojawi się coś lepszego, jest bardzo marnotrawnym naruszeniem YAGNI. Jeśli wielokrotnie wywołujesz kod innej firmy w sposób charakterystyczny dla Twojej aplikacji, powinieneś (należy) przekierować te wywołania do klasy zawijającej, aby wyeliminować powtarzanie. W przeciwnym razie w pełni używasz interfejsu API biblioteki, a każde opakowanie wyglądałoby tak jak sama biblioteka.
Załóżmy teraz, że pojawi się nowa biblioteka z lepszą wydajnością lub czymkolwiek. W pierwszym przypadku po prostu przepisujesz opakowanie dla nowego interfejsu API. Nie ma problemu.
W drugim przypadku tworzysz opakowanie dostosowujące stary interfejs do sterowania nową biblioteką. Trochę więcej pracy, ale nie ma problemu i nie więcej pracy niż zrobiłbyś, gdybyś napisał opakowanie wcześniej.
źródło
Podstawowym powodem napisania opakowania wokół biblioteki innej firmy jest to, że możesz wymieniać bibliotekę innej firmy bez zmiany kodu, który jej używa. Nie można uniknąć sprzężenia z czymś, więc argumentuje, że lepiej jest połączyć się z napisanym przez Ciebie interfejsem API.
To, czy jest to warte wysiłku, to inna historia. Ta debata prawdopodobnie potrwa jeszcze długo.
W przypadku małych projektów, w których prawdopodobieństwo, że taka zmiana będzie konieczna, jest niewielkie, jest to prawdopodobnie niepotrzebny wysiłek. W przypadku większych projektów ta elastyczność może znacznie przewyższyć dodatkowy wysiłek związany z opakowaniem biblioteki. Trudno jednak wcześniej ustalić, czy tak jest.
Innym sposobem spojrzenia na to jest podstawowa zasada abstrakcji tego, co może się zmienić. Tak więc, jeśli biblioteka innej firmy jest dobrze ugruntowana i jest mało prawdopodobne, że zostanie zmieniona, może być nieopakowanie jej. Jeśli jednak biblioteka innej firmy jest stosunkowo nowa, istnieje większe prawdopodobieństwo, że będzie trzeba ją wymienić. To powiedziawszy, rozwój ustalonych bibliotek był porzucany wiele razy. Odpowiedź na to pytanie nie jest łatwa.
źródło
Oprócz tego, co powiedział już @Oded , chciałbym tylko dodać tę odpowiedź do specjalnego celu logowania.
Zawsze mam interfejs do logowania, ale nigdy nie musiałem zastępować
log4foo
frameworka.Dostarczenie interfejsu i napisanie opakowania zajmuje tylko pół godziny, więc myślę, że nie tracisz zbyt wiele czasu, jeśli okaże się to niepotrzebne.
To szczególny przypadek YAGNI. Chociaż nie potrzebuję go, nie zajmuje to dużo czasu i czuję się z tym bezpieczniej. Jeśli dzień wymiany rejestratora naprawdę nadejdzie, cieszę się, że zainwestowałem pół godziny, ponieważ pozwoli mi to zaoszczędzić więcej niż dzień na wymianie połączeń w projekcie z prawdziwego świata. Nigdy nie pisałem ani nie widziałem testu jednostkowego do rejestrowania (oprócz testów samej implementacji rejestratora), więc oczekuj defektów bez opakowania.
źródło
Dokładnie rozwiązuję ten problem w projekcie, nad którym obecnie pracuję. Ale w moim przypadku biblioteka służy do grafiki i dlatego jestem w stanie ograniczyć jej użycie do niewielkiej liczby klas, które zajmują się grafiką, w przeciwieństwie do rozsypywania jej w całym projekcie. Dlatego w razie potrzeby można łatwo zmieniać interfejsy API; w przypadku rejestratora sprawa staje się znacznie bardziej skomplikowana.
Dlatego powiedziałbym, że decyzja ma wiele wspólnego z tym, co dokładnie robi biblioteka innej firmy i ile bólu wiązałoby się z jej zmianą. Jeśli zmiana wszystkich wywołań interfejsu API byłaby łatwa, niezależnie od tego, prawdopodobnie nie warto tego robić. Jeśli jednak późniejsza zmiana biblioteki byłaby naprawdę trudna, prawdopodobnie teraz ją zapakuję.
Poza tym inne odpowiedzi bardzo dobrze ujęły główne pytanie, więc chcę skupić się na ostatnim dodatku, na wstrzykiwaniu zależności i próbnych obiektach. Zależy to oczywiście od tego, jak dokładnie działa środowisko rejestrowania, ale w większości przypadków nie wymagałoby to opakowania (chociaż prawdopodobnie skorzysta z niego). Po prostu ułóż interfejs API dla fałszywego obiektu dokładnie tak samo, jak biblioteki innej firmy, a następnie możesz łatwo zamienić sztuczny obiekt do testowania.
Głównym czynnikiem tutaj jest to, czy biblioteka innej firmy jest nawet implementowana poprzez wstrzykiwanie zależności (lub lokalizator usług lub jakiś taki luźno powiązany wzór). Jeśli dostęp do funkcji biblioteki uzyskuje się za pomocą metod singletonowych lub statycznych lub w inny sposób, należy to zawinąć w obiekt, z którym można pracować przy wstrzykiwaniu zależności.
źródło
Jestem zdecydowanie w obozie pakowania i nie jestem w stanie zastąpić biblioteki strony trzeciej największym priorytetem (choć jest to premia). Moje główne uzasadnienie sprzyjające pakowaniu jest proste
I objawia się to zwykle w postaci mnóstwa duplikacji kodu, jak na przykład programiści piszący 8 wierszy kodu tylko w celu stworzenia
QButton
i nadania mu stylu, tak jak powinien wyglądać aplikacja, tylko dla projektanta, który chce nie tylko wyglądu ale także funkcjonalność przycisków, które można całkowicie zmienić w całym oprogramowaniu, co w końcu wymaga cofnięcia i przepisania tysięcy wierszy kodu, lub stwierdzenia, że modernizacja potoku renderowania wymaga epickiego przepisania, ponieważ podstawa kodu obsypana ad-hoc niskiego poziomu naprawiona- potokuj kod OpenGL w dowolnym miejscu zamiast scentralizować projekt mechanizmu renderującego w czasie rzeczywistym i pozostawić użycie OGL wyłącznie do jego implementacji.Te projekty nie są dostosowane do naszych konkretnych potrzeb projektowych. Oferują one ogromny nadzór nad tym, co jest rzeczywiście potrzebne (a to, co nie jest częścią projektu, jest tak ważne, jeśli nie większe niż to, co jest), a ich interfejsy nie są zaprojektowane tak, aby konkretnie obsługiwać nasze potrzeby w „wysokim” think = one request ”sposób, który pozbawia nas wszelkiej centralnej kontroli projektu, jeśli wykorzystamy je bezpośrednio. Jeśli programiści skończą pisać dużo kodu niższego poziomu, niż powinien być zobowiązany do wyrażenia tego, czego potrzebują, mogą czasem sami go zapakować w sposób ad-hoc, co sprawia, że kończy się to dziesiątkami pochopnie napisanych i nieuprzejmie- zaprojektowane i udokumentowane opakowania zamiast jednego dobrze zaprojektowanego i dobrze udokumentowanego.
Oczywiście zastosowałbym wyraźne wyjątki od bibliotek, w których opakowania są prawie jeden do jednego tłumaczeniem tego, co oferują interfejsy API stron trzecich. W takim przypadku może nie być poszukiwanego projektu wyższego poziomu, który bardziej bezpośrednio wyraża wymagania biznesowe i projektowe (może tak być w przypadku czegoś bardziej przypominającego bibliotekę „narzędziową”). Ale jeśli istnieje o wiele bardziej dostosowany projekt, który bardziej bezpośrednio wyraża nasze potrzeby, to jestem zdecydowanie w obozie pakowania, podobnie jak zdecydowanie opowiadam się za użyciem funkcji wyższego poziomu i ponownego użycia jej zamiast wstawiania kodu asemblera wszędzie wokoło.
Dziwne, że starłem się z programistami w taki sposób, że wydawali się tak nieufni i tak pesymistyczni co do naszej zdolności zaprojektowania, powiedzmy, funkcji tworzenia przycisku i zwrócenia go, że wolą napisać 8 wierszy kodu niższego poziomu skoncentrowanego na mikroskopii szczegóły dotyczące tworzenia przycisków (które w przyszłości musiały się wielokrotnie zmieniać) nad projektowaniem i używaniem tej funkcji. Nawet nie widzę celu, w którym staramy się projektować cokolwiek, jeśli nie możemy ufać sobie, że zaprojektujemy tego rodzaju opakowania w rozsądny sposób.
Innymi słowy, postrzegam biblioteki stron trzecich jako potencjalne sposoby na zaoszczędzenie ogromnego czasu podczas wdrażania, a nie jako substytuty projektowania systemów.
źródło
Mój pomysł na biblioteki stron trzecich:
Nie było pewne dyskusje niedawno w społeczności iOS na temat zalet i wad (OK, głównie cons) korzystania z zależnościami osób trzecich. Wiele argumentów, które widziałem, było dość ogólnych - pogrupowanie wszystkich bibliotek stron trzecich w jeden koszyk. Jednak jak w przypadku większości rzeczy, nie jest to takie proste. Spróbujmy więc skupić się na jednym przypadku
Czy powinniśmy unikać korzystania z bibliotek interfejsu użytkownika innych firm?
Powody, dla których warto rozważyć biblioteki stron trzecich:
Wydaje się, że istnieją dwa główne powody, dla których programiści rozważają użycie biblioteki innej firmy:
Większość bibliotek interfejsu użytkownika ( nie wszystkie! ) Ma tendencję do zaliczania się do drugiej kategorii. To nie jest nauka o rakietach, ale zbudowanie jej wymaga czasu.
Istnieją prawie dwa rodzaje kontroli / widoków:
UICollectionView
zUIKit
.UIPickerView
. Większość bibliotek stron trzecich zwykle zalicza się do drugiej kategorii. Co więcej, często są one pobierane z istniejącej bazy kodu, dla której zostały zoptymalizowane.Nieznane wczesne założenia
Wielu programistów dokonuje przeglądu kodu wewnętrznego, ale może przyjmować jakość kodu źródłowego innej firmy za pewnik. Warto poświęcić trochę czasu na przeglądanie kodu biblioteki. Możesz być zaskoczony, widząc czerwone flagi, np. Zamiatanie używane tam, gdzie nie jest potrzebne.
Nie możesz tego ukryć
Ze względu na sposób zaprojektowania UIKit najprawdopodobniej nie będziesz w stanie ukryć biblioteki interfejsu użytkownika innej firmy, np. Za adapterem. Biblioteka przeplata się z twoim kodem interfejsu użytkownika, stając się de facto twoim projektem.
Koszt w przyszłości
UIKit zmienia się z każdą wersją iOS. Wszystko się zepsuje. Twoje uzależnienie od osób trzecich nie będzie tak bezobsługowe, jak możesz się spodziewać.
Wniosek:
Z mojego osobistego doświadczenia wynika, że większość zastosowań kodu interfejsu użytkownika innej firmy sprowadza się do wymiany mniejszej elastyczności na pewien czas.
Używamy gotowego kodu, aby szybciej wysłać naszą aktualną wersję. Wcześniej czy później przekroczyliśmy granice biblioteki i stanęliśmy przed trudną decyzją: co dalej?
źródło
Bezpośrednie korzystanie z biblioteki jest bardziej przyjazne dla zespołu programistów. Kiedy dołącza nowy programista, może mieć pełne doświadczenie ze wszystkimi używanymi frameworkami, ale nie będzie w stanie wnieść produktywnego wkładu przed nauczeniem się własnego API. Gdy młodszy programista próbuje rozwijać się w grupie, będzie zmuszony nauczyć się konkretnego interfejsu API, którego nie ma nigdzie indziej, zamiast nabyć bardziej przydatnych ogólnych kompetencji. Jeśli ktoś zna przydatne funkcje lub możliwości oryginalnego API, może nie być w stanie sięgnąć ponad warstwę napisaną przez kogoś, kto nie był ich świadomy. Jeśli ktoś dostanie zadanie programowania podczas szukania pracy, może nie być w stanie zademonstrować podstawowych rzeczy, z których korzystał wiele razy, tylko dlatego, że przez cały ten czas uzyskiwał dostęp do potrzebnej funkcjonalności za pośrednictwem twojego opakowania.
Myślę, że te problemy mogą być ważniejsze niż raczej odległa możliwość późniejszego korzystania z zupełnie innej biblioteki. Jedynym przypadkiem, w którym użyłbym opakowania, jest to, że migracja do innej implementacji jest zdecydowanie zaplanowana lub opakowany interfejs API nie jest wystarczająco zamrożony i ciągle się zmienia.
źródło