Listy starego diabła do młodego | Programistyczne pokusy: Primitive Obsession i wyjątki

zycienakodach.pl 3 lat temu

Ilustracja pochodzi ze strony: BelaArt.com

Z życia na kodach

Git commit, git push… exit building… Ah… jeszcze tylko release. I… nareszcie piątek, weekendu początek! - pomyślał nieświadomy swoich przewinień programista. Niestety, w zespole bardzo sobie ufali. Dlatego code review nie było częstą praktyką. A o tym, komu kłamliwy kod naszego bohatera najbardziej przypadł do gustu, dowiemy się już niebawem…

Kodeks postępowania dla programistów w przypadku zagrożenia :) Ilustracja pochodzi z artykułu: https://medium.com/@rafia.anwar95/git-for-beginners-513372d7aca7

TL; DR

Nagminne stosowanie typów prymitywnych (tzw. Primitive Obsession), wyjątków i wartości (a adekwatnie jej braku) NULL może być niezwykle dramatyczne w skutkach. Zamiast tego warto rozważyć użycie, pochodzącego z Domain-Driven Design, wzorca ValueObject i/lub Monad znanych z programowania funkcyjnego.

Wprowadzenie

Najpierw chwila o pochodzeniu listu, z którym zaraz się zapoznasz. Myślę, iż na próżno się trudzić i dociekać dokładnie kto jest autorem przedstawionej poniżej wiadomości. Z pewnością po jej przeczytaniu każdy będzie miał na ten temat jakąś własną teorię. Możemy z treści wywnioskować, iż to ktoś, kto pod swoją pieczą miał już wielu programistów, albo jak on to zwykł nazywać — pacjentów. Niestety — z pewnością nie zależy mu też na wytwarzaniu programów wysokiej jakości. Jego podopieczni popsuli już niejeden program, pisali niemożliwe do utrzymania testy, rzucili wiele wyjątków i testowali na produkcji. A to tylko niektóre z obrzydliwości, do jakich był zdolny ich nakłonić. Teraz Krętacz, przekazuje rady mniej doświadczonemu demonowi, w którego szpony wpadł dopiero co świeżo upieczony — junior developer. Oboje zrobią wszystko, żeby The Worst Practices i Design Anti-Patterns były chlebem powszednim pacjenta. Krętacz przedstawi też wiele wzorców nieprzyjacielskich programistów, z którymi zetknięcie należy ustrzec podopiecznego za wszelką ceną. Inaczej jego kod będzie czytelny, łatwy w rozwoju i nie spowoduje spustoszenia na świecie opanowanym przez technologie.

Ze sposobu pisania każdy może zrobić adekwatny dla siebie użytek. Jednakże osoby, które wchodzą na tego bloga w zupełnie innym celu niż znalezienia dobrych praktyk, poznania wzorców i otrzymania przestrogi przed złymi nawykami, niech lepiej od razu wpiszą w przeglądarce inny adres.

Mój drogi Piołunie

jestem wielce rad, z tego, co mi donosisz, o Twoim nowym pacjencie. Dostałeś pod swoje kusicielskie skrzydła programistę. Junior Developera, który jest dopiero na początku swojej zawodowej kariery. Dlatego jest duża szansa, iż jeszcze zrobimy z niego typowego klepacza kodu, który jedynie co umie napisać to CRUDowe przeglądarki do bazy danych.

Sam już miałem z niejednym takim do czynienia i nie chwaląc się — wielu z nich skończyło bardzo marnie. Do emerytury pracowali w wielkich światowych korporacjach, utrzymując systemy z klasami po kilka tysięcy linii kodu i bez żadnego testu. Mam nadzieję, iż spotka to i Twojego podopiecznego.

Mam nadzieję, iż w takim razie pozwolisz mi udzielić Ci wielu niezbędnych porad jak zabrać się za takiego osobnika. Gdy wcielisz moje rady w życie, nasze Ministerstwo Złych Praktyk Programistycznych i Anty-Wzorców będzie z Ciebie niezmiernie dumne. I kto wie, może następnym razem zabierzemy się za jakiegoś konferencyjnego programistę-celebrytę? Teraz jednak do rzeczy…

Jak mi donosisz Twój pacjent, przygodę z programowaniem, zaczynał z językami bez silnego typowania. Kiedy poznał takie wynalazki jak Java czy C# był wniebowzięty i dalej taki pozostaje. Sprawia to, iż będzie bardzo podatny na nasze podszepty. Nie zdaje sobie jeszcze sprawy, iż nie wystarczy znać technologię, czy składnie języka. Jak we adekwatny sposób go używać — to jest sztuka, która zajmuje długie lata.

Na szczęście, w tych językach jest też to, co lubimy najbardziej. A dokładnie brak wartości, czyli NULL. Uwielbiany przez nas, błyskotliwy pomysłodawca tego przekleństwa sam nazwał ją “błędem wartym milion dolarów”. Choć myślę, iż z tymi dolarami, to jednak grubo nie do szacował…

Poniższy kod Twojego pacjenta, który mi pokazałeś, też nie jest wolny od tych wspaniałych defektów.

class Orders{ Collection<Order> findOrdersBy(String username){ var customer = customers.findByUsername(username); return customer == null ? null : customer.getOrders(); } }

Postaraj się, żeby zostawiał jak najwięcej takich pułapek na osoby, które będą używać tego kodu. A wtedy… NullPointerException w najmniej odpowiednim momencie oczywiście gwarantowany!

Pomyśl o tych wszystkich wywołaniach nieświadomych programistów używających zaimplementowanej funkcji (prezentuję Ci kilka możliwości poniżej).

findOrdersBy("MateuszNowak").forEach(it -> System.out.println(it)) findOrdersBy("MateuszNowak").stream().map(...) findOrdersBy("MateuszNowak").get(0)

Z pewnością nikt z nich nie domyśli się, iż tutaj może pojawić się sławny NullPointerException. A spowoduje go choćby już „pierwsza kropeczka” w napisanym ciągu wywołań. Przecież nasz wielki nieprzyjaciel, Joshua Bloch w książce Effective Java przypominał, iż wynikiem metody, która zwraca kolekcję, nigdy nie powinien być NULL. W takim przypadku odpowiednia zawsze będzie pusta kolekcja! No, ale kto by to czytał…

Niestety Twój pacjent, w końcu zorientuje się, czym to grozi. A jest to nieuchronne — zdarzy się najpóźniej przy pierwszym NPE na produkcji. Jak widzisz, nie doprowadzimy w ten sposób do końca świata, ale spokojnie… Bardzo nam jest to na rękę, iż analizowany osobnik wybrał się na studia informatyczne. Nawet nie wiesz ilu naszych sprzymierzeńców tam mamy… Z ich drobną pomocą możesz rzucić go ze skrajności w skrajność.

W realizacji tego celu niezastąpieni są uczelniani prowadzący. Z powodzeniem odwalają za nas brudną robotę. Chodzącą oni między ławkami gdzie studenci „kodują” na kartce i krzyczą co chwile po skompilowaniu kodu interfejsem białkowym (swoimi oczami): „NullPointerException!“.

Dzięki temu teraz zasiejesz w nim niepewność. Będzie się czuł osaczony przez NULLe, tak iż będę mu się choćby śnić. Najlepiej było, kiedy nie był świadom problemów z NullPointerException, ale teraz, skoro już o nich wie… Nie pozwól mu o tym zapomnieć, postaraj się, żeby wszędzie widział możliwy czarny scenariusz. To zawsze jest dla nas wielki dylemat. Możemy kogoś przekonywać, iż NULL nie istnieje i totalnie go ignorować. Jednakże, jeżeli jest już tego świadom i nie da sobie wmówić inaczej, to obieramy właśnie tę drugą strategię. To jak z naszym istnieniem — największe pole do popisu mamy, kiedy ten młody programista choćby nie podejrzewa, iż jesteśmy :) Dzięki temu jego kod będzie naszprycowany czymś w rodzaju jak poniżej.

class Orders { Collection<Order> findOrdersBy(String username){ if(username == null){ return List.empty(); } var customer = customers.findByUsername(username); if(customer == null){ return List.empty(); } var orders = customer.getOrders(); if(orders == null){ return List.empty(); } return orders; } }

Podsuń mu jeszcze jakiś wątpliwej jakości artykuł o „defensive programming”. Dzięki temu jeszcze bardziej się utwierdzi, iż to, co robi, jest adekwatne.

Niestety, wcześniej jego kod tuszował trochę prawdy. Sygnatura (deklaracja) metody nie mówiła nic o możliwym nullu, chociaż go zwracała. Tym razem sygnatura wygląda na poprawną. Klient tej funkcji nie spodziewa się nulla, ale kolekcję i w każdym przypadku właśnie to dostanie. Na pierwszy rzut oka wygląda jak byśmy i tak przegrali. Jednakże… jeżeli będziesz wystarczająca przebiegły, to bez problemu popchniesz go w kierunku naszych The Worst Practices.

Okoliczności, w jakich znajduje się nasz Junior, są naprawdę, baaaardzo sprzyjające do dalszych naszych poczynań. Szczęście, iż w swojej pierwszej pracy jest otoczony przez Senior Developerów z 12-letnim doświadczeniem. Inni z naszego departamentu, już dawno odpowiednio pokierowali ich karierami. Wszyscy te “wyrocznie” siedzące razem z nim w pokoju tak naprawdę mają jeden rok doświadczenia, ale powtórzony 12 razy. Przez ten czas nie dbali za bardzo o swój rozwój, a w każdym kolejnym projekcie stosowali zawsze te same techniki, których nauczyli ich na wątpliwych jakości studiach. Niemała w tym była też moja zasługa.

Sprawiliśmy, iż przez całe swoje programistyczne życie zawodowe cały czas robili to samo. Dlatego — jeszcze raz powtórzę — w rzeczywistości nie mają 12 lat doświadczenia, ale jeden rok — przepracowany dwanaście razy. Już dawno zatrzymali się w rozwoju. Bardziej wartościowymi specjalistami zdają się już choćby studenci — szczególnie Ci z zapałem i świeżą wiedzą, dla których programowanie to też hobby.

Tak na marginesie: zrób wszystko, żeby już na studiach Twój pacjent brzydził się programowaniem. Jeśli będzie to traktował jako smutny, nudny sposób na zarobienie pieniędzy, to już praktycznie wygrałeś. Nie wróżę mu sukcesów w programowaniu, a praca z nim będzie katorgą dla osób wokoło.

Wróćmy jeszcze do tych Senior Developerów, którzy przez lata powtarzają te same błędy… Spraw, żeby Twój pacjent ich podziwiał. Niech myśli, iż bardziej doświadczeni, zawsze wiedzą lepiej. Nie dopuść do niego myśli „Think for yourself. Question authority”.

Jest jeszcze inna możliwość. jeżeli osobowość Twojego podopiecznego będzie buntownicza i zupełnie przeciwna, to możesz go poprowadzić w drugą stronę. Wtedy powinieneś zrobić wszystko, żeby powitać go w Klubie Kruggera-Duninga. Nie zepsuj tego! To wielka szansa.

Kotlin VS Java? 1 to NULL

Zasiej w nim, już od samego początku pewność, iż Java to najlepszy język, jaki mógł wybrać i nie skłaniaj do poszerzania horyzontów. Mam nadzieję, iż nigdy nie dowie się o językach jeszcze lepiej wspierających paradygmat funkcyjny, ani choćby o Kotlinie czy Scali (pozostając już na podwórku JVMa). Wtedy dzięki Null-Safety mógłby z łatwością wyeliminować niektóre swoje kłamstwa z kodu. Wspomniani wcześniej “Seniorzy” z jego pracy z pewnością słyszeli o tym, jak JetBrains promuje Kotlina, ale ich przyzwyczajenie do tego, iż “tak było zawsze” zwyciężyło nad chęcią poznania czegoś nowego. Spójrz na metodę poniżej. Jej sygnatura od razu mówi, iż zwrócona wartość może być nullem. Teraz niestety nikt o niej nie zapomni i programista na etapie samego pisania kodu będzie MUSIAŁ ją odpowiednio obsłużyć.

fun findBy(orderId: Int, username: String): Order?

Gdyby jednak stało się na tyle źle, iż jakiś nasz nieprzyjaciel wprowadził w firmie Kotlina — to i tak pozostało nadzieja. Nieustanne używanie operatora tzw. Elivs (inaczej Optional Chaining), czy innych przeciw nullowych rozwiązań z pewnością jest nużące. Nakłoń naszego Juniora, do zrobienia czegoś innego. Wmów mu, iż przecież właśnie to jest mityczna sytuacja wyjątkowa!

fun findBy(orderId: Int, username: String): Order = this.orders[username].find{it -> it.id == orderId} ?: throw IllegalStateException("Order not found!")

Jeśli uda Ci się zgrabnie wprowadzić do tej metody rzucanie wyjątku, to istnieje uzasadniona nadzieja, iż popsujesz niejeden kod, z niej korzystający. Choć nie zmienia się sygnatura metody, to wymagane już jest dodatkowe działanie po stronie klienta.

fun processOrders(orderId: Int, username: String){ var orders = emptyList<Order>() try{ orders = findBy(orderId, username) } catch (e: IllegalStateException){ log.error(e) } }

I to jeszcze nie koniec! Za jakiś czas nasz coraz bardziej doświadczony programista będzie widział zagrożenia, które wcześniej umykały jego czujności.

Spójrzmy na to jeszcze raz… OrderId nie będzie nullem, to już wiemy — dzięki systemowi typów Kotlina (brak znaku ?), ale czy może być wartością ujemną? Też nie! Nasza domena mówi też, iż username nie może być krótszy niż 5 znaków. Czas wyrazić te niezmienniki w jakiś sposób w kodzie.

fun findBy(orderId: Int, username: String): Order { if(orderId < 0){ throw InvalidOrderIdException(orderId) } if(username.trim().size() < 5){ throw InvalidUsernameException(username) } return this.orders[username].find{it -> it.id == orderId} ?: throw IllegalStateException("Order not found!") }

W końcu nasza metoda spełnia te wymagania biznesowe! Mógłbyś mnie zapytać, z czego tutaj się cieszyć? A no dlatego, iż dobrymi chęciami to jest piekło usłane. Twój podopieczny, chcąc usunąć te małe kłamstewka ze swojego kodu, popadł w o wiele większą zbrodnię! Może kiedyś słyszał coś o SOLID, ale patrząc na tę metodę, to nie ma tutaj pojedynczej odpowiedzialności — metoda nie tylko wyszukuje żądane zamówienie, ale też sprawdza poprawność wejścia. Może Open-Closed? Niestety, zmiana wymagania odnośnie poprawności username zawsze pociągnie za sobą zmiany w metodzie, która… wyszukuje zamówienie.

Spójrzmy teraz jeszcze na samą sygnaturę omawianej metody, czyli z perspektywy jej użytkownika.

fun findBy(orderId: Int, username: String): Order

Czy patrząc na samą deklarację, nasuwa się choćby podejrzenie, iż zostanie rzucony wyjątek? Unchecked Exceptions (wyjątki, których obsługa nie jest wymuszana na etapie pisania kodu) to jedna z najbardziej zdradzieckich rzeczy, jakie wymyśliliśmy. Ludzie piszą całe książki o tym, jak poprawnie wersjonować API, a my… możemy to wszystko tak łatwo zaprzepaścić. Zauważ, iż dodanie nowego wyjątku do metody nie powoduje choćby żadnych błędów kompilacji. Zdarzy się to dopiero w najmniej spodziewanym momencie. A jest to de facto zmiana publicznego kontraktu. To tak jakbyś zmienił typ, jaki jest zwracany przez metodę.

Wyobraź sobie teraz jaką satysfakcję czerpią z tego opiekunowie innych programistów, którzy używali tej metody. Niczego nie świadomi, mknął w kierunku nieobsłużonych wyjątków. Wspaniale, prawdziwy sukces!

Niefortunny komunikat błędu w aplikacji Pizza Hut. jeżeli już stosujemy wyjątki, to warto zadbać choćby o przyzwoity opis, który ewentualnie wyświetli się na ekranie urządzenia.

Obsesja typów prostych & Value Object

A wiesz, iż może być jeszcze lepiej? Podpowiedz swojemu podopiecznemu, iż przecież username jest używany w wielu miejscach aplikacji. W takim razie w każdym miejscu musimy sprawdzać, czy przekazany string jest poprawną nazwą użytkownika. Wspaniale! Kod zaraz zaroi się od wyjątków, a o przestrzeganiu zasady DRY możemy zapomnieć.

Koniecznie nie dopuść, aby nasz Junior zaaplikował jakieś remedium na tę sytuację. Zobacz jeszcze raz, jakim wspaniałym oszustwem jest teraz sygnatura metody, jaką zdefiniował.

fun findBy(orderId: Int, username: String): Order

Patrząc na tę deklarację, nasuwa się kilka wniosków. Jako programista używający takiej metody bez problemu stwierdzam:

  • metoda oczekuje orderId. Mogę podać każdą liczbę całkowitą (w zakresie Int), czyli np. -999, 0, -1, 12 [KŁAMSTWO, po podaniu np. -1 nastąpi rzucenie wyjątku]
  • metoda oczekuje nazwy użytkownika. Przyjmuje każdy String. Z tego dowiaduję się, iż nazwą użytkownika może być "", ” ”, “test” [KŁAMSTWO, po podaniu np. pustego Stringa nastąpi rzucenie wyjątku]
  • metoda zawsze zwraca mi znaleziony obiekt typu Order (deklaracja nie jest Order? więc nie spodziewam się tutaj NULLa) [KŁAMSTWO, nie zawsze, bo może zostać rzucony wyjątek]

Niestety, istnieją sposobu na prawdomówność pisanego kodu… Pierwszym krokiem, jaki może zrobić, to wyeliminowanie Primitive Obsession.

Jeden z naszych wielkich nieprzyjaciół — Eric Evans, w swojej książce Domain-Driven Design: Tackling Complexity in the Heart of Software zdefiniował wzorzec Value Object. Nigdy nie dopuść, żeby żadna z tych książek wpadła w jego ręce! A choćby jeśli… i tak jest duża szansa, iż jej nie zrozumie. Albo przynajmniej kupi egzemplarz w języku polskim — wtedy jeszcze mniej pojmie :) Jedno z najprostszych zastosowań tego wzorca możesz zobaczyć poniżej (jeśli chcesz go poznać lepiej, to zajrzyj TUTAJ). Do jego implementacji użyłem Kotlinowej funkcjonalności - data class. Nie jest to w tym przypadku konieczne, ale Data Class automatycznie implementuje od razu metody equals i hashcode oparte na wartościach pól klasy.

data class OrderId private constructor(val raw: Int){ companion object { fun of(raw: Int): OrderId { if(raw < 0){ throw InvalidOrderIdException(raw) } return OrderId(raw) } } } data class Username private constructor(val raw: String){ companion object { fun of(raw: String): Username { if(raw.trim().length < 5){ throw InvalidUsernameException(raw) } return Username(raw) } } } fun findBy(orderId: OrderId, customer: Username): Order? = this.orders[customer].find{it -> it.id == orderId}

Niestety, jeżeli dojdzie już do tak niebezpiecznego kodu. Developerzy zyskują naprawdę dużo. Logika poprawności obiektów jest w jednym miejscu, obiekt Username jest zawsze poprawną nazwą użytkownika, a metoda mówi prawdę. Spójrz, co teraz można wywnioskować po samej deklaracji.

fun findBy(orderId: OrderId, customer: Username): Order?
  • metoda zwraca Order bądź null. Tak jak wcześniej zakładamy, iż nie jest rzucany wyjątek [PRAWDA]
  • metoda przyjmuje jako parametr orderId, który jest poprawnym identyfikatorem zamówienia (wg zasad domenowych) [PRAWDA]
  • metoda przyjmuje jako parametr customer (a adekwatne jak mówi nazwa parametru — nazwę użytkownika klienta). W metodzie Username jest z pewnością poprawny wg. zasad domenowych. Nie jest wymagane żadne dodatkowe sprawdzenie. [PRAWDA]

Takie zastosowanie wzorca ValueObject nie tylko pociąga za sobą takie benefity jak powyżej. Chroni choćby przed podaniem parametrów w nieprawidłowej kolejności. Wyobraź sobie taką metodę, która oczekuje kilku argumentów tego samego typu.

fun register(firstName: String, lastName: String, email: String)

Możemy ją wywołać np. w poniższy sposób.

register("mateusz@zycienakodach.pl", "Mateusz", "Nowak")

Na etapie kompilacji wszystko będzie wspaniale. Chociaż zrobiliśmy błąd logiczny w wywołaniu, to dla języka programowania wszystko się zgadza. Dopiero później programiści wspaniale zmarnują czas na poszukiwanie i debugowanie, aż dowiedzą się — iż znowu, po raz 100, zamienili parametry miejscami. Albo jeszcze lepiej — przez długi czas mogą tego być wcale nie świadomi. Skutki mogą być różne. Mail nie doszedł? Albo doszedł do kogoś o imieniu Nowak. Czemu nie? A w logach zupełnie nic, żadnego błędu…

W Kotlinie ewentualnie możliwa pozostało mitygacja tego poprzez użycie Named Arguments tak jak poniżej.

register(email = "mateusz@zycienakodach.pl", firstName = "Mateusz", lastName = "Nowak")

Jednakże jeżeli zastosujemy tutaj podobnie jak wcześniej, wzorzec ValueObject, to oprócz walidacji np. poprawności adresu Email zyskujemy od razu błąd na etapie pisania kodu.

fun register(firstName: FirstName, lastName: LastName, email: Email)
register(Email.of("mateusz@zycienakodach.pl"), FirstName.of("Mateusz"), LastName.of("Nowak"))

Przy takim wywołaniu IDE od razu poinformuje programistów o potencjalnych problemach. Na szczęście programiści długo to bagatelizują, aż nie odbije się to na niedziałającym oprogramowaniu i dużych stratach w firmie. Chociaż… jest już (ciężko mi to przechodzi przez gardło) - o niebo lepiej — to choćby w tej implementacji zostały nam jakieś wyjątki do ewentualnego obsłużenia, ponieważ musi gdzieś w kodzie mieć miejsce zamiana z typów prostych na Value Object.

Musisz być świadom, iż istnieją sposoby na zupełnie się ich pozbycie. Mam nadzieję, iż Twój podopieczny nie słyszał jeszcze o możliwościach, jakie daje w tym zakresie czerpanie z paradygmatu programowania funkcyjnego? Gdybyś jednak Ty chciał wiedzieć, przed czym należy go chronić, to czekaj na kolejny z moich listów. Sądzę jednak, iż nie jest to sprawa bardzo paląca, ponieważ większość programistów choćby nie dochodzi do etapu rozwoju, który opisałem Ci tutaj. Na pewno będzie potrzebował duuuużo praktyki, żeby wejść na ten wyższy poziom wtajemniczenia, a to da nam odpowiedni czas na przygotowanie.

Mam nadzieję, iż moje wskazówki przydadzą Ci się w trakcie trwania Twojej misji!

Twój oddany stryj

Krętacz

Post Scriptum

Powiedz szczerze, co sądzisz o moich radach? Czy chciałbyś poznać więcej The Worst Practices przedstawionych w takiej formie? Czekam na Twoją odpowiedź poniżej.

Disclaimer

Forma tego posta jest inspirowana książką C. S. Lewisa „Listy starego diabła do młodego”. Tak, to ten od „Opowieści z Narnii” - tylko trochę poważniej :) Nie jestem w stanie się równać z tak znakomitym pisarzem. To jednak mam nadzieję, iż Twój kod zmieni się nie do poznania po przeczytaniu tego posta. Tak samo, jak po lekturze wspomnianej książki — życie nie jest już takie samo, choćby to życie daleko od kodu :) Z formą prezentowaną przez Lewisa możesz zapoznać się TUTAJ - fragment do czytania online, posłuchać audio-booka na YouTube, albo od razu przejść do czytania dalej.

Inni też tym żyją…

Zobacz też jak kod może kłamać na prezentacji o rekrutacji programistów Tomasz Dziurko - Brzydka Pani od HR radzi, czyli 1011 błędów, które popełniają programiści.

Jeśli chcesz zagłębić się w tematy poruszane w tym wpisie, to polecam Ci na początek te miejsca w Internetach.

Idź do oryginalnego materiału