Ostatnio przyjrzeliśmy się symptomom gnijącego designu oraz chorobie, która jest jego główną przyczyną – złemu zarządzaniu zależnościami. Wysnułem wniosek, że w leczeniu pomaga dążenie do designu, w którym mamy niski coupling i wysoką kohezję. Dlatego dzisiaj opowiem o couplingu i jego typach, jak je rozpoznawać i jakie to ma konsekwencje dla Twojego designu. Zapnij pasy i startujemy!
Z couplingiem wiąże się pewien problem: każdy z nas może zupełnie inaczej rozumieć to, czy coś jest mocno/słabo powiązane. Innymi słowy, pojęcie to po prostu jest wysoce abstrakcyjne. A jak sobie często radzimy z abstrakcjami? Szukamy analogii do życia codziennego. Można się o tym bardzo szybko przekonać, przeglądając wysoko oceniane odpowiedzi na StackOverflow. Roi się tam od porównań typu:
- „iPods, iPads are a good example of tight coupling” – silny coupling; podzespoły są tak mocno powiązane ze sobą, że zwykła wymiana baterii może być bardzo czasochłonna i kosztowna.
- „Car and spare parts analogy of coupling” – przykłady silnego i słabego couplingu; niektóre części łatwo wymienić, inne nie.
- „You and your wife” – silny coupling.
Analogie są fajne. Pozwalają nabrać kontekstu, załapać, o co mniej więcej chodzi. Na rozmowę rekrutacyjną jak znalazł. Ale czy faktycznie wszystko już jest jasne? Nie do końca…
Warto się cofnąć w czasie do roku 1974, kiedy to trzech panów: Larry Constantine, Glenford Myers oraz Wayne Stevens, opublikowało artykuł Structured design w trzynastym wydaniu „IBM Systems Journal”. W publikacji tej zostały dokładnie zdefiniowane i opisane koncepty zarówno couplingu, jak i kohezji. Za ich ojca uważa się pierwszego z wymienionej wyżej trójki.
Coupling to metryka opisująca, jak klasy są ze sobą powiązane – jaka jest siła powiązań między nimi. Jeśli dwie klasy są silnie powiązane, wtedy istnieje wysokie prawdopodobieństwo, że jeśli zmienimy coś w jednej, to będziemy musieli też zmienić coś w drugiej. Oczywiście to z kolei wpływa na koszty developmentu i utrzymania.
Tym samym powinno się dążyć do słabego couplingu, aby na przykład jedna osoba mogła przestudiować/debugować/utrzymywać daną klasę bez szczegółowej wiedzy o innych klasach w systemie. Im słabsze powiązania, tym klasy są bardziej niezależne. Ale jak określić, jak bardzo jedna klasa zależy od drugiej? Warto się przyjrzeć komunikacji między nimi. W jaki sposób to robią? Jak wygląda wiadomość, którą sobie wysyłają? Wiedząc to, będziemy mogli stwierdzić, jaki jest typ tego powiązania i jakie niesie to za sobą konsekwencje.
Oryginalny podział zaproponował Glenford Myers (drugi ze wspomnianej wcześniej trójki) w książce Reliable software through composite design. Wyróżnił on sześć typów couplingu i im właśnie poświęcimy teraz trochę uwagi. Przejdziemy od tych najmniej do najbardziej pożądanych.
Content coupling
Na początek content coupling, zwany również patologicznym. A to dlatego, że występuje wtedy, gdy jedna klasa sięga bezpośrednio do zawartości drugiej. Przez to jej obiekty manipulują wewnętrznym stanem obiektów innej klasy i są w stanie zmieniać ich zachowanie.
Jak to wygląda z poziomu wiadomości wymienianej między obiema klasami? Jest ona narzucana. Jesteśmy z nią tak nachalni, że wręcz wpływamy na wewnętrzny stan odbiorcy. W efekcie powoduje to, że odbiorca jakby nie może oprzeć się naszej wiadomości.
Przykłady:
- nierespektowanie prywatnych modyfikatorów dostępu,
- mechanizmy refleksji,
- monkey patching – zmiana zachowania jakiejś zewnętrznej biblioteki w trakcie wykonania programu.
Jak dużo wiemy o innej klasie? W tym przypadku praktycznie wszystko.
Listing 1. Content coupling – obiekt klasy Image zapewne wolałby osobiście zarządzać wartością pola description
Common coupling
Następnie mamy common coupling. Występuje on, gdy klasy opierają komunikację między sobą na jakimś globalnym, współdzielonym stanie, który może być przez nie zmieniany. Co za tym idzie – jeśli w czasie działania aplikacji stan się zmieni, to możemy nie wiedzieć, jak zareagują na tę zmianę obiekty innych klas, które z niego korzystają.
Przy tym typie powiązania, jeśli nie chcemy czegoś zepsuć, nadal musimy wiedzieć bardzo dużo o innych klasach. Musimy wiedzieć, jakie one są i jak ich obiekty zareagują na zmianę współdzielonego stanu.
External coupling
Rozważmy przypadek, kiedy klasy komunikują się ze sobą wiadomościami, których struktura przychodzi z zewnątrz. Mamy wtedy do czynienia z external couplingiem.
Na przykład: jeśli mamy jakąś klasę z zewnętrznej biblioteki, której używamy do komunikacji między dwoma klasami, to jesteśmy z nią powiązani. Nie mamy kontroli nad zmianami w zewnętrznej bibliotece i jeśli cokolwiek się w niej zmieni, może to mieć wpływ na nasze klasy.
Kolejnym przykładem może też być jakiś zewnętrzny format wymiany danych. Praktycznie każdy z naszych systemów integruje się z czymś zewnętrznym i zmuszeni jesteśmy wtedy komunikować się w narzucony sposób.
W obu wymienionych przykładach warto odcinać się od tych zależności najszybciej jak to możliwe i wewnątrz aplikacji posługiwać się strukturami, nad którymi mamy kontrolę. Dzięki temu ograniczymy liczbę miejsc, w których jesteśmy podatni na zmiany przychodzące z zewnątrz.
Control coupling
Wyobraź sobie sytuację, że piszesz kod i wywołujesz metodę jakiejś innej klasy, a metoda ta przyjmuje flagę, od której zależy wynik wywołania. Ty jako wywołujący musisz przeanalizować, w jaki sposób tę flagę ustawić, żeby otrzymać konkretny wynik. Jak widać, w tym przypadku klasa, której metodę wywołujesz, nie jest dla ciebie czarną skrzynką i musisz dość dużo o niej wiedzieć. Stawiasz się w tym momencie w pozycji koordynatora – mówisz, co ma zostać zrobione i czego oczekujesz w zamian. Jest to zachowanie charakterystyczne dla control couplingu. Jedna klasa przekazuje do drugiej elementy kontroli, czyli takie, które mają wpłynąć na wykonanie jej logiki oraz ostatecznie na zwracany wynik. Najczęściej elementami kontroli są właśnie różnej maści flagi czy typy wykorzystywane w instrukcji switch.
W programowaniu zorientowanym obiektowo to obiekt powinien decydować o tym, co ma zostać zrobione i zwrócone, na podstawie swojego własnego stanu i otrzymanych danych, a nie na podstawie tego, co ktoś sobie życzy.
Listing 2. Może warto byłoby rozbić tę metodę na dwie różne?
Stamp coupling i data coupling
Gdy osiągamy stan, w którym komunikacja nie zachodzi w żaden z wcześniej wspomnianych sposobów, czyli nie jest patologiczna (content coupling), nie odbywa się przez globalny stan (common coupling) ani przez zewnętrzny protokół (external coupling) oraz nie narzucamy, co ma zostać zwrócone (control coupling), to dochodzimy do dwóch typów couplingu, które na pierwszym miejscu stawiają samą wiadomość – to, jaką ona przybiera formę.
W pierwszym z nich (stamp coupling) klasy komunikują się za pomocą wewnętrznej struktury danych (najczęściej poprzez różnego rodzaju DTO). Odbiorca wiadomości ostatecznie może zdecydować się na wykorzystywanie tylko niektórych pól całej struktury. Reszty może w ogóle nie potrzebować.
Często w systemie możemy spotkać się z klasami, których nazwa kończy się angielskim słowem „details” albo „data”. Brzmi znajomo? Nie? No to przykład: mamy klasę EmployeeData. Jak już się pewnie domyślasz, pod tą przewrotną nazwą może kryć się wszystko – sky is the limit. Mamy też metodę, która otrzymuje obiekt takiej klasy, a następnie zwraca adres tytułowego klienta. Wykorzystuje ona jednak z całej tej struktury tylko jedno pole – jego identyfikator. Można by się teraz zastanowić, jakie powiązanie tutaj występuje. Przecież nadawca jedynie tworzy jakiś obiekt z danymi i przesyła go odbiorcy. Mamy tu do czynienia z couplingiem do struktury samej w sobie. Zmiana może nastąpić w polach struktury, które w konkretnym kontekście nie są wykorzystywane.
Ten typ couplingu może też prowadzić do powstawania sztucznych struktur, które będą trzymały w sobie dane niezwiązane ze sobą. Dochodzimy wtedy do momentu, kiedy widząc taki „worek”, stwierdzamy, że dorzucenie kolejnych nie zaszkodzi.
Zastanówmy się, jak możemy uniknąć powiązania do struktury, która może zawierać w sobie wiele danych – niekoniecznie potrzebnych. Odpowiedź jest prosta: zamiast używać struktury, przesyłajmy wyłącznie dane. W taki oto sposób dojdziemy do najluźniejszego typu couplingu, którym jest data coupling.
W praktyce wygląda to tak, że metoda przyjmuje listę argumentów opisujących wartości. Są one kluczowe dla działania metody. Innymi słowy – nie ma tam miejsca na przekazywanie zbędnych elementów. Na przykład: metoda zwracająca kod pocztowy klienta, jako parametr wywołania, zamiast wcześniej wspomnianej struktury EmployeeData otrzymuje wyłącznie to, czego potrzebuje, czyli tylko sam identyfikator klienta.
Opisaną sytuację oraz różnicę między tymi dwoma typami couplingu można zaobserwować na poniższym listingu.
Listing 3. Stamp coupling i data coupling
W tym momencie możemy usłyszeć głos naszego wewnętrznego malkontenta: „Eee tam, gdybyśmy wszędzie używali tylko wbudowanych typów języka zamiast struktur danych, popadlibyśmy w primitive obsession”. Bardzo dobrze, że masz takie wątpliwości, dlatego też liczę na Twoje pragmatyczne podejście do tematu. Nie zmienia to jednak faktu, że coupling jest silniejszy, gdy przekazujemy strukturę danych, niż gdy przekazujemy konkretne wartości. Unikamy wtedy powiązania do samej struktury wiadomości.
Podsumowanie typów
Jeśli chodzi o typy couplingu, to już prawie wszystko. Chciałbym podkreślić jedną rzecz – podział, który przedstawiłem (według Myersa), to podział oryginalny. Kładzie się w nim nacisk na komunikację i treść tej komunikacji, czyli samą wiadomość. Piszę to, ponieważ są jeszcze typy, które wynikają stricte z obiektowego paradygmatu programowania. I to właśnie te typy są nam chyba najbardziej znane. Chodzi tu o to, jak fizycznie zostali ze sobą połączeni nadawca i odbiorca. Czy nadawca „stworzył” odbiorcę, czy też nadawca operuje na jego interfejsie, czy w końcu nadawca rzuca swoją wiadomość w świat, nie wiedząc, kto na nią zareaguje.
Typy couplingu według podziału Myersa zostały zdefiniowane, gdy wiodącym paradygmatem programowania był paradygmat proceduralny. Później, kiedy liderem stało się OOP, wydaje się, że zapomniano o nich, zwracając bardziej uwagę na nowe typy charakterystyczne dla programowania zorientowanego obiektowo. Mimo to oryginalny podział jest nadal bardzo aktualny. Skąd wniosek, że o nich zapomniano? Gdy popatrzymy na kawałki kodu w internecie, które znajdujemy pod hasłem „przykłady słabego/mocnego couplingu”, to w przeważającej liczbie przypadków widzimy kod, w którym rozważa się to wyłącznie na poziomie interfejs (słaby coupling) kontra konkretna implementacja (mocny coupling). Czyli na poziomie, który jest charakterystyczny właśnie dla OOP. Ale to temat na zupełnie inną dyskusję.
Decoupling
Mówiąc o couplingu, warto też wspomnieć kilka słów o decouplingu. Ma on dość prostą definicję: jest to jakakolwiek metoda lub technika, dzięki której moduły/klasy osiągną większą autonomię.
Każdy z typów couplingu sugeruje jakieś formy decouplingu. Ogólną zasadą może być po prostu faworyzowanie typów ze słabszym couplingiem. Na przykład: jeśli zidentyfikowaliśmy control coupling, bo mamy metodę, w której są dwa „przebiegi” i rezultat wywołania jest inny dla każdego z nich, to może udałoby się nam rozbić tę metodę na dwie mniejsze. Dzięki temu może uzyskalibyśmy nawet data coupling. Innym przykładem może być metoda, która przyjmuje jako parametr strukturę danych i wykorzystuje tylko jej małą część – może udałoby się ją zmienić tak, by przyjmowała tylko te dane, których potrzebuje, w postaci krótkiej listy argumentów.
Kolejną techniką może być projektowanie klasy w taki sposób, jakby komunikacja z nią odbywała się przez kolejkę. W takim podejściu koncentrujemy się na tym, co konkretna klasa ma robić i czego faktycznie potrzebuje. Odcinamy się tym samym od czasu – moment wykonania przestaje być tak istotny.
Dobrą praktyką jest też posługiwanie się w komunikacji między klasami strukturami – czy też wartościami charakterystycznymi dla konkretnego kontekstu. Innymi słowy, chodzi tu o to, żeby na przykład klasy w ramach swoich paczek komunikowały się między sobą „lokalnymi” strukturami danych, a niekoniecznie strukturami, które są dostępne globalnie.
Na koniec chciałbym przekazać jeszcze jedną bardzo ważną uwagę: couplingu nie powinno się rozpatrywać w kategorii tylko dobry lub tylko zły. Jeśli chodzi o design całej aplikacji czy systemu, powinniśmy dążyć do luźnego couplingu. Znajdą się jednak w nim pewne miejsca, gdzie będziemy potrzebować silniejszego couplingu, i próba jego rozluźniania nie będzie miała najmniejszego sensu. Musimy brać pod uwagę to, że coupling ma kilka rodzajów i każdy z nich jest inny. Musimy umieć je rozpoznawać i być świadomi konsekwencji swoich wyborów.
To tyle, jeśli chodzi o coupling. W kolejnym wpisie wracam z tematem kohezji. Porozciągamy się trochę!