Code Smells: (Zbyt) Duże klasy

Posted by Przemysław Owsianik on 2021-10-03
<p>Problem ze zbyt dużymi klasami występuje powszechnie w kodzie zastanym. Rzadko kiedy klasa tuż po commicie jest ogromna w sensie liczby linii kodu. Często wskutek zmian robionych już później, w ‚utrzymaniu’, przybiera na wadze. Nie znaczy to, że nie można od początku napisać klasy źle bo wciąż zdarzają się programiści którzy całą logikę aplikacji zamykają w kodzie formatki. Dlaczego i po co? Znów na pierwszy plan (podobnie jak w przypadku zbyt długich metod) wychodzi nie respektowanie zasady pojedynczej odpowiedzialności. Duże klasy na 85% (strzelam ;)) mają zbyt rozległy zakres obowiązków.</p> <p>Wiele odpowiedzialności w klasie łatwo rozpoznać przyglądając się składowym&nbsp;(zwłaszcza publicznym). Spójrzmy na poniższy fragment interfejsu. Załóżmy, że implementacja klasy Tap ma 1000+ linijek:</p> <pre class="language-cpp" tabindex="0"><code class="language-cpp">include <string> #include <fstream> #include <sqlite3> class Tap { public: void TurnOnHotWater(); void TurnOffHotWater(); void TurnOnColdWater(); void TurnOffColdWater(); void ChangeWaterPressure(float newPressure); std::string GetWaterMeasurment(); void PrintCurrentReportToFile(std::ofstream&); void OpenDbConnection(const char *filename, sqlite3 **db ); void SaveCurrentReportToDb(); void CloseDbConnection(sqlite3 **db); private: (...) }</code></pre> <p>Jako pierwszą powinniśmy poddać ocenie spójność jaką reprezentują składowe publiczne klasy.&nbsp;Nawet bez, choćby względnego, pojęcia na temat kontekstu w jakim działa powyższa klasa można podzielić jej składowe na kilka grup. Spróbujmy to zrobić pamiętając, że Tap jest jak sama nazwa wskazuje abstrakcją kranu.</p> <p>Pierwsze cztery metody są moim zdaniem jak najbardziej poprawne. Faktycznie w kranie możemy zakręcić i odkręcić, ciepłą lub zimną wodę. Dalej mamy metodę, z której nazwy możemy wnioskować że pozwala zmienić ciśnienie wody – średnio mi ona pasuje, ale z drugiej strony ciśnienie wody lecącej z krany zależy częściowo od tego jak bardzo odkręcimy kurek, więc nie będę się czepiał.</p> <p>Dalej mamy&nbsp;<i>GetWaterMeasurment</i>() &nbsp;zwracającą coś w postaci łańcucha znaków. Nazwa mi nie wiele mówi, ale na pewno wiem, że kranu o żadne miary wody poprosić nie mogę. Podobnie jest z kolejną metodą&nbsp;<i>PrintCurrentReportToFile</i>()&nbsp;jestem przekonany, że kran to nie drukarka.</p> <p>Ostatnie trzy metody służą, wnioskuję po nazwach, do komunikacji „kranu” z bazą danych. Warte zauważenia jest to, że prezentują one różne poziomy abstrakcji tzn. posługujemy się wyższym poziomem: zapisujemy coś do bazy, by w pewnym momencie zejść do poziomu połączenia z db.</p> <p>Teraz, gdy już wiemy które metody są ze sobą spójne a które nie, możemy zabrać się do refaktoryzacji. Klasę Tap kastrujemy do takiego stopnia:</p> <pre class="language-cpp" tabindex="0"><code class="language-cpp">class Tap { public: void TurnOnHotWater(); void TurnOffHotWater(); void TurnOnColdWater(); void TurnOffColdWater(); void ChangeWaterPressure(float newPressure); (...) private: (...) }</code></pre> <p>Pozostałe metody publiczne przenosimy do poniższych klas:</p> <pre class="language-cpp" tabindex="0"><code class="language-cpp">class WaterReportSaver{ public: void SaveCurrentReport(); (...) private: WaterReportRepsitory *repository; (...) }</code></pre> <pre class="language-cpp" tabindex="0"><code class="language-cpp">class WaterReportsRepsitory{ public: void virtual OpenContext() = 0; void virtual CloseContext() = 0; (...) private: (...) }</code></pre> <p><i>WaterReportsRepository&nbsp;</i>byłaby przypuszczalnie klasą czysto abstrakcyjną po której dziedziczyłyby klasy takie jak np.&nbsp;<i>WaterReportsFileRepo&nbsp;</i>czy&nbsp;<i>WaterReportsSqlRepo&nbsp;</i>itd. Mogłyby one być wstrzykiwane przez konstruktor do&nbsp;<i>WaterReportSaver</i>, a nad wszystkim czuwałby&nbsp;<i>WaterSystemClient&nbsp;</i>:).</p> <p>Jak widać jeżeli chcemy (zwykle powinniśmy) podzielić dużą klasę na mniejsze, to skutkuje to pewnym wzrostem złożoności na poziomie projektu niosąc wiele mniejszych klas – nie wydaje mi się jednak by była to zbyt wysoka cena za łatwiejszą czytelność na poziomie klasy.</p>