Software Craftsmanship: Błędy i kompromisy
W ramach podcastu “Porozmawiajmy o IT” miałem okazję porozmawiać z Łukaszem Drynkowskim z SOLID.Jobs o błędach i kompromisach w programowaniu.
Posłuchaj naszej rozmowy w wersji audio 🎧 https://porozmawiajmyoit.pl/poit-248-software-craftsmanship-bledy-i-kompromisy/
Spotykamy się po raz ósmy w ramach specjalnej serii podcastów o software craftsmanship i dzisiaj będziemy mówić o temacie nieco odmiennym, bo wcześniej poruszaliśmy takie, mam wrażenie, nieco wyniosłe tematy. Mówiliśmy o etosie programisty, o tym, jakie są dobre praktyki w tym naszym rzemiośle. Mówiliśmy trochę o paradygmatach programowania, a nawet wzorcach projektowych, a dzisiaj spojrzymy sobie na błędy, spojrzymy sobie na kompromisy, które w codziennej pracy musimy podejmować, ale które też, mam wrażenie, są taką immanentną cechą programowania i po prostu nie da się bez nich tworzyć softu, więc brzmi ciekawie.
To jednocześnie będzie ostatni odcinek zamykający nam tę serię podcastów, więc też jesteśmy bardzo ciekawi: jeśli chcielibyście coś skomentować, dodać, dać nam jakiś feedback, to oczywiście do tego zapraszamy.
Łukasz, myślę, że trzeba sobie zacząć od tego, żeby powiedzieć, czym te trade-offy w programowaniu są, jaką funkcję, jaką rolę sprawują.
Trade-offy w programowaniu to jest taka sztuka wyboru między jedną pożądaną cechą a drugą pożądaną cechą i często jest tak, że nie jesteśmy w stanie zapewnić tych dwóch cech naraz albo w takim samym stopniu. Bo wiadomo, że to jest jakiś taki suwak, który przesuwamy albo w jedną stronę, albo w drugą. Nie jest to wbrew pozorom zero-jedynkowe. I takie typowe trade-offy to czy robimy coś w sposób jak najprostszy, taka wersja, żeby działało, czy może robimy trochę takiej inżynierii i staramy się zrobić jednak coś, co będzie miało jakieś przyszłościowe cechy i od razu robimy troszkę bardziej skomplikowany soft. To jest taki przykład, żeby tutaj pokazać, o czym będziemy dzisiaj rozmawiać.
Tak, chciałbym tylko jedną rzecz skomentować, że faktycznie to jest może takie istotne rozróżnienie tych kompromisów, trade-offów vs błędy, bo błędy się zdarzają albo popełnia się je gdzieś tam, mając je z tyłu głowy, że po prostu w którymś momencie trzeba będzie je naprawić, natomiast tak jak powiedziałeś, trade-offy to jest sztuka wyboru pomiędzy jednym a drugim i w zależności od kontekstu, od projektu, sytuacji, od tego, w którym momencie rozwoju projektu jesteśmy, to wybór tych dwóch nieraz skrajnych elementów może mieć znaczenie i to nie znaczy, że my robimy jakiś błąd albo jesteśmy czegoś nieświadomi, tylko np. decydujemy się zrobić coś prosto na początku, ponieważ zależy nam na tym, żeby szybko dostarczyć produkt, żeby szybko zweryfikować go na rynku.
Albo też wiemy, że poruszamy się w takiej domenie, która jest np. skomplikowana, jest regulowana i tej inżynierii na początku jest trochę więcej wymagane. Wiemy, że będzie się w danym określonym obszarze to rozwijało i też musimy na początku przynajmniej spróbować tam architekturę zaprojektować.
Wówczas decydujemy się może na troszkę dłuższy development, ale mimo wszystko na stworzenie czegoś bardziej skomplikowanego, więc jak gdyby dążę do tego, że w tych kompromisach nie ma nic złego. Myślę sobie, że tylko trzeba być świadomym, że decydujemy się właśnie na taki, a nie inny kompromis, że w związku z tym będziemy musieli być może z czegoś zrezygnować, że być może w pewnym momencie będziemy musieli zweryfikować te nasze założenia, ale mimo wszystko robimy to świadomie.
Tak, jeszcze ciekawa rzecz, którą tutaj podjąłeś — może ja trochę będę kontynuował wątek: to, jaką decyzję podejmujemy, to może zależeć od tego, w jakim po prostu jesteśmy momencie danego projektu. I tę samą decyzję podejmiemy zupełnie inaczej, będąc na samym początku, będąc gdzieś tam w połowie projektu, a na samym końcu po prostu. To zależy od różnych czynników też zewnętrznych, od różnych uwarunkowań, w których jesteśmy, i może na początku jesteśmy gotowi pójść na jakiś kompromis i po prostu coś zrobić szybciej, coś zrobić mniej dokładnie. A jeśli jest to już ten końcowy etap projektu, już jesteśmy blisko wydania danego oprogramowania, to podejmujemy zupełnie inne decyzje, inne priorytety mamy.
Dokładnie, myślę, że to jest istotne. Dobrze, to może porozmawiajmy właśnie o tych najczęściej występujących trade-offach, kompromisach, które musimy na swojej drodze programistycznej prędzej czy później podejmować. Tutaj przywołałeś ten przykład zrobienia czegoś prosto vs być może włożenia więcej czasu, wysiłku po to, żeby zbudować solidniejsze fundamenty.
Oczywiście możemy pójść w jakąś tam skrajność i wpaść w taką pułapkę over engineeringu, czyli robienia czegoś zbyt skomplikowanego, próbując przewidzieć różne ewentualności, które tak jak wiemy, rzadko kiedy dochodzą do skutku. I myślę sobie, że takim przykładem tutaj, niekoniecznie może z tej naszej domeny programistycznej, ale i z informatyki ogólnie jest multicloud, czyli próba stworzenia takiego rozwiązania, które będzie działało tak dosyć agnostycznie na różnych chmurach. Wtedy musimy tego wysiłku programistycznego, inżynieryjnego włożyć dużo więcej, a wiemy, że mało jest tak dużych, tak skomplikowanych projektów, które będą musiały albo będą chciały w pewnym momencie np. przełączyć się na inną chmurę, dajmy na to.
Drugi przykład, który możemy tutaj na pewno przywołać, to jest bardzo klasyczne skupienie się na różnych zasobach, które mamy w przypadku, dajmy na to, serwera. Mówię tutaj o pamięci vs procesor. Albo będziemy chcieli zawsze dane przetwarzać, wyliczać za każdym razem, kiedy użytkownik ich zażąda, wtedy wiemy, że są świeże, jak to się ładnie mówi: zawsze up-to-date.
I aktualne.
I aktualne, ale nie zawsze to jest najlepsze rozwiązanie, bo oczywiście wtedy poruszamy się w tym wąskim gardle procesora. Możemy oczywiście zastosować klasyczne podejście związane z cachem po to, żeby składować sobie te wstępnie wyliczone jakieś elementy naszego przetwarzania danych po to, żeby np. później skorzystać już z tych danych, a nie przetwarzać je po raz kolejny.
Oczywiście to też nie jest jak gdyby żaden, jak to się mówi ładnie, darmowy lunch, bo wiemy, że utrzymanie cashe’a, jego tej aktualności to też jest jak gdyby kolejny problem, tutaj nic nie przychodzi za darmo, ale decydując się na jedno albo drugie, w zależności od też potrzeby biznesowej, niektóre rzeczy musimy po prostu za każdym razem wyliczyć na nowo, bo takie są wymagania, a inne możemy sobie spokojnie założyć, że np. w ciągu pięciu minut będziemy je dostarczać z cache’a, a nie wyliczać.
Tak, tutaj podobne rozkminy mogą być typu, czy ten eventual consistency jest okej, czy bardziej chcemy np., żeby od razu użytkownik otrzymał te aktualne dane, czy np. zależy to od tego procesu biznesowego, co ktoś robi, czy właśnie teraz podejmie na podstawie tego wyniku decyzję użytkownik, czy jest to tylko taka informacja, nie wiem, o statusie np. zamówienia, i jeśli ten status się zmieni za jakiś czas na to, że jest w realizacji, to nic się nie stanie.
Tak że tutaj dochodzimy do kwestii tego, jak kompensować te sytuacje, kiedy coś jest nie do końca aktualne. Wtedy np. musimy włożyć energię swoją w to, żeby rozwiązać problem, jeśli coś zrobiliśmy nad wyraz. Takim przykładem tutaj jest Amazon. Nie wiem, czy to jeszcze jest dalej aktualne, ale z tego co pamiętam, to Amazon właśnie robił w ten sposób, że jak kupowałeś dany przedmiot, to dostawałeś informację, że okej, udało Ci się kupić, a jeśli potem w tych ich background serwisach się okazało, że jednak nie mają już na stanie danego produktu, no to dostawałeś maila z przeprosinami i z jakimś rabatem. I w ten sposób oni to rozwiązywali.
Tak, czyli nie zawsze da się tylko i wyłącznie w sposób techniczny problem rozwiązać, nieraz biznesowe podejście i taka kompensacja jak najbardziej też się sprawdza. I tak sobie ogólnie myślę, że te trade-offy właśnie pewnie można podzielić na takie dwie grupy, jedne bardzo techniczne, wynikające z jakichś limitów, chociażby to użycie bardziej pamięci vs bardziej procesora, a inne, które wynikają właśnie z tego biznesowego case’a, w ramach którego się poruszamy.
Mówiliśmy tutaj o takim podejściu, że na początku tworzymy coś prostego, po to, żeby zweryfikować to chociażby szybko na rynku, zobaczyć, czy są klienci na tę naszą usługę, czy produkt. I to jest z kolei właśnie taki przykład zastosowania, czy takiego rozróżnienia bardziej z gatunku wpływu biznesowego. I myślę sobie, że kolejnym przykładem właśnie takiego typu trade-offu jest szybkość dostarczenia.
To oczywiście może być powiązane z tą prostotą, że robimy sobie jakieś MVP i weryfikujemy na rynku, ale też szybkość, myślę sobie tutaj, która jest niestety kompromisem dokładności, tego craftsmanshipu, o którym tutaj rozmawiamy. Wiemy, że nie zawsze to, co tworzymy, to jest taki state of the art, ale mimo wszystko zależy nam albo są przesłanki właśnie biznesowe do tego, żeby jednak po prostu to gdzieś upublicznić i zweryfikować.
Tak, czyli tutaj masz taki trade-off szybko kontra dobrze, tak, ale to szybko to tutaj też możesz, masz trade-off w tym, bo szybko to może znaczyć właśnie, no to zróbmy mniej, ale to, co zrobimy, no to niech będzie tip-top, albo zróbmy, pójdźmy szeroko, dajmy tu dużo funkcji, ale takich, niedogotowanych, że gdzieś tam happy-puffy działają, ale gdzieś np. jakaś obsługa błędów jeszcze tutaj wymagać będzie dodatkowej pracy w przyszłości.
I jeśli jesteśmy przy szybkości, to myślę sobie, że rozwój języków programowania to jest takie nakładanie kolejnych warstw abstrakcji, prawda? Już dawno nie musimy się gdzieś tam babrać w asemblerze, żeby dodać jedną liczbę do drugiej. Ale wszyscy ci, którzy pracują w pewnych szczególnych domenach, wiedzą, że zejście właśnie do tego niskiego poziomu powoduje, że jesteśmy w stanie wycisnąć więcej z tej maszyny albo że jesteśmy w stanie szybciej pewne rzeczy przetwarzać, wykorzystując po prostu mniej zasobów, więc taki trade-off tego, że bawimy się zabawkami, tworzymy ten soft z użyciem zabawek takiego wysokiego poziomu, ma właśnie ten kompromis, że one nie będą maksymalnie szybkie, maksymalnie wydajne, ale z drugiej strony dla programistów daje to taką możliwość łatwiejszego, szybszego budowania tych rozwiązań.
Przykładem może być low-code, to są tego typu rozwiązania, że jesteśmy w stanie w miarę szybko zbudować rozwiązanie, które wydajnościowo nie jest może optymalne, nie zawsze nadaje się do tych wszystkich obliczeń, które faktycznie są wymagające, ale z drugiej strony pozwala dostarczyć dosyć szybko rozwiązanie, które może być już szybko dostępne, szybko pomagać w rozwiązywaniu jakiegoś problemu.
Tak, czyli np. tutaj też ten trade-off by się objawił w zakresie tego, jak tutaj bardzo chcemy mieć wpływ, czy też możliwość konfiguracji tej wynikowej funkcji, a jak dużo tutaj po prostu jesteśmy gotowi poświęcić w ramach tego, że po prostu dostaniemy coś out of the box i dostaniemy coś gotowego. To też mi się wiąże z takim tematem np. używania zewnętrznych bibliotek, czy też szerzej frameworków. Framework daje nam dużo, tak? Ale też z drugiej strony w dużej mierze nas ogranicza i też nam daje jakieś tutaj ramy, w których się musimy poruszać. I czasami jest tak, że więcej na tym stracimy w frameworku, bo będziemy musieli jakby płynąć pod prąd, próbować zawrócić rzekę kijem i finalnie niekoniecznie będzie to najlepsze rozwiązanie, jeśli chodzi o ilość włożonej przez nas pracy.
Natomiast nie jestem wrogiem w żadnym razie bibliotek. Ale trzeba zawsze tutaj wziąć to pod uwagę i gdzieś mieć z tyłu głowy, że rzeczywiście nie zawsze użycie biblioteki to będzie mniej pracy, a z drugiej strony właśnie może warto jest użyć tej biblioteki, żeby nie robić jakiejś rzeczy, które już ktoś wymyślił, już raz to koło zostało tutaj opracowane, że jednak jest okrągłe. To jest to.
Tak, dokładnie. W odcinku o Secure by Design też poruszyliśmy taki ciekawy trade-off, że jednocześnie chcemy zapewnić, aby to rozwiązanie było bezpieczne. Chcemy maksymalnie wstępnie okroić ilość możliwych opcji, żeby to bezpieczeństwo zapewnić, ale z drugiej strony chcielibyśmy, żeby mimo wszystko ktoś korzystał z tego naszego software’u i żeby przede wszystkim ten user experience był na wysokim poziomie.
Więc to też jest zawsze taka sztuka zbalansowania, czy chcemy się poruszać i działać w sposób bardzo bezpieczny z użyciem wielu mechanizmów zabezpieczających, które będą wymagały na przykład jakiejś akcji ze strony użytkownika, czy też chcemy być funkcjonalni i przydatni dla użytkownika.
To tutaj z bezpieczeństwem także łączy się drugi taki temat, czyli jakość, bo im wyższa jakość, to też tym wyższe bezpieczeństwo tego rozwiązania i myślę, że tutaj też trade-offy, które codziennie podejmujemy, to też są właśnie odnośnie do tej jakości tych testów, jak testujemy dane oprogramowanie, ile czasu ma nam zejść na samo testowanie, a ile na dodawanie nowych funkcji, dodatkowej funkcjonalności. I to też jest taki trade-off, który ten programista codziennie w swojej pracy podejmuje.
To, co tutaj się łączy z tym, to czytelność kodu a jego wydajność. Często jest tak, że chcielibyśmy fajnie coś wyinżynierować, użyję takiego słowa, ale np. ze względów wydajności lepiej jest coś zrobić trochę gorzej, ale nie wyliczać czegoś kilkukrotnie. Albo może da się zrobić to i czytelnie i wydajnie, ale to w ten sposób zajmie nam trzy dni zamiast jednego dnia, i niekoniecznie to jest akceptowalne z kolei pod względem kosztów.
To tutaj z bezpieczeństwem także łączy się drugi taki temat, czyli jakość, bo im wyższa jakość, to też tym wyższe bezpieczeństwo tego rozwiązania i myślę, że tutaj też trade-offy, które codziennie podejmujemy, to też są właśnie odnośnie do tej jakości tych testów, jak testujemy dane oprogramowanie, ile czasu ma nam zejść na samo testowanie, a ile na dodawanie nowych funkcji, dodatkowej funkcjonalności. I to też jest taki trade-off, który ten programista codziennie w swojej pracy podejmuje.
👉 Czytaj dalej: https://porozmawiajmyoit.pl/poit-248-software-craftsmanship-bledy-i-kompromisy/