W czasie gdy zajmowałem się swoim pierwszym projektem równolegle pochłaniałem kolejne strony książki Czysty Kod od Roberta Martina. Zawiera ona całą masę praktycznych wskazówek jak pisać oprogramowanie – częścią z nich się dzisiaj z wami podzielę, za przykład biorąc kawałki mojego, dopiero co ukończonego programu.
Ogólnie rzecz biorąc esencja płynąca z lektury Czystego Kodu wygląda następująco:
- Jeśli twój program działa wolno – nie szkodzi, zoptymalizuje się.
- Jeśli twój program zawiera bugi – nie szkodzi, przetestuje się go i znajdzie.
- Jeśli kod twojego programu jest nieczytelny – wyrzuć to gówno i napisz od początku!
To co z początku wydaje się wyjątkowo nieintuicyjne, jest prawdziwe:
Ważniejsze od tego aby twoja aplikacja działała, jest to aby inni programiści mogli zrozumieć jak działa.
Z dokładnie takim podejściem siadałem do mojego pierwszego samodzielnego projektu. Nie chodziło mi, aby gra wyglądała cudownie, w głębokim poważaniu miałem też zastosowanie nowoczesnych frameworków. Najważniejsze dla mnie było to, aby kod był schludny, czytelny i w dobrym stylu. Tutaj warto wspomnieć o czymś takim jak „zasada najmniejszego zaskoczenia” – czyli po prostu aby wszystko było tam gdzie się tego spodziewamy, że będzie. W uczynieniu kodu właśnie takim pomogły mi popularne zasady programowania: KISS, YAGNI, DRY i SOLID.
KISS
Keep It Simple Stupid
Moja ulubiona zasada na początek. KISS mówi nam aby nie „przeinżynierzyć” (ang. overengineering) rozwiązania danego problemu. Czasami programiści chcą zamanifestować swoje umiejętności i gwiazdorzą tworząc długaśne metody, korzystając z mało znanych bibliotek. A przecież chodzi o jak najprostsze i jak najszybsze rozwiązanie zadania.
Na stworzenie gry Clean Them All (CTA) wystarczyło mi dwa tygodnie pracy. To w sumie nie tak dużo, ale poszedłem po linii najmniejszego oporu – skorzystałem ze starej ale wciąż użytecznej biblioteki Swing do warstwy wizualnej. Mogłem też zrobić pełnoprawny front z użyciem JavaScriptu, HTML i pewnie jeszcze Reacta, ale zanim bym się tego nauczył minąłby co najmniej miesiąc. A najgorsze, że byłaby to wiedza chwilowo bezużyteczna, bo szukając pracy w backendzie nikt by mnie nie pytał o front.
YAGNI
You Ain’t Gonna Need It
„Nie będziesz tego potrzebować”. Kolejna fajna reguła łącząca się z KISS oraz często powtarzanym w Czystym Kodzie stwierdzeniem, że „przedwczesna optymalizacja jest źródłem wszelkiego zła”. W trakcie prac nad CTA chciałem dodać tam znacznie więcej elementów: animacje znikania kwadratów, obrazki zamiast kolorów czy górny panel z dodatkowymi przyciskami. I tak sobie próbowałem różnych opcji, a czas leciał. A ja marnowałem go na rzeczy, których poza tym projektem nie będę już nigdy potrzebował. A książka „Wzorce projektowe” leżąca tuż obok zbierała kurz. A kurs Springa na Udemy już 3 razy był na promocji i znowu wrócił do starej ceny.

Cytując myśl przewodnią filmu Interstellar: „Time is the resource”. Czas jest naszym zasobem i musimy nauczyć się nim mądrze dysponować. Róbmy to co mamy do zrobienia i nic więcej (chociaż naturalnie ciekawymi pomysłami warto się podzielić z drużyną).
DRY
Don’t Repeat Yourself
Świetna zasada mówiąca nam aby w kodzie unikać powtórzeń, z którą mam niestety najwięcej problemów. Nie wiem, może to skaza nauczyciela, która we mnie siedzi? W końcu powtarzanie jest esencją nauki. Chociaż gdzie tylko mogę staram się wyodrębniać elementy wspólne do pojedynczych klas czy metod, to czasem idzie coś przegapić. Na przykład zamiast tworzenia 16 kopii (dla każdego użytego koloru, sic!) takiej metody:
public static List<Square> createYellowSquares(int amount) {
List<Square> yellowSquares = new ArrayList<>();
for (int i = 0; i < amount; i++) {
String name = "yellow" + i;
Square square = new Square(SquareColor.YELLOW, name);
yellowSquares.add(square);
}
return yellowSquares;
}
Mogłem dodać drugi parametr z kolorem i załatwić problem pojedynczą funkcją:
public static List<Square> createColoredSquares(int amount, SquareColor color) {
List<Square> squares = new ArrayList<>();
for (int i = 0; i < amount; i++) {
String name = color.getName + i;
Square square = new Square(color, name);
squares.add(square);
}
return squares;
}
Z drugiej strony zbytnie odchudzanie klas może przynosić czasem problemy z czytelnością dlatego z całkowitą premedytacją zamiast napisać:
int[][] array = new int[19][25];
int[6][11] = 1;
int[6][13] = 1;
int[13][6] = 1;
int[13][18] = 1;
wolałem wrzucić tutaj pełną tabelę gdzie dobrze widać co i jak:

SOLID
W odróżnieniu od powyższych, SOLID to nie jedna ale aż 5 reguł, po jednej dla każdej litery:
S
Single responsibility
Zasada pojedynczej odpowiedzialności – jedna klasa powinna mieć tylko jedną funkcjonalność. Kierując się tym credo udało mi się stworzyć aplikację na którą składają się 24 klasy (nie licząc klas testujących). Dla przykładu pojedynczy kwadrat ma aż dwie klasy – jedna w modelu (która przechowuje jego nazwę, kolor oraz położenie), a druga w widoku – odpowiedzialna za narysowanie go i przechwycenie klików.
O
Open/Closed
“Otwarty na rozbudowę, zamknięty na modyfikacje”. Z początku brzmi jak sprzeczność, ale nic bardziej mylnego. Klasa raz stworzona powinna być zostawiona w spokoju, bowiem nieopatrzna zmiana z pozoru błahego drobiazgu może zdestabilizować cały program. Nie znaczy to jednak, że nie można do aplikacji dodawać nowych wodotrysków. Możemy, ale należy w tym celu korzystać z dziedziczenia i/albo interfejsów. Ale, by to było możliwe, pierwotny projekt musi zakładać taką rozbudowę i od początku ją umożliwiać poprzez np. stworzenie odpowiednich klas abstrakcyjnych. Jeśli chodzi o CTA to nie kierowałem się zbytnio zasadą O/C, gdyż projekt nie przewiduje rozbudowy (a co najwyżej kompletną przebudowę z użyciem frameworków i porządnego frontu) aczkolwiek użyty przeze mnie wzorzec strategii dobrze wpisuje się w w/w regułę – mogę dodawać nowe klasy z różnymi poziomami trudności bez modyfikacji tych już istniejących.


L
Liskov Substitution Principle
IMO najtrudniejsza zasada z wszystkich tu przedstawionych. Polega to na tym, że klasa dziedziczona musi być zamienna we WSZYSTKICH przypadkach z klasami po niej dziedziczącymi. Jeśli mamy klasę Animal.. wróć! Nigdy nie lubiłem tych wszechobecnych przykładów ze zwierzakami, zamiast tego weźmy klasy postaci z gier fantasy RPG.
Wyobraźcie sobie, że macie czarodzieja – niska siła, wysoka inteligencja, płaszczyk, kijaszek, szpiczasty kapelutek – te sprawy. Dopiero zaczyna przygodę, więc posiada on tylko dwa czary – kula ognia i lodowy pocisk (to wasz interfejs pod nazwą Spellbook). Na levelu dziesiątym macie możliwość specjalizacji – idziecie albo w magię ognia albo wody. Dostajecie podklasę DZIEDZICZĄCĄ po klasie Czarodzieja. Waszym bazowym interfejsem jest wciąż Spellbook, czyli nie może być tak, że mag ognia nagle zapomina jak się strzela lodowym pociskiem. Za to jak najbardziej możemy użyć bazowej kuli ognia, ale np. zwiększyć jej obrażenia dwukrotnie.
Zatem, bez względu na to czy jesteście zwykłym czarodziejem, czy też bardziej prestiżowym magiem ognia, wciąż macie te same zaklęcia (choć zmodyfikowane). Teraz, kiedy w karczmie formuje się nowa drużyna i szukają czarodzieja do polowania na żywiołaki ognia (magik musi znać magię lodu!) możecie się tam zgłosić nawet będąc magiem ognia. W końcu wciąż jesteście czarodziejem. Zasada Liskov jest zachowana.
I
Interface Segregation
O ile klasa dziedziczona mówi nam przede wszystkim CZYM JEST klasa po niej dziedzicząca, o tyle implementowany interfejs stawia na relację CO MA. Co ma klasa implementująca Runnable? Ma metodę run() do utworzenia nowego wątku i start() do jego rozpoczęcia. Co ma klasa implementująca Serializable? Nie ma żadnych nowych metod, ale ma przyzwolenie od JVM na spakowanie swojego obiektu do postaci pliku. Jak dobrze przyjrzymy się standardowym interfejsom to zobaczymy, że mają po 0-2 metod i tak właśnie należy to robić w naszych projektach. NIE DAWAJMY KLASOM METOD Z KTÓRYCH NIE KORZYSTAJĄ!
W powyższym przykładzie z czarodziejami nic nie stoi na przeszkodzie aby mag ognia implementował drugi interfejs: FireSpells z nowymi czarami ognia. Nie powinniśmy za to dać ich już zwykłemu czarodziejowi z warunkami aby z nich nie korzystał np. przed uzyskaniem poziomu dziesiątego.
D
Dependency Inversion
Zasada odwrócenia zależności. Nie zliczę ile razy R. Martin w Czystym Kodzie użył sformułowania „poziom abstrakcji”, ale czasem było to nawet po kilka razy w jednym zdaniu. Musi to być coś ważnego – pomyślałem i sprawdziłem. Faktycznie.
Całe „nowożytne” projektowanie obiektowe obiera się na abstrakcji – klasach abstrakcyjnych i (w szczególności) interfejsach. Ma to służyć zmniejszaniu zależności pomiędzy klasami. Klasa, która jest parametrem w jakiejś metodzie uzależnia tę metodę od siebie. Dla przykładu:
public void addSquareToPocket(Square square){
if (noFreeSlots()) model.gameLost();
for (Map.Entry<PocketSlots, Square> slot : squaresInPocket.entrySet()) {
square.setPocketSlot(slot.getKey());
if (slot.getValue() == null) {
slot.setValue(square);
break;
}
}
removeSquareFromGameBoard(square);
}
Ta metoda przyjmuje tylko jeden prametr: obiekt klasy Square. Taki kwadrat nie jest niczym szczególnym, ma tylko kolor, koordynaty na planszy i metodę onClick(), która mówi co się z nim dzieje po kliknięciu. Równie dobrze mógłby być kółkiem, trójkątem czy gwiazdką. Ale wtedy musiałbym dołożyć kolejną metodę dla każdego z tych kształtów. Głupota.
A wystarczy, że wyposażę te kwadraty, kółka, kwiatki czy cokolwiek wymyślę, w interfejs Shape z metodą onClick() i w/w zmiennymi, a następnie wszędzie tam gdzie w parametrze widać ’Square’ zamienię na ’Shape’. Sortujemy po interfejsie, a nie po klasie.
Ja tego nie zrobiłem, bo projekt jest krótki i dobrze wiem, że żadnych gwiazdek tam nie będzie. Ale jeśli siedzimy nad wieloletnim zadaniem nigdy nie mamy pewności gdzie nas poprowadzi i lepiej nie uzależniać metod od pojedynczych klas.
TDD
Wpis już jest zatrważająco długi, ale zanim go zamknę jeszcze kilka słów o Test Driven Development.
Napisałeś program, działa tak jak chcesz. Przetestowałeś go manualnie – podczas użytkowania i wszystko się zgadza. Po co ci jeszcze jakieś testy?
Jeśli projekt już jest skończony to testowanie jednostkowe może być tylko taką sztuką dla sztuki, co najwyżej aby nabrać trochę wprawy. Tak było w przypadku Clean Them All. Nie miałem tutaj zbytnio czego testować, bo aplikacja jest dosyć zamknięta, a jedyną formą interakcji są kliki i to tylko na obiekty Square. Wielokrotnie przeszedłem grę samemu aby wykryć wszelkie nieprawidłowości.
Mimo to douczyłem się w międzyczasie co nieco o testowaniu, a CTA stało się moim poligonem doświadczalnym. Dobrze przetestowana klasa nie boi się refaktoryzacji i ulepszeń. Być może n-ty raz przeglądając swój kod będziemy chcieli przerobić go nieco, np. aby lepiej zgrywał się z regułami SOLID. Choćby zamienić ten obiekt Square na interfejs Shape. Niby wszystko wtedy powinno działać jak wcześniej, ale czy mamy pewność? Porządnie zrobione testy nam tę pewność zagwarantują.
Podsumowując i wyciągając sedno z moich dzisiejszych wywodów, posłużę się jeszcze raz Czystym Kodem, a dokładniej ilustracją ze wstępniaka:
