Trudno dyskutowalna atrakcyjność stanu bycia programistą ma kilka przyczyn. Jedną z nich jest możliwość (a wręcz konieczność) ciągłego rozwoju. Świat pędzi coraz szybciej. Jego wzrastającą prędkość mocno stymulują wynalazki z dziedziny IT. Zawody takie jak analityk, programista czy architekt w IT odgrywają wiodącą rolę. Wchodząc w te profesje, od samego początku pędzimy IT-odrzutowcem, stopniowo, jak się uda, zyskując miano „seniora” (standardowo pięć lat doświadczenia). Zaraz po tym uważamy (błędnie, ale na szczęście nie zawsze), że to już właśnie ten moment. Ten, kiedy wiemy już na tyle dużo, że może już wystarczy, aby poradzić sobie w każdej sytuacji. Seniorzy „starsi” wiedzą jak jest naprawdę. Nie uczysz się, nie rozwijasz, cofasz się. Ci sami seniorzy, dbając o higienę warsztatu, czytają, słuchają, ćwiczą itd. Oczywiście na tym poziomie, to co aplikują swoim umysłom, musi być odpowiedniej jakości. To musi być „stuff” dla seniora, a nie jakieś tam juniorskie podstawy. Czy aby na pewno?!

Uważam, że nie koniecznie. Na dowód, chcę podzielić się swoim własnym doświadczeniem. Pokornie schylam kark i przyznaję, że całkiem niedawno, materiał skierowany do juniorów (kurs https://devupgrade.online ) był w stanie skłonić mnie do przemyślenia pewnych tematów. Aby to dokładnie pokazać, posłużę się przykładem.

Wyobraźmy sobie, że tworzymy system, aplikację, czy serwis zajmujący się obsługą sprzedaży. Szybko zauważamy, że w kilku miejscach występuje temat naliczania podatku VAT. VAT to w zasadzie stały zestaw: wartość netto, stawka i wartość VAT oraz wartość brutto. Algorytm obliczania tych wartości jest prosty, można go zawrzeć w jednej klasie:

Kod jest prosty, nie ma tu nic szczególnego zasługującego na uwagę. Łatwo to testować, klasa jest niezmienna (fajnie) no i działa. Ale czy to jest dobra i czysta architektura? Zastanówmy się nad takimi kwestiami. Klasa ma konstruktor który przyjmuje kierunek liczenia i wartość wejściową. Autor milcząco założył, że przy kierunku liczenia od netto, wartość przekazana też będzie wartością netto. Założenie jest ryzykowne z dwóch powodów.

Po pierwsze, jak to zweryfikować? Przecież wartością wejściową jest BigDecimal, a to może być dowolna wartość: netto, brutto, a nawet bonifikata czy marża, lub całkiem dowolna liczba. Istnieje niemałe ryzyko policzenia VAT’u od złej wartości – przez prostą pomyłkę, złe podstawienie. Choć wiadomo, testy powinny to wykryć. Ale czy trzeba czekać na testy?

Po drugie, jak obliczyć te wszystkie wartości, jeżeli liczymy od netto a ktoś chce nam podać wartość brutto i z niej rozpocząć obliczenia? Ten model tego nie umożliwia, ale wiadomo można dodać drugiego enuma … trochę if’ów w if’ach i da się.

Te dwa główne problemy to nie wszystko. Kolejnym problemem jest to, że wynikiem działania tej klasy są wartości BigDecimal. W zasadzie to dziwne. BigDecimal to dowolna liczba z którą można potem robić co się chce, np. dodać netto do brutto! Powstanie z tego jakaś wartość, ale czy ona ma sens? Wiemy, że nie ma. Więc dlaczego robimy kod który to umożliwia?!

Po odpowiedź i rozwiązanie zwróćmy się do OOP (Object Oriented Programming), czegoś na czym wyrośliśmy i jak się nam wydaje co dobrze znamy. W OOP jednym z fundamentów jest klasa, klasa to meta-pojęcie dla obiektów. Rola projektanta OOP to identyfikacja bytów/obiektów i ich klas. Jeżeli mamy w wielu miejscach naszego systemu liczbę, która pełni rolę wartości netto np. są to takie obiekty „100,00” albo „0,23”, to jaka jest ich klasa? BigDecimal czy NetValue? Idąc tym tropem dalej, mamy także wiele obiektów które pełnią rolę stawki VAT np. „0,23” czy „0,03”, jaką tu zastosować klasę? BigDecimal czy VatRate? Odpowiedź „BigDecimal” jest tyle powszechna, co problematyczna. Tak odpowiedział sobie autor klasy VatCalculator wpędzając siebie i innych w opisane wyżej problemy. Dlaczego chcąc być w zgodzie z OOP, nie należy wybierać BigDecimal? Bo sami widzimy, że dziwne jest nadawanie tej samej klasy obiektom klas różnych. Obiekty te pełnią przecież różne role w systemie!

Zobaczmy co by się stało gdyby uznać, że nasze klasy to NetValue, VatValue, GrossValue i VatRate, np:

NetValue to klasa reprezentująca wartość netto. Jej instancje można utworzyć na kilka sposobów. Z dowolnej wartości numerycznej lub znakowej, z innej wartości netto (jako kopię), albo z wartości brutto i wartości vat. To wszystko bardzo logiczne. Ma ona jeszcze metodę „add”, która dodaje do niej wartość vat (bo to ma sens) i zwraca wartość brutto (bo netto plus vat daje brutto), super. Nie da się wykonać innych działań. Wartość brutto jest podobną klasą:

Jak widać jej instancje można tworzyć z wartości netto i wartości vat (tego można się było spodziewać). Ma także metodę odejmującą od niej wartość vat (bo tylko to ma sens odejmować) co w wyniku daje wartość netto (bo brutto minus vat to netto). Znowu nie da się wykonać innych działań. Dobrze. Teraz ostatni aktor z tej trójki:

W tym przypadku instancje wartości vat mogą powstać zarówno z wartości netto jak i z wartości brutto. Nie ma tu miejsca na pomyłkę. Kompilator wykryje każda próbę naruszenia typu. Można postawić pytanie, czy wartość vat nie powinna pamiętać od czego został naliczona? Wydaje mi się, że nie.

Podsumujmy, mamy cztery klasy. Każda z nich reprezentuje dokładnie i precyzyjnie tożsamość i interfejs swoich instancji. Nie ma ryzyka powstania dziwolągów w rodzaju stawka vat plus wartość brutto, tak się nie da i to jest super. Nasze klasy jakoś tak przy okazji potrafią „liczyć się” nawzajem i to tylko sensownie, tak jak trzeba. Gdzie zatem jest nasz kalkulator? Przestał być potrzebny. Co dalej? Mamy w pełni funkcjonalne klasy składowe. Brakuje nam zestawu tych wartości, który pilnował by kierunku liczenia i dopasowania tych wartości do siebie. Skoro tak, to zacznijmy od klasy która co prawda Enum’a z kierunkiem nie ma, ale coś już potrafi:

Kwoty vat’u (na razie abstrakcyjne) ukryły swój konstruktor, dając w zamian dwie statyczne metody fabrykujące w których pomyłki być nie może. dostajemy jedną z dwóch implementacji, od netto lub od brutto. Zwracane na zewnątrz wartości są silnie typowane, nie ma tu miejsca na pomyłki. Klasa, jak poprzednie, jest niezmienna, lubimy tak. Elegancka delegacja szczegółów implementacji do klas potomnych, pozwala nam na niezajmowanie się zbędnymi szczegółami. Na marginesie to bardzo proste użycie dziedziczenia. Zobaczmy jeszcze dwie ostatnie implementacje:

 

To tyle. Pozostaje podsumować. Jest pięknie, ambitny i zgodny z OOP model. Ale czy na pewno?! Nie macie wrażenia, że z jednej prostej (choć „słabej”) klasy zrobiliśmy … cały kombinat? Czy to się opłaca? Czy to ma sens?

Wg mnie, to zależy. Jeżeli kwestia naliczania podatku VAT jest dla waszego systemu ważna, jest w nim powszechna, jest to ta domena, która należy do kluczowych, to tak, warto. Jeżeli natomiast gdzieś, raz na peryferiach trzeba policzyć trzy liczby … wtedy może nie. Czyli jest jak zawsze. Tak jak zasady OOP tak i rozsądek w ich stosowaniu nie powinien nas nigdy opuszczać. Wniosek też jest taki, zawsze warto się uczyć.