O zasadach SOLID to słyszał pewnie każdy programista. A znasz zasady STUPID? I czy jesteś w stanie określić, do których należy twój aktualny projekt? Jaki jest twój kod - SOLID czy STUPID? A może po trochu każdego?
Zanim przejdę do STUPID, to warto przypomnieć SOLID. Jeśli masz to w małym palcu, możesz śmiało przejść do następnego akapitu. Skąd skrót SOLID? Od 5 zasad
Klasa/funkcja powinna mieć tylko jedną odpowiedzialność. Inaczej powinien być tylko jeden powód do modyfikacji klasy. Najlepiej zobaczyć na przykładzie
function formatTags(){
const tags = fetchTags();
return tags.map(tags=>tags.name)
}
Ta funkcja, mimo że mała już łamie zasadę SRP. W funkcji robimy dwie rzeczy.
I teraz mamy dwa powody do zmiany tej funkcji:
Dużo lepiej jest to podzielić na dwie funkcje i do funkcji formatTags
przekazywać tablicę tagów
function formatTags(tags){
return tags.map(tags=>tags.name)
}
Zasada otwarte/zamknięte, czyli kod powinien być otwarty na rozbudowę, ale zamknięty na modyfikacje. Brzmi tajemniczo, ale już tłumaczę, o co chodzi. Załóżmy, że mamy osobny serwis, który jest odpowiedzialny za tworzenie PDF’a. I wykorzystujemy ten serwis do tego, by konwertować maile, strony, raporty do PDF’a. Nie będziemy tego implementować bezpośrednio w tym serwisie, ponieważ ilość if’ów byłaby przerażająca. No i z każdym nowym wymaganiem będzie konieczna zmiana.
Dużo lepiej będzie wykorzystać jakiś wspólny interfejs. Każdy, kto będzie potrzebować tworzenia PDF’a wykorzysta serwis i rozbuduje go do swoich potrzeb. Czyli nasz kod będzie zamknięty na modyfikacje (nie modyfikujemy kodu naszego serwisu), ale możemy go rozbudować o nowe opcje.
Zasada podstawiania Liskov - każdą klasę bazową powinniśmy być w stanie zastąpić klasą pochodną. Szybki przykład
interface Fetch {}
interface FetchFromGraphQL {}
function fetchTags(fetch: Fetch){}
Mamy funkcję, która odpowiada za pobranie tagów. I przyjmuje ona obiekt klasy Fetch
. Mamy też klasę FetchFromGraphQL
, która dziedziczy po Fetch
. Zgodnie z zasadą Liskov powinniśmy być w stanie w dowolnym momencie wykorzystać obiekt klasy FetchFromGraphQL
, żeby pobrać tagi. W zasadzie Liskov chodzi o to, by w obiektach potomnych nie zmieniać sposobu działania klasy bazowej. Jeśli w klasie pochodnej nie musisz implementować dowolnej metody z klasy bazowej - to znaczy, że abstrakcja została źle stworzona.
Zasada segregowania interfejsów mówi o tym, że interfejsy powinny być odpowiednio zdefiniowane. Interfejsy powinny być małe, tak aby obiekt nie musiał nadmiarowo implementować niepotrzebnych elementów. Przy trzymaniu się tej zasady interfejsy będą małe i będziemy częściej przypisywać do klasy ich większą ilość. To też się łączy z zasadą Liskov. Jeśli nasze interfejsy będą odpowiednio małe, to implementując klasy pochodne, nie złamiemy zasady Liskov. Im większy interfejs tym szansa na to, że złamiemy zasadę Liskov jest większy.
Zasada odwrócenia zależności, czyli klasy/funkcje wysokopoziomowe nie powinny zależeć od niskopoziomowych. Brzmi skomplikowanie, ale szybki przykład pokaże zasadę działania. Najpierw przykład złamania zasady
function fetchTags(){
return fetch(...)
}
W tym przypadku funkcja fetchTags
zależy od implementacji wbudowanej w przeglądarkę funkcji fetch
. Zamiast tego lepiej stworzyć ogólny interfejs, który musi być spełniony, by móc pobrać tagi.
function fetchTags(fetch: Fetch){
return fetch(...)
}
Od razu mamy też kod, który da się przetestować, ponieważ nie wykorzystujemy mechanizmu wbudowanego w przeglądarkę i jesteśmy w stanie ją podmienić w testach.
Na tej samej zasadzie co zasady SOLID powstał skrót STUPID. Składają się na niego następujące zasady:
Warto omówić każdą z tych zasad.
Pewnie słyszałeś o wzorcach projektowych. Singleton jest natomiast określany jako anty-wzorzec. Ograniczamy w nim możliwość tworzenia obiektów danej klasy do dokładnie jednej instancji. No i mamy zawsze globalny dostęp do tego obiektu. Problemem tego wzorca jest łatwość w jego nadużywaniu. W wielu przypadkach będziemy mieć dostępne dużo lepsze rozwiązanie.
Coupling jest miarą powiązania poszczególnych elementów. Gdy dwa elementy są szczelnie (ściśle) ze sobą połączone, to zmiana w jednym z tych elementów wymaga zmiany w drugim. Singleton jest idealnym przykładem takiej sytuacji. Kiedy korzystamy z tego wzorca, to dowolna zmiana w kodzie Singletonu powoduje konieczność zmiany w każdym komponencie, który korzysta z tego elementu. Innym przykładem może być integracja z konkretną biblioteką zamiast z jakąś abstrakcją, która umożliwi zmianę biblioteki w przyszłości.
Najgorszy kod to ten, którego nie potrafimy przetestować. Dlaczego? Ponieważ do takiego kodu nie napiszemy testów, więc kod z każdą kolejną zmianą będzie coraz bardziej zapuszczony. Nawet w przyszłości próba zmiany tego będzie trudna. Bez testów każdy będzie się bał wprowadzania zmian i w końcu kod będzie można określić tylko w jeden sposób - tu mieszkają smoki. Dlatego też powstały takie techniki jak TDD, które zwracają uwagę na istotę testów.
premature optimization is the root of all evil (or at least most of it) in programming
Donald Knuth
Przedwczesna optymalizacja powoduje tylko problemy w kodzie:
✅ przestajemy rozwijać produkt i tkwimy w jednym miejscu,
✅ próbujemy rozwiązać problem, który w danym momencie może nie być najważniejszy,
✅ w większości przypadków użytkownik systemu wolałby nową funkcjonalność niż przyspieszenie o 0.5 sek,
✅ zwiększamy stopień skomplikowania kodu i narażamy się na błędy.
Chyba każdy pisał w pętli for zmienne typu let i=0
. I tak jak jeszcze w takich miejscach możemy na to przymknąć oko, to nie chcemy tego widzieć w reszcie kodu. Wyobraź sobie taką funkcję
function changeEmail(u,n){}
W momencie pisania tego kodu pewnie byś pamiętał, co znaczą te zmienne. A co będzie za dzień/tydzień/miesiąc? Czy będziesz w stanie powiedzieć, co znaczą te zmienne bez patrzenia na wywołanie? A popatrz na taką funkcję.
function changeEmail(user, newEmail){}
Tak samo nie ma co pisać skrótów, jakiś mało ogólnych nazw. Dużo lepiej mieć dłuższą nazwę zmiennej. Idealny kod powinien się czytać jak książkę bez zastanawiania się co autor (nawet jeśli to ty) miał na myśli. Twoja praca będzie dzięki temu efektywniejsza.
Powtórzony kod jest pierwszym krokiem do powstania bałaganu. Nagle musimy pamiętać o kilku miejscach do aktualizacji danych i uważać przy modyfikowaniu kodu. Dlatego warto wyciągnąć wspólny kod do osobnego miejsca. Ale nie można też przesadzać. Czasami powtórzenie kodu ma sens i nie można na siłę szukać wspólnej abstrakcji. Jeśli wspólna funkcja ma mieć milion argumentów i tonę if-ów to lepiej powtórzyć kod.