<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 (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. 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 <i>GetWaterMeasurment</i>() 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ą <i>PrintCurrentReportToFile</i>() 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 </i>byłaby przypuszczalnie klasą czysto abstrakcyjną po której dziedziczyłyby klasy takie jak np. <i>WaterReportsFileRepo </i>czy <i>WaterReportsSqlRepo </i>itd. Mogłyby one być wstrzykiwane przez konstruktor do <i>WaterReportSaver</i>, a nad wszystkim czuwałby <i>WaterSystemClient </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>