Jako programiści, w trakcie naszej kariery, napotykamy mnóstwo zasad związanych z programowaniem. Niektóre zasady podpowiadają, czy warto implementować daną funkcjonalność. Inne wskazują na rozmieszczenie kodu w klasach. Istnieją też zasady mówiące o hierarchii klas czy o użyciu abstrakcji. Po głębszym zastanowieniu okazuje się jednak, że w programowaniu obiektowym prawie wszystko sprowadza się do trzech koncepcji. Są to hermetyzacja, kohezja (ang. cohesion) i coupling (pozwolę sobie nie tłumaczyć). Ten wpis poświęcę dwóm ostatnim. Warto znać te koncepty, ponieważ są one jednymi z fundamentalnych metryk służących do mierzenia jakości oprogramowania.
Kohezja
Kohezja odnosi się do stopnia, w jakim elementy są ze sobą powiązane. Jak to zwykle bywa w programowaniu, możemy to aplikować na różnych poziomach, ale najłatwiej tłumaczyć na poziomie klasy. Zacznijmy od tego, że istnieje wiele rodzajów kohezji. Najczęściej dążymy do kohezji pod względem funkcjonalności i na niej się będę skupiał. Osiągamy najwyższą kohezję pod względem funkcjonalności, gdy wszystkie metody w klasie korzystają z wszystkich pól tej klasy. Choć osiągnięcie takiej kohezji wszędzie wydaje się utopijne, staramy się uzyskać jej jak najwyższy poziom. Innymi słowy, elementy zmieniające się wspólnie powinny znaleźć się w jednym „pudełku”.
W przypadku obiektów, które posiadają metody implementujące określoną logikę, dążymy do wysokiej kohezji.
W obiektach, które są jedynie strukturami danych, takimi jak DTO (Data Transfer Object), niska kohezja jest akceptowalna, ponieważ zwykle zawierają one tylko gettery i settery. Metryka kohezji sugeruje, że powinniśmy tworzyć mniejsze klasy i umieszczać w nich te pola, które często są używane razem w tych samych metodach. Zachęca nas to do dobrych praktyk programowania obiektowego.
Mierzenie kohezji
Zarówno kohezja, jak i coupling związane z kodem są mierzalne. Można robić to samemu za pomocą algorytmów, ale raczej to zbędna praca, skoro istnieją narzędzia umożliwiające wykonanie tej pracy za nas, np. CodeMR lub JDepend.
Weźmy za przykład klasy o wysokiej kohezji:
record RewardDateRange(LocalDate startDate, LocalDate endDate) {
RewardDateRange {
if (startDate.isBefore(LocalDate.now())) {
throw new IllegalArgumentException("Start date must be from the future");
}
if (endDate.isBefore(startDate)) {
throw new IllegalArgumentException("End date must be after start date");
}
}
boolean isAvailableAt(LocalDate date) {
return date.isAfter(this.startDate) && date.isBefore(this.endDate);
}
RewardDateRange extendDate(LocalDate newEndDate) {
if (newEndDate.isBefore(this.endDate)) {
throw new IllegalArgumentException("New end date must be after old end date");
}
return new RewardDateRange(startDate, newEndDate);
}
}
W tej klasie znajdują się tylko dwa pola, a wszystkie metody tej klasy z nich korzystają, co oznacza maksymalną kohezję. Klasa, którą tu widzisz, to Value Object – jest to wzorzec doskonale nadający się do zwiększenia kohezji w kodzie, a co za tym idzie, do poprawy jego jakości. Więcej o tym wzorcu można przeczytać tutaj: Value Object.
Oczywiście, jak wspomniałem, kohezja jest miarą, więc możemy obliczyć współczynnik kohezji dla tej klasy przy użyciu odpowiedniego algorytmu. Istnieje kilka różnych algorytmów do tego celu, ale wybiorę ten, którego używa narzędzie CodeMr, czyli LCOM3
.

Gdzie:
m
to liczba metod w klasie,a
to liczba pól w klasie,mA
to liczba metod, które mają dostęp do danego pola,sum(mA)
to suma wartościmA
dla wszystkich pól klasy.
Obliczenie kohezji dla klasy RewardDateRange
:
m
: konstruktor,isAvailableAt()
orazextendDate()
= 3.a
:startDate
iendDate
= 2mA
: w przypadku obustartDate
iendDate
, wszystkie 3 metody je wykorzystują.sum(mA)
:3 (metody korzystające z startDate) + 3 (metody korzystające z endDate)
= 6

Zobaczmy jak policzyło kohezje dla tej klasy narzędzie CodeMr:

Dla klasy RewardDateRange
mamy LCOM=0.0
, co oznacza klasę o maksymalnej kohezji, dążymy do tej wartości. Oczywiście, na pewno znalazłoby się kilku purystów, którzy powiedzieliby, że przecież record
ma również metody do pobierania pól, czyli startDate()
i endDate()
, i że należą one do interfejsu klasy, więc powinny być uwzględnione podczas mierzenia kohezji. Ja wyznaję zasadę, że mierzenie kohezji klasy z getterami trochę zniekształca tę metrykę (oczywiście settery trzeba uwzględniać zawsze!). Jeśli w projekcie korzystamy z narzędzi do analizy statycznej kodu, warto ustalić wytyczne specyficzne dla naszego projektu – to już kwestia do ustalenia w zespole. Co najważniejsze, warto pamiętać, że każda metryka ma za zadanie pomóc nam znaleźć problem i nie można jej ślepo ufać.
Ogólnie rzecz biorąc, jeśli zauważysz w klasie z logiką, że kilka metod głównie korzysta z tych samych pól, które rzadko lub wcale nie znajdują zastosowania w innych metodach, prawdopodobnie powinieneś wydzielić te pola do oddzielnego obiektu.
Inne rodzaje kohezji
W większości przypadków będziemy dzielić klasy ze względu na ich funkcjonalność, ale nie zawsze. Jak wspomniałem, istnieje kilka rodzajów kohezji. Czasami chcemy, aby jedna klasa wykonywała różne funkcjonalności, które są logicznie ze sobą połączone (kohezja logiczna). Przykładem mogą być operacje matematyczne, gdzie każda metoda jest niezależna i odpowiada za inną funkcjonalność, ale umieszczenie ich w jednej klasie jest jak najbardziej w porządku. W Javie mamy np. java.lang.Math
. Innym rodzajem kohezji jest kohezja czasowa, gdy elementy, które muszą być modyfikowane jednocześnie, są umieszczone w jednym miejscu itd.
Zatem istnieje wiele typów kohezji, ale nie ma sensu wymieniać ich wszystkich, ponieważ nie trzeba ich znać na pamięć. Najważniejsze jest zrozumienie kohezji pod względem funkcjonalności, bo to do niej najczęściej dążymy. A jeśli nie znamy innych rodzajów kohezji i złamiemy naszą zasadę dążenia do kohezji pod względem funkcjonalności, ale widzimy z tego korzyści i zgadzamy się z tym w zespole, to znaczy, że to jest w porządku i nie musimy nawet znać dokładnej nazwy tego, co zrobiliśmy. 🙂
Coupling
Coupling mierzy stopień powiązań między komponentami. Na przykład, jeśli jedna klasa zależy od wielu innych, to mówimy o wysokim couplingu, co wskazuje na silne powiązania i znajomość wewnętrznej implementacji między klasami. Oczywiście dążymy do niskiego couplingu, aby unikać sytuacji, w której zmiana w jednej klasie prowadzi do błędów w innych. Klasy muszą z sobą rozmawiać, żeby tworzyć system, ale najlepiej, aby robiły to za pomocą ustalonego interfejsu. Istnieje wiele rodzajów couplingu, nie będę ich tu wymieniać, bo to nie wniesie za dużej wartości. Na te chwile zostańmy przy tym, że jest coupling wejściowy, który określa, od ilu klas zależy dana klasa, oraz coupling wyjściowy, określający, ile klas zależy od konkretnej klasy.

AC (Afferent Couplings): Liczba klas z innych pakietów, które zależą od klas w analizowanym pakiecie.
EC (Efferent Couplings): Liczba klas w analizowanym pakiecie, które zależą od klas w innych pakietach.
Z metryk mogłoby się wydawać, że pakiet stanowiący serce aplikacji, czyli ..domain.core
, jest źle skrojony, ponieważ zależy od innych klas. W tym przypadku mamy wspólny moduł mavenowy, jest on w formie biblioteki (Shared Kernel). Ta biblioteka przechowuje między innymi Value Objects, które są współdzielone między mikroserwisami, stąd te zależności. Dlatego te metryki są tylko wsparciem do pokazania ewentualnych problemów, a każdy przypadek należy rozważyć osobno.
Narzędzie CodeMR potrafi prezentować dane w formie atrakcyjnych graficznych wykresów. Dzięki temu możemy identyfikować potencjalne problemy w naszej aplikacji, na przykład miejsca, gdzie logika nam cieknie.


Coupling:
Po zidentyfikowaniu problematycznych pakietów możemy zmieniać widoki. Przechodzić na poziom klas w tych pakietach, a nawet analizować metody. Dzięki temu identyfikujemy problemy i wyznaczamy lepsze granice komponentów. Po analizie może się okazać, że obecny podział jest właściwy na nasz aktualny stan wiedzy. Warto korzystać z narzędzi wspomagających identyfikację potencjalnych problemów ze spójnością komponentów. Dobrze skrojone granice nie tylko usprawniają rozwój, ale również ułatwiają późniejsze utrzymanie aplikacji.
Coupling niemierzalny
W ramach jednego modułu, metryki takie jak liczba współdzielonych zmiennych, metod czy interfejsów mogą służyć jako wskaźniki poziomu związania między komponentami. Jednak w architekturze mikroserwisów, gdzie serwisy są rozproszone i komunikują się głównie przez sieć, tradycyjne metryki mogą nie być adekwatne. W takich przypadkach często jeden mikroserwis musi posiadać wiedzę o drugim, aby wykonać jego żądanie lub zareagować na wydarzenia, które miały miejsce. Taki rodzaj couplingu nazywamy semantycznym.
Przykładowo, w aplikacji do zamawiania jedzenia (mam zaimplementowaną, na końcu wpisu link do GitHuba), system zamówień wysyła event orderCreated
, a system płatności nasłuchuje tego zdarzenia. Oznacza to, że mikroserwis płatności posiada pewną wiedzę o zamówieniach, mimo że nie jest to jego domena. Można to zrobić w drugą stronę: system zamówień wysyła komendę makePayment
, więc to system zamówień musi coś wiedzieć o płatnościach (wpis: różnica między command a event). Decyzja o kierunkach zależności są kluczowa dla naszej architektury systemu. Warto mieć takie relacje zdefiniowane, na przykład, za pomocą map kontekstowych z DDD. Jest to jednak temat na osobny wpis.
Złe rozdzielenie granic między mikroserwisami może być znacznie bardziej problematyczne niż nieoptymalne podział pakietów w obrębie pojedynczego mikroserwisu. Dlatego warto rozważyć użycie metryk couplingu, aby pomóc w podziale monolitu na niezależne moduły, które później można ekstrahować jako osobne mikroserwisy. Ostateczna decyzja w tej kwestii jednak zawsze będzie zależała od posiadanej wiedzy o domenie i kontekstu, w którym system funkcjonuje.
Podsumowanie
W uproszczeniu kohezja opisuje, jak mocno komponenty są połączone ze sobą wewnątrz, a coupling jak mocno od siebie zależą.

Dążmy do tego, by nasz system charakteryzował się wysoką kohezją i niskim couplingiem. Dobrze zaprojektowany system jest często opisywany jako „high cohesion” i „loose coupling”. Jednak są to ogólne zasady, a nasze decyzje zawsze zależą od konkretnego projektu. Więc nie możemy na ślepo kierować się tylko tymi metrykami. Przykładowo, wiem, że niski coupling w klasie jest pożądany. Jednak wolę, gdy moja klasa zależy od pięciu stabilnych komponentów, niż od trzech, które często się zmieniają. Analiza i statystyki mogą sygnalizować problem. Każdy przypadek jednak wymaga indywidualnej oceny. Wszystko zależy od konkretnego kontekstu.
Kohezja i coupling to koncepty sięgające lat 60., które od początku służyły jako wskazówki do tworzenia oprogramowania. Znalazły się one również w GRASP (General Responsibility Assignment Software Patterns), czyli w dziewięciu fundamentalnych zasadach projektowania obiektowego. Przez lata nałożyliśmy wiele abstrakcji na te koncepty i wprowadziliśmy wiele „nowych” zasad programowania, które swoją drogą często wprowadzają spory chaos (ale o tym będzie kolejny wpis!).
Zostaw swój e-mail, a raz na jakiś czas wyśle Ci info o nowych postach na blogu. Tylko wartościowe e-mail!