31-08-2022
Primitive Obsession jest jednym z zapaszków kodu (code smells).
Czym są zapaszki kodu?
Są to miejsca w kodzie, które powinny wywołać u Ciebie zapalenie się lampki, że dany kod może jednak nie jest najlepiej napisany. Code Smells są wskazówkami jakie miejsca powinny zostać zmienione. Chcesz się dowiedzieć więcej o Code Smells? Zapraszam do mojego wpisu „Code Smells„.
Dziś opowiem Ci o zapaszku Primitive Obsession (nazywany również „pierwotna obsesja” czy „opętanie typami prostymi”).
Zapaszek ten możesz zauważyć, gdy w kodzie używanych jest spora ilość typów pierwotnych/prymitywnych/podstawowych (pozwolę sobie używać zamiennie tych nazw 🙂 ). Co mam na myśli poprzez typy pierwotne? W PHP będzie to między innymi „int” / „string” / „float”. Są to podstawowe typy zmiennych języka. Co nie jest typem podstawowym? Przykładowo klasa (w uproszczeniu: klasa jest „szablonem” na podstawie którego tworzony jest obiekt. Inaczej: obiekt jest instancją klasy).
Czy zawsze jak widzisz, że używane są typy podstawowe to powinna Ci się zapalić czerwona lampka?
NIE!
Bez typów podstawowych raczej ciężko byłoby napisać cokolwiek 😉 Chodzi o sytuacje, gdy te zmienne typu prymitywnego używane są często, a można by użyć klasy. Bądź też w przypadkach, gdy logika kodu sterowana jest za pomocą takiej zmiennej pierwotnej. Wtedy należy spojrzeć na taki fragment kodu i przemyśleć czy na pewno tak to powinno wyglądać? Do oceny sytuacji należy podejść dość indywidualnie.
Czy jak używam w Java klasy Integer zamiast int to pozbywam się zapaszku?
NIE.
Pisząc o tym, aby używać klasy zamiast typu prostego nie miałam na myśli, aby używać klasy która nadal reprezentuje po prostu ten typ wartości. W tym przypadku, biznesowo użycie klasy Integer dużo nie zmieni dla Twojej aplikacji co użycie int. Co najwyżej wpłynie negatywnie na wydajność Twojej aplikacji przy dużej ilości takich obiektów zamiast typów prostych. Więc co miałam na myśli? Mam nadzieję, że przykłady które znajdziesz poniżej rozwieją Twoje wątpliwości o co chodzi 😉
Czy jeśli do metody jest przekazywana tylko jedna zmienna typu prostego to zapaszek nie występuje?
To zależy.
A teraz od czego zależy zobaczysz w przykładach 😉
Przykłady
Pieniądze
Wyobraźmy sobie klasę której zadaniem jest wyliczanie ceny wszystkich produktów w koszyku. Dla ułatwienia załóżmy, że nie bawimy się w żadne promocje, zniżki itp., tylko po prostu sumujemy cenę produktów, które zostały dodane na poniższym przykładzie metodą add().
Analizując poniższy kod możesz zauważyć, że metoda add otrzymuje cenę. Pomijając fakt, że przekazywanie pieniądza jako float nie jest najlepszym pomysłem ze względów zaokrąglania, to co widzisz w tym przykładzie, że mogłoby być zrobione inaczej?
class Basket { private float $totalPrice = 0; public function add(float $price): void { $this->totalPrice += $price; } public function getTotalPrice(): float { return $this->totalPrice; } //... }
Czy pieniądz to float?
NIE.
Pieniądz to ilość oraz waluta. W związku z czym może warto przedstawić to jako nowy obiekt? Spójrz na kod poniżej. Wykorzystana tu została biblioteka Money. Klasa Money otrzymuje w konstruktorze ilość pieniędzy oraz walutę [public function __construct(int|string $amount, Currency $currency)].
class Basket { private Money|null $totalPrice; // ... public function add(Money $money): void { if ($this->totalPrice === null) { $this->totalPrice = $money; return; } $this->totalPrice = $this->totalPrice->add($money); } public function getTotalPrice(): ?Money { return $this->totalPrice; } // ... }
Wyobraź sobie sytuację, że wszędzie w Twojej aplikacji e-commerce przekazujesz cenę jako float/int, bo powiedzmy, że firma działa tylko na jednym rynku i początkowo to założenie wydaje się dobre. Pewnego dnia jednak przychodzi do Ciebie Twój przełożony/przełożona i mówi Ci, że za miesiąc firma rusza na rynek niemiecki i klienci będą płacić w euro. Ile miejsc w kodzie / bazie będziesz mieć do zmiany? Podejrzewam, że całkiem sporo 😉
Jaki możesz mieć zysk z użycia obiektu zamiast przekazywania zmiennych typu prymitywnego w tym przypadku?
- zmniejszasz ilość przekazywanych zmiennych do metody (w przypadku jeśli przekazujesz walutę i ilość pieniędzy jako dwie zmienne add(float $price, string $currency) ), a mniejsza ilość zmiennych to większa czytelność i mniejsza szansa na pomyłkę,
- pieniądz składa się z waluty i ilości – więc tak na logikę ma to sens, aby opakować to w jedną klasę,
- zmniejszasz ryzyko, że zapomnisz gdzieś przekazać walutę – przekazujesz po prostu cały pieniądz.
NIP / PESEL / numer telefonu / e-mail
Innym przykładem takiej podmiany mogłoby być używanie obiektu PESEL, NIP, numer telefonu czy adres e-mail zamiast przekazywać wszędzie zmienną typu string. Wydawać by się mogło, że jaki niby z tego zysk? A taki, że walidację danych można dać do takiej klasy. Jeśli przykładowo za jakiś czas by zmieniła się zasada sprawdzania PESELu to zmienisz to w jednej klasie.
Ktoś mógłby zauważyć, że przecież ma napisaną jedną klasę „ValidatePESEL”, więc mój argument wyżej trochę dla tego przypadku nie ma sensu. Podejścia są różne. Jednakże wyobraź sobie teraz sytuację z NIP. Powiedzmy, że w bazie wolisz trzymać w postaci bez myślników, aby mniej miejsca zajmowały. Natomiast w warstwie prezentacji chcesz mieć z myślnikami. W takiej sytuacji w klasie takiej oprócz walidacji możesz mieć też metodę zwracającą format do prezentacji.
Waga
Wyobraź sobie, że masz system e-commerce, który na podstawie wagi wybranych produktów dobiera koszt przesyłki. Poniżej 2 produkty wybrane przez użytkownika:
- gwoździe – 20g
- młotek – 0,8kg
W jaki sposób zrealizujesz kalkulator sumujący produkty do zakupu w sklepie?
Czym będzie waga produktów? Int? Float? Klasa Integer w Java?
Po pierwsze warstwa prezentacji, a warstwa w kodzie to dwie różne rzeczy. Czy prezentowane dane muszą tak samo być zapisane w bazie? Nie 🙂
Następnym wyzwaniem są jednostki. Zanim dodasz „20g” do „0,8kg” musisz uwzględnić jednostki. Jeśli przekazujesz w kodzie float i nie rozróżniasz jednostek to możesz bardzo niemiło się zaskoczyć (a w sumie bardziej Twój użytkownik!!), gdy Twoja aplikacja powie, że przesyłka będzie ważyć 20,8g , lub 20,8kg 🙂
W związku z powyższym tutaj również tak jak i przy przykładzie pieniądza czy NIP warto utworzyć obiekt i przekazywać go zamiast samego typu prostego. Bo tak jak w przypadku pieniądza przekazując tylko liczbę nie jesteś w stanie określić jednostki, a bez jednostki nie zsumujesz produktów (to znaczy możesz zsumować, ale z różnych historii słyszałam, że różnie potem bywało i firma nie była zadowolona 😉 ).
Sterowanie logiką
Masz w kodzie przekazywaną zmienną typu prostego do metody, która odpowiada za sterowanie logiką kodu (czyli mówiąc inaczej: jest kilka IFów na tą zmienną i w zależności od jej wartości jest uruchamiana inna logika)? To jest również symptom Primitive Obsession. O możliwych rozwiązaniach tego problemu napiszę osobny wpis, ale już teraz napomknę, że są rozwiązania: wzorzec projektowy Strategia / wzorzec Stan. 😉 Ale czy zawsze to jest problem? NIE. W momencie, gdy ta logika nie jest zbyt rozrośnięta, to może się okazać, że próbując rozwiązać ten „problem” niepotrzebnie skomplikujemy sobie kod. Dlatego warto tutaj podejść do tematu rozsądnie 🙂
Poniżej przykład właśnie tego zapaszku w sterowaniu logiką. Poniżej mamy klasę Validator która waliduje wartość w zależności od przekazanego typu. W momencie gdy typem jest NIP (TaxIdentificationNumber) to wykonuje jedną logikę, a gdy typem jest REGON to robi walidację na inny sposób.
class Validator { public function isValid(string $value, string $type): bool { switch ($type) { case 'TaxIdentificationNumber': // ... return $digits[9] === $checkSum; case 'REGON': // ... return $digits[8] == $checksum; default: throw new \InvalidArgumentException("Invalid type"); } }
Podsumowanie
- Programując zastanów się czy dany typ prosty nie powinien być przedstawiony w kodzie jako jakiś obiekt. Przykładowo: pieniądz, data, waga, NIP, numer telefonu itp.
- Masz IFy/SWITCHe które opierają się o zmienną? Logika w dla tych przypadków jest rozrośnięta? Przemyśl użycie wzorca strategii / stanu.
- Typy proste same w sobie nie są problemem. Nie zawsze gdy masz typ prosty oznacza, że musisz od razu przejść do tworzenia nowej klasy czy używać wzorca strategii/stanu. Wszystko zależy od konkretnej sytuacji.
Mam nadzieję, że ten wpis był dla Ciebie wartościowy i chociaż trochę pozwolił Ci zrozumieć na czym polega Primitive Obsession 😉
Coś wymaga doprecyzowania? Napisz w komentarzu.
Źródła
- książka: Refaktoryzacja do wzorców projektowych, Joshua Kerievsky
- książka: Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Martin Fowler we współpracy z Kentem Beckiem
- www: Primitive Obsession