Cohesion (spójność) — to metryka pokazująca, jak ściśle powiązane są elementy wewnątrz jednego modułu lub klasy. Według Wikipedia, wysoka spójność jest oznaką dobrze zaprojektowanego modułu, w którym wszystkie metody i pola pracują nad jednym zadaniem. Cohesion bezpośrednio wpływa na łatwość utrzymania kodu i jest przeciwstawiany coupling — powiązaniu między modułami.
Najważniejsze
Cohesion (spójność) — metryka, która ocenia, jak bardzo metody, pola i właściwości wewnątrz jednej klasy lub modułu są ze sobą logicznie powiązane. Wysokospójny moduł wykonuje jedno zadanie i zawiera tylko te elementy, które są niezbędne do jego realizacji. Niskospójny moduł próbuje robić kilka rzeczy jednocześnie — metody są słabo powiązane znaczeniowo.
W kontekście programowania obiektowego cohesion jest ściśle powiązany z Single Responsibility Principle (S). Jeśli klasa ma jedną wyraźną odpowiedzialność, jej cohesion jest z reguły wysoki. Jeśli klasa zajmuje się zarówno UI, jak i logiką biznesową i komunikacją sieciową — cohesion jest niski i taką klasę warto podzielić na kilka oddzielnych klas o węższej odpowiedzialności.
Rozumienie cohesion pomaga programiście podejmować decyzje o refaktoryzacji. Kiedy widzisz, że w klasie jest metoda, która nie korzysta z pól klasy, to sygnał niskiej spójności. Taka metoda albo jest zbędna w klasie, albo klasa jest źle zaprojektowana. Dążenie do wysokiej spójności — to ciągła praca nad poprawą architektury na każdym poziomie kodu.
W inżynierii oprogramowania wyróżnia się siedem poziomów cohesion, ułożonych od najgorszego do najlepszego. Zrozumienie tej skali pozwala obiektywnie ocenić jakość modułu i określić, w jakim kierunku podążać podczas refaktoryzacji. Im wyższy poziom, tym bardziej łatwy w utrzymaniu i zrozumiały będzie kod.
Przypadkowa (coincidental) — najgorszy poziom, gdy elementy w module są zgrupowane przypadkowo, bez żadnego logicznego powiązania. Przykład: klasa Utilities, w której zebrano metody formatowania daty, wysyłania emaila i obliczania rabatu. Takiej klasy nie da się zrozumieć bez przeczytania wszystkich metod, a zmiana jednej metody może zepsuć inne tylko dlatego, że znajdują się obok siebie.
Logiczna (logical) spójność — elementy wykonują logicznie powiązane, ale różne co do istoty zadania. Klasa z metodami parseJSON, parseXML i parseCSV jest logicznie powiązana tematem „parsowanie“, ale każda metoda wykonuje zasadniczo inną pracę. Problem: przy dodawaniu nowego formatu (YAML) klasa rośnie, a jej interfejs staje się rozdęty.
Czasowa (temporal) spójność — elementy są zgrupowane według czasu wykonania. Klasa AppInitializer, która konfiguruje bazę danych, ładuje konfigurację, inicjalizuje analitykę — wszystko to dzieje się przy uruchomieniu aplikacji, ale same zadania nie są ze sobą powiązane. Lepiej podzielić je na osobne Initializer dla każdego obszaru odpowiedzialności.
Proceduralna (procedural) spójność powstaje, gdy elementy są połączone sekwencją wykonania. Moduł „Przetwarzanie zamówienia” zawiera metody validateCart, processPayment, sendConfirmation — każda metoda jest wywoływana ściśle po poprzedniej. Jest to lepsze niż przypadkowa lub logiczna spójność, ale wciąż nie ideał: każdy krok może być wydzielony do osobnego modułu.
Komunikacyjna (communicational) spójność — elementy pracują z tymi samymi danymi. Klasa UserService z metodami getUser, updateUser, deleteUser jest połączona wspólną encją User. Jest to znacznie lepsze niż proceduralna spójność: klasa ma wyraźny obszar tematyczny. Większość klas Repository w projektach mobilnych ma komunikacyjną spójność.
Funkcjonalna (functional) spójność — najwyższy poziom, gdy każdy element modułu uczestniczy w wykonaniu jednego zadania. Klasa PasswordValidator z jedyną metodą validate, która sprawdza długość, obecność znaków i złożoność hasła — przykład funkcjonalnej spójności. Jeśli taka klasa się zmienia, to tylko dlatego, że zmieniły się reguły walidacji haseł.
Osiągnięcie funkcjonalnej spójności — główny cel refaktoryzacji architektonicznej. Każda klasa powinna mieć dokładnie jeden powód do zmiany. W programowaniu mobilnym funkcjonalna spójność jest osiągana przez wydzielenie osobnych Use Cases, niestandardowych View, formatujących i walidatorów. Każda taka klasa to kompletny blok budulcowy z wyraźnym obszarem odpowiedzialności.
Cohesion i coupling — dwie strony tej samej jakości. Im wyższy cohesion wewnątrz modułu, tym z reguły niższy coupling między modułami. Dobrze zaprojektowany system jednocześnie dąży do wysokiej spójności wewnątrz i słabego powiązania na zewnątrz. Ta zasada jest uznawana za fundamentalną w inżynierii oprogramowania od lat 70. XX wieku.
Relację cohesion-coupling można przedstawić jako równowagę. Jeśli programista poświęca cohesion, łącząc w jednej klasie kilka zadań, sąsiednie moduły otrzymują więcej zależności — muszą odwoływać się do tej przeciążonej klasy w różnych celach, co zwiększa coupling. I odwrotnie, podział na małe, wysokospójne klasy zmniejsza liczbę punktów interakcji między modułami.
W praktyce oznacza to: kiedy wydzielasz nową klasę ze spójnością funkcjonalną, jednocześnie uwalniasz inne moduły od konieczności znajomości szczegółów jej implementacji. Na przykład, wydzielając EncryptionManager do osobnej klasy ze spójnością funkcjonalną, dajesz innym modułom prosty interfejs encrypt/decrypt bez konieczności rozumienia szczegółów algorytmu szyfrowania.
// Niska spójność — klasa robi wszystko od razu
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// Wysoka spójność — każda klasa rozwiązuje jedno zadanie
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
Przykład pokazuje różnicę: UserManager ma spójność logiczną — wszystkie metody dotyczą użytkowników, ale każda robi zasadniczo inną pracę. Po refaktoryzacji każda klasa ma spójność funkcjonalną, a coupling maleje, ponieważ inne moduły zależą tylko od potrzebnej im klasy, a nie od całego UserManager.
LCOM (Lack of Cohesion of Methods) — najbardziej znana metryka do pomiaru spójności klasy. LCOM zlicza, ile par metod nie używa wspólnych pól. Wartość 0 oznacza idealną spójność (wszystkie metody pracują z tymi samymi polami), wysoka wartość — niską spójność. LCOM4 (ulepszona wersja) uwzględnia przechodnie powiązania przez inne metody.
W Android metryki spójności można uzyskać przez Detekt z regułą TooManyFunctions. Klasy z dziesiątkami metod używającymi różnych grup pól najprawdopodobniej mają niską spójność. W iOS SwiftLint ma reguły file_length i function_body_length — pośrednie wskaźniki: długie pliki i metody często sygnalizują niski cohesion.
Ręczny sposób oceny: zadaj pytanie „Czy ta klasa zmieni się z jednego powodu czy z kilku?” Jeśli możesz wymienić więcej niż jeden niezależny powód — klasa ma niską spójność. Drugi test: „Czy można podzielić tę klasę na dwie niezależne klasy?” Jeśli tak — zrób to. Regularne sprawdzanie cohesion podczas code review zapobiega powstawaniu klas God i zmniejsza dług techniczny.
Pierwszy krok — zastosuj Single Responsibility Principle. Każda klasa powinna mieć jedną wyraźną odpowiedzialność. Jeśli w klasie jest metoda, która nie należy do jej głównego zadania, wydziel ją do osobnej klasy. Technika Extract Class lub Extract Delegate w IDE automatyzuje ten proces. Po wydzieleniu sprawdź, czy oryginalna klasa stała się bardziej skupiona.
Drugi krok — użyj wzorca Facade do uproszczenia interfejsu. Jeśli klasa udostępnia 20 metod, z których klienci używają tylko 3-4, prawdopodobnie klasa ma niską spójność — oferuje zbyt wiele różnorodnej funkcjonalności. Pogrupuj metody według tematów i wydziel osobne klasy dla każdej grupy, a oryginalną klasę zamień w fasadę lub usuń.
Trzeci krok — zwróć uwagę na grupy pól. Jeśli klasa ma pola, które są używane tylko przez część metod — to wskaźnik niskiego cohesion. Podziel klasę według grup pól. Na przykład, jeśli klasa zawiera pola userRepository, networkClient i analyticsTracker, ale metody pierwszej grupy używają tylko userRepository, a drugiej — networkClient — to są to dwie różne klasy.
Czwarty krok — unikaj tworzenia klas „narzędziowych” z przypadkowymi metodami static. Każda metoda static znajdująca się w klasie Utils lub Helpers — kandydat do wydzielenia do specjalistycznej klasy. FormatUtils.dateToString lepiej przenieść do DateFormatter, a ValidationUtils.isValidEmail — do EmailValidator. To zwiększa cohesion każdej klasy i czyni kod samodokumentującym się.
Często zadawane pytania
Prawie zawsze. Spójność funkcjonalna czyni kod zrozumiałym i przewidywalnym. Jednak doprowadzenie do skrajności może prowadzić do nadmiernego dzielenia: gdy dla każdej operacji tworzona jest osobna klasa, a architektura staje się niepotrzebnie skomplikowana. Równowaga — kilka klas na funkcję, każda ze spójnością funkcjonalną.
Cohesion — metryka wewnętrznej spójności jednego modułu lub klasy. Modularity — zasada architektoniczna, w której aplikacja jest dzielona na fizyczne moduły. Wysoki cohesion jest celem przy projektowaniu zarówno pojedynczych klas, jak i całych modułów.
Detekt dla Android i Xcode Analyzer dla iOS podświetlają klasy z podejrzanie dużą liczbą metod lub pól. IntelliJ IDEA i AppCode mają wizualizację zależności — możesz zobaczyć graf powiązań i wykryć klasy z niską spójnością. SonarQube oblicza metryki LCOM automatycznie.
Tak. Interfejs z metodami connect, disconnect i isConnected ma wysoką spójność — wszystkie metody dotyczą zarządzania połączeniem. Interfejs z metodami connect, parseData i renderUI ma niską spójność. Zasada Interface Segregation (SOLID) wymaga tworzenia wyspecjalizowanych interfejsów o wysokiej spójności.
Zadaj trzy pytania: czy można opisać przeznaczenie klasy jednym zdaniem? Czy wszystkie metody wspierają to przeznaczenie? Czy w klasie są pola, które nie są używane przez część metod? Jeśli odpowiedź na którekolwiek pytanie jest negatywna — cohesion jest niski i klasę należy podzielić.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również