Jak pisać dobry kodzik? – Czyli geneza „to zależy”

W dzisiejszym artykule chciałbym Wam nieco opowiedzieć o tym jak pisać „dobry kodzik”. Część z Was pewnie spodziewa się ciekawych wzorców, praktyk oraz zasad dobrego programowania. Otóż nic bardziej mylnego! Ugryzę temat nieco z innej strony i postaram się pokazać Wam, że znajomość technicznych zagadnień nie zawsze wystarcza.

Co to znaczy dobry kodzik?

Zanim zaczniemy drążyć temat dalej najpierw, odpowiedzmy sobie na to jakże ważne pytanie. Jeśli zapytamy się programistów: jakie cechy powinien posiadać dobry kod? To większość z nich zgodnie odpowie, że dobry kod powinien być:

  • czytelny
  • łatwo testowalny
  • powinien posiadać dużą ilość testów
  • pisany z zachowaniem zasad dobrego programowania takich jak np. SOLID, KISS, DRY itp.
  • wydajny
  • odporny na błędy

Druga część zacznie się „wymieniać argumentami” :

Programista 1: Dobry kodzik powinien posiadać architekturę warstwową.
Programista 2: Wiadomo, że architektura heksagonalna jest najlepsza!
Programista 3: A na co mi te heksagony, jakieś warstwy, duża ilość klas i moduły. To kompletna strata czasu… Jeden serwis wszystko załatwia! Programuję od 20 lat i to podejście nigdy mnie nie zawiodło!
Programista 4: Tylko assembler nas wyzwoli!

developers don't argue they explain why you are stupid

Większość z nas (w tym ja na początku swojej kariery) uważa, że dobry kod powinien posiadać wszystkie wyżej wymienione cechy. Część z nas ma też swoje osobiste upodobania co do sposobu pisania kodu, architektury oraz technologii i nie uznaje innych podejść. Ale czy aby na pewno dobry kod powinien być łatwo testowalny, czytelny, posiadać testy oraz być napisany w architekturze heksagonalnej?

Kilka lat temu tworzyłem aplikację dla jednej z bardziej znanych marek sprzedających alkohol w Polsce. Była to aplikacja do obsługi konkursu, który ta firma chciała zorganizować. Klient ogłosił konkurs na swoich social mediach wraz z datą startu. Zarezerwował również bardzo drogą nagrodę na konkretny termin dla zwycięzcy. Niestety, wtedy panowała najbardziej popularna choroba świata i ustalenia odnośnie formy konkursu, zasad oraz reguł trwały bardzo długo i często się zmieniały przez wprowadzane obostrzenia. Finalnie na implementację zostało mi tylko dwa tygodnie. Na szczęście wszystko się udało. Konkurs się odbył, a zwycięzca mógł się cieszyć z wymarzonej nagrody.

Kod napisany przeze mnie w dwa tygodnie nie był ani czytelny, ani łatwo testowalny, nie posiadał testów oraz posiadał architekturę typu big ball of mud. Czy uważam, że mój kodzik był dobry? Oczywiście, że tak! Najlepszy!

Ale jak to?! Przecież wyżej napisałem, że dobry kod powinien posiadać wszystkie cechy, których właśnie nie spełniłem i jeszcze się tym chwalę…

dobry kod

Czym są cechy i jakie mają znaczenie?

Cechy to tylko cechy. One w jakiś sposób „coś” charakteryzują. Nie są ani pozytywne, ani negatywne. One po prostu są. Zastanówmy się zatem na co wpływają niektóre z nich.

Czytelność – pomaga nam PO CZASIE łatwiej zrozumieć kod, przez co koszt jego utrzymania jest mniejszy oraz łatwiej jest wdrożyć nowego dewelopera. Ułatwia to również pracę naszym kolegą z zespołu.

Testowalność – ułatwia i przyspiesza pisanie testów co znacząco wpływa na czas oraz koszty.

Kod powinien posiadać dużą ilość testów – testy zabezpieczają nas przed wprowadzeniem regresji. Dodatkowo po napisaniu nowej funkcjonalności pomagają nam upewnić się, że ona działa bez konieczności manualnego przeklikiwania się przez aplikację. Stanowią również formę dokumentacji.

Odporność na błędy – zapewnia nam ciągłość w działaniu przez co nie tracimy pieniędzy ani reputacji przez to, że nasze kluczowe procesy biznesowe nie działają.

Pisany z zachowaniem zasad dobrego programowania takich jak np. SOLID, KISS, DRY itp. – ułatwia testowanie, utrzymanie oraz modyfikację kodu. Zwiększa również czytelność (pkt 1). To wszystko wpływa na czas oraz koszty.

Wydajny – pozwala nam obsłużyć duży ruch przez co klienci nie muszą długo czekać na odpowiedź.

Dostępność – dzięki niej mamy pewność, że każdy nasz klient zostanie obsłużony.

A więc zastanówmy się:

Czy ja potrzebowałem czytelnego kodu w mojej aplikacji? No nie. Może nie jestem mistrzem zapamiętywania, ale aplikacje pisałem tylko dwa tygodnie więc doskonale wiedziałem co się tam dzieje. Aplikacja miała żyć tylko przez tydzień, a potem zostać usunięta. Nie miałem zespołu, ani potrzeby wdrożenia nowego developera. Pracowałem sam.

Czy potrzebowałem łatwo testowalnej aplikacji z dużą ilością testów? Nie miałem w ogóle testów, a więc zapewnienie testowalności by mi nic nie dało.. Jedyne testy jakie posiadałem to testy manualne, które robiłem własnoręcznie. Nie musiałem zabezpieczać regresji, ponieważ takowej nie było. Aplikacja została napisana w dwa tygodnie, nie była duża i żyła tylko tydzień, a więc zrobienie pełnych manualnych testów zajęło mi w sumie godzinę czasu.

Czy zasady typu solid, kiss, dry mi w czymś pomogły? Również nie. Nigdy nie miałem zamiaru wracać do tej aplikacji a co dopiero jej modyfikować, czy rozwijać.

Czy dostępność była potrzebna? Oj tak! Była to aplikacja, która musiała obsłużyć duży ruch i nie mogła przestać działać, ponieważ bardzo źle by to wpłynęło na reputację marki.

Czy wydajność była kluczowa? I tak i nie. W pewnym stopniu na pewno, natomiast nie były to wielkości rzędu milionów zapytań na minutę, a więc mocna maszyna i odpowiednie skalowanie wystarczyły. Nie potrzebowałem robić optymalizacji na poziomie kodu.

Weźmy na tapet kolejny przykład. Na potrzeby wcześniej wspomnianej choroby dla jednego z większych portali informacyjnych w Polsce, tworzyłem aplikację, która miała odpowiadać na podstawowe pytania związane z COVID. Aplikacja musiała powstać szybko, aby jak najbardziej zminimalizować skutki choroby i ryzyko zachorowania oraz oczywiście zwiększyć reputację, renomę, popularność oraz ocieplić wizerunek firmy.

Aplikacja została napisana, działała według wcześniej określonego scenariusza oraz żyła kilka miesięcy. Podobnie jak wcześniej praktycznie ŻADNA z wyżej wspomnianych cech (poza wydajnością) nie była dla nas przydatna ani nie została spełniona.

Pewnego razu miałem również okazję do napisania modułu sprawdzającego legalność przeprowadzanych transakcji w jednym z większych kantorów kryptowalutowych na świecie.

Czy odporność na błędy oraz dostępność była konieczna? Oczywiście, że tak! Źle rozpatrzona transakcja realnie wpływała na zarobki firmy. Nie było mowy o tym, aby transakcja została odrzucona bo na przykład zewnętrzny dostawca przestał działać. Trzeba było zapewnić mechanizm retry, a w najgorszym wypadku oznaczyć transakcję jako wymagającą ręcznej ingerencji.

Czy czytelność była potrzebna? Oczywiście, że tak! Nad modułem pracował spory zespół. Sam moduł był wykorzystywany przez kilka różnych zespołów, a więc zarówno API jak i kod w jego wnętrzu musiał być na najwyższym poziomie czytelności.

Czy zapewnienie testowalności oraz dużej ilości testów było konieczne? Jak najbardziej! Czas napisania nowej funkcjonalności, a co za tym idzie jej o testowania musiał być jak najkrótszy. Musieliśmy bardzo szybko reagować na wymagania narzucone przez konkretnych regulatorów państwowych. Przy tym nie mogliśmy sobie pozwolić na wprowadzenie błędów do już istniejących funkcjonalności. Wiązałoby się to z realnym naruszeniem prawa.

Czym tak naprawdę jest dobry kod?

Dobry kod to taki, który spełnia wszystkie wymagania biznesowe. Właśnie dlatego uważam, że kod napisany przeze mnie w wyżej wspomnianych aplikacjach jest dobry. Pomimo że, w dwóch pierwszych nie posiadał wszystkich cech, które dla większości programistów są konieczne, aby nazwać kod dobrym to spełnił wszystkie założenia biznesowe.

Czy jeśli kod w dwóch pierwszych aplikacjach byłby czytelny, łatwo testowalny, zoptymalizowany w każdy możliwy sposób, posiadał dokumentację to byłby dobry? Purysta technologii i człowiek nieznający kontekstu na pewno stwierdziłby, że tak. Jednak prawda jest taka, że każda z tych cech kosztuje zarówno czas jak i pieniądze i żeby je spełnić musiałbym znacząco zwiększyć zespół oraz wydłużyć czas developemntu co najmniej kilkukrotnie. Mijałoby się to kompletnie z celem, ponieważ po takim czasie aplikacje nie miałby by kompletnie sensu pod względem biznesowym. Co więcej koszt jej wytworzenia wzrósłby kilkukrotnie, a żadna z tych cech nie przyniosłaby realnej wartości.

Pod względem technicznym aplikacja byłaby na pewno lepsza, ale należy pamiętać, że aplikacji nie tworzymy dla samego tworzenia. Tworzymy je aby pomóc ludziom w ich pracy. Każda aplikacja, która powstaje ma konkretny cel, który musi spełnić. Jeśli go nie spełnia to znaczy, że przepaliliśmy tylko pieniądze i czas lub innymi słowy okradliśmy inwestora.

Natomiast w ostatnim przykładzie wszystkie te cechy musiały zostać spełnione (nawet była architektura heksagonalna) i kod również był dobry, ponieważ ponownie: spełniał wszystkie założenia biznesowe. A więc to jaki kod możemy nazwać dobrym kodem zależy tylko i wyłącznie od tego czy przynosi realną wartość biznesowi.

A wymagania biznesowe zależą od kontekstu w jakim się znajdujemy. Bez jego znajomości nie możemy stwierdzić czy dany kod jest dobry, czy dany wzorzec jest w porządku, czy dana technologia ma sens.

Bardzo często na różnych grupach i forach widzę, jak developerzy kłócą się o to, który jezyk/technologia jest lepsza przerzucając się przy tym argumentami typu: „a bo rust jest szybszy”. Jednak czy my potrzebujemy szybkości na takim poziomie? Bez znajomości kontekstu nie jesteśmy w stanie tego stwierdzić. Być może robimy aplikację dla ZUS’u w której ani szybkość, ani dostępność ani intuicyjność nie jest wymagana.

Czy COBOL jest lepszy od C#? W pewnym kontekście tak. Na przykład w kontekście w którym mamy dostępnych tylko programistów COBOL’a, którzy mają ograniczony czas na wdrożenie funkcjonalności.

Czy kod proceduralny jest lepszy od zastosowania pełnego DDD? Tak, jeśli piszemy prostego CRUD’a. Ale nie jeśli mamy skomplikowaną logikę biznesową.

Czy architektura heksagonalna jest lepsza od warstwowej? Tak, wiem obie architektury są jak Shrek i mają warstwy, ale zależności pomiędzy nimi biegną nieco inaczej. Wracając do pytania architektura heksagonalna sprawdzi się w przypadku gdy chcemy chronić nasz kodzik domenowy przed zmianami, natomiast nie sprawdzi się zupełnie jeśli piszemy prostego CRUD’a. Wtedy narobimy sobie abstrakcji oraz niepotrzebnych warstw, które nie przyniosą nam żadnej korzyści.

Jeśli z całego artykułu mielibyście zapamiętać jedną rzecz to chciałbym, żeby to było to: wymagania biznesowe są najważniejsze, a one zależą od kontekstu w którym się znajdujemy.

Podobnie jest z technologią. Nie jesteśmy w stanie dobrać odpowiedniej technologii bez znajomości kontekstu. Przykładowo być może nie będziemy mogli użyć PostgreSQL ze względu na politykę firmy, ponieważ wspierają tylko MongoDB. Być może nie będziemy mogli użyć rabbita, ponieważ developerzy pracujący nad aplikacją go nie znają.

Niech pierwszy rzuci kamieniem ten, który przyszedł do nowego projektu i od razu nie stwierdził, że kod oraz architektura są bez sensu i pewnie pisali ją jacyś głupi ludzie bez wykształcenia. Ale czy ktokolwiek zastanowił się dlaczego projekty wygląda jak wygląda? Być może jest to PoC lub terminy narzucone przez inwestorów były bardzo napięte i trzeba było dowieźć funkcjonalności w jakikolwiek sposób po to aby firma mogła przetrwać. Być może tak jak w przykładzie z kantorem kryptowalutowym jakiś regulator wymagał wprowadzenia poszczególnych rzeczy w szybkim tempie lub może projekt został odziedziczony po innej firmie.

Więc zanim zaczniemy kogoś osądzać za podjęte decyzje to najpierw poznajmy kontekst w jakim się znajdujemy.

W realnym świecie nie jest tak kolorowo

Bardzo rzadko trafiają nam się do napisania aplikacje, które w całości muszą powstać w dwa tygodnie, albo będą żyć tylko przez kilka miesięcy. Które w całości muszą być mega wydajne, testowalne, audytowalne i dobrze zabezpieczone. Z reguły jest tak, że tylko w niewielkiej części naszej aplikacji poszczególne cechy lub technologie przyniosą nam jakieś korzyści. W innych miejscach spowodują tylko dodatkowy narzut pracy, a nawet wprowadzą problemy.

Aplikacje nad którymi pracujemy z reguły nie posiadają jednego kontekstu ale wiele. Wyobraźmy sobie, że piszemy aplikację do zarządzania software housem. Przecież w software housie nie mamy jednego kontekstu o nazwie „Software house”. W takowej firmie znajduje się kilka pod kontekstów tzw. subdomen. Dla uproszczenia na razie możemy uznać, że jeden kontekst jest równy jednej subdomenie. W kolejnym artykule wyjaśnię dlaczego to niekoniecznie musi być prawdą, ale na razie dla uproszczenia uznajmy, że tak jest.

W software housie znajduje się subdomena finansów, delivery, sprzedaży itp. Każda z tych subdomen będzie miała inne wymagania biznesowe i w każdej inne cechy oraz technologie przyniosą inne korzyści. Czy dostępność na poziomie 99.9999% w domenie finansów będzie kluczowa? Prawdopodobnie nie. Jeśli nie będziemy mogli wystawić faktury przez kilka sekund lub minut to nic wielkiego się nie stanie. Czy wprowadzenie architektury heksagonalnej i zapewnienie wysokiej czytelności w domenie sprzedaży będzie potrzebne? Również prawdopodobnie, nie. To jest software house, a nie agencja reklamowa więc w tym miejscu prawdopodobnie będziemy mogli użyć gotowca.

Więc jak widzicie znajomość biznesu (domeny) oraz jego części (subdomen) jest kluczowa do tego aby dobrać odpowiednie rozwiązania. Bez tego nie jesteśmy w stanie zrobić dobrej aplikacji. Teraz pewnie rozumiesz dlaczego większość dobrych programistów, konsultantów i trenerów na większość pytań odpowiada „to zależy”.

Ale co to znaczy, że coś zależy od kontekstu?

Powiedzenie, że tutaj użyłem architektury heksagonalnej bo tę część systemu robiłem dla subdomeny finansów nic nam nie daje. W jednej firmie na przykład biurze rachunkowym, subdomena finansów będzie najważniejszą subdomeną (core domain) i to do niej będziemy wrzucać najlepszych ludzi oraz zapewniać czytelność, testowalność itp. Ale przecież w przypadku software house nie jest ona czymś dzięki czemu biznes zarabia pieniądze.

W software house główną subdomeną (core domain) będzie natomiast delivery. To tam skupia się wartość biznesowa. Subdomena finansów będzie tylko domeną pomocniczą i prawdopodobnie będziemy mogli użyć jakiegoś gotowca do wystawiania faktur.

Idąc dalej, nawet jeśli będziemy robić aplikację dla dwóch różnych software house’ów to wymagania mogą być zupełnie inne. Jeden software house może mieć nieograniczony budżet i zasoby, natomiast drugi musi dowieźć aplikację w jak najszybszym tempie bo nie jest w stanie kontrolować swojego biznesu i wpada w długi. Jeden software house może specjalizować się w javie i to będzie preferowana technologia, natomiast drugi może się specjalizować w C#.

Więc jak widzisz nawet jeśli konteksty na pierwszy rzut oka wydają się bardzo podobne to jednak mogą się istotnie różnić. A więc aby określić jakiej technologii użyć, jakiego zespołu będziemy potrzebować oraz jakie cechy musimy zapewnić naszej aplikacji musimy mieć jakieś wskaźniki oraz metryki czyli tzw. drivery architektoniczne.

Czym są drivery architektoniczne?

Drivery architektoniczne są to metryki, które determinują nam to jakiego rozwiązania użyjemy. Drivery architektoniczne możemy podzielić na kilka kategorii:

Wymagania funkcjonalne

Jest to lista funkcji, które musi zapewniać nasz system. W przypadku aplikacji dla software house’u takim wymaganiem funkcjonalnym może być to, że czas zalogowany przez deweloperów musi być później widoczny dla menadżerów. Dzięki takiemu wymaganiu wiemy, że musimy użyć jakiegoś środka trwałego zapisu na przykład bazy danych.

Atrybuty jakościowe

Są to nasze „cechy”. Czyli na przykład skalowalność, czytelność, testowalność, wydajność, audytowalność itp. Warto na nie szczególnie zwrócić uwagę, ponieważ ich dodanie może być bardzo ciężkie w momencie kiedy mamy już stworzoną aplikację.

Ograniczenia projektowe

Pamiętacie jak mówiłem Wam o ograniczonym czasie, budżecie, braku znajomości danej technologii czy konieczności użycia postgres’a? Są to właśnie ograniczenia projektowe, czyli rzeczy, które „przeszkadzają” nam w zrobieniu aplikacji i do których musimy się dostosować. Do tego grona zalicza się również polityka firmy. Przykładowo nasza firma może mieć wykupioną licencję na oracle i to właśnie tej bazy musimy użyć. Możemy również posiadać tylko programistów COBOL’a przez co nie będziemy w stanie w rozsądnym czasie napisać aplikacji w C#. To również jest ograniczenie.

Konwencje

Przypomnijcie sobie kłócących się programistów. Każdy z nich miał jakieś swoje upodobania i przekonania. Podobnie sytuacja wygląda na nieco wyższym poziomie całego zespołu lub firmy. Być może firma dla której pracujemy ma swoje zasady odnośnie doboru architektury oraz technologii, wielkości zespołów czy sposobu zarządzania projektem. One również wpływają na to jakiego rozwiązania finalnie zdecydujemy się użyć.

Cele projektowe

Jest to ostatnia kategoria driverów architektonicznych. Cele projektowe są to cele biznesowe. Przykładowo w aplikacji do obsługi konkursu celem było stworzenie aplikacji, która miała żyć tylko przez czas konkursu więc dobór architektury czy zapewnienie niektórych cech nie było aż tak istotne. Podobnie sytuacja ma się w momencie kiedy piszemy PoC. Przykładowo nie musimy w nim korzystać z asynchronicznych kolejek lub bazy danych. Całość możemy zrobić „in memory”. PoC nie jest finalnym rozwiązaniem.

Dlaczego drivery architektoniczne są ważne?

Jak już pewnie wiesz pozwalają nam one spełnić wszystkie założenia biznesowe, których oczekuje klient. Dzięki nim poznajemy również potrzeby jakimi się kierował i jakie musi zaspokoić nasz produkt. Znacząco oszczędzają czas potrzebny na wdrożenie, a co za tym idzie redukują koszta. Pomagają nam również zawęzić wybór docelowego rozwiązania.

Jak poznać drivery architektoniczne?

Aby poznać drivery architektoniczne należy pytać. Ale pytać o co? No właśnie! Aby wiedzieć o co pytać musimy poznać biznes dla którego tworzymy aplikację. Najlepszym źródłem informacji odnośnie driverów architektonicznych i samego biznesu są nasi interesariusze. To od nich możemy zdobyć potrzebne wymagania.

Natomiast spytanie wprost „Czy nasz system powinien być skalowalny, dostępny, odporny na błędy czy spójny?” jest skazane na porażkę. W 99% przypadków odpowiedź to będzie „tak”, pomimo, że zapewnienie wszystkich cech jest technicznie niemożliwe. W takich przypadkach większość developerów się irytuje i myśli sobie „no co za mało inteligentny człowiek, jak system może być skalowalny, dostępny i spójny jednocześnie…”, a następnie mówi „klient nie wie czego chce”. Natomiast klient doskonale wie czego chce, ale prawdopodobnie nie rozumie co właśnie do niego powiedzieliśmy naszym technicznym językiem.

Z klientem jest jak z wizytą u mechanika. Kiedy mechanik się nas pyta co się dzieje to prawdopodobnie nie mówimy mu: „pierścienie na trzecim tłoku są do wymiany oraz uszczelka pod głowicą”. Raczej mówimy coś w stylu: „no bo jak jadę autem to robi tak buuuuuu tyk, tyk, tyk, a wcześniej tak nie robił i dodatkowo słychać takie piiiiii z tylnego koła”. Czy to znaczy, że nie jesteśmy wystarczająco elokwentni bo nie umiemy powiedzieć co dokładnie jest do wymiany i czego oczekujemy? Nie, od tego właśnie jest mechanik. My staramy się jak najlepiej zobrazować czego potrzebujemy, natomiast to mechanik musi od nas wyciągnąć potrzebne informacje i na ich podstawie dostarczyć nam wartość.

Mechanik nie kłóci się z nami, że nie wiemy czego chcemy tylko zadaje dodatkowe pytania w stylu: „piszczy tylko przy przyspieszaniu, czy również hamowaniu?”, „czy tyka tylko po odpaleniu auta, czy po dłuższej trasie też?”.

My jako deweloperzy również powinniśmy tak robić. Jeśli klient nie potrafi wprost powiedzieć czego oczekuje, tylko rzuca jakimiś porównaniami albo na otwarte pytania odpowiada „tak” lub „nie” to powinniśmy przyjąć inną strategię zbierania wymagań. To jakie informacje uda nam się pozyskać bardzo zależy od naszych umiejętności miękkich. Jest to rzecz, która moim zdaniem jest kluczowa do zostania dobrym programistą. Jeśli nie posiadamy umiejętności miękkich, a szczególnie komunikacyjnych to nie jesteśmy w stanie zrobić dobrej aplikacji.

Należy również pamiętać, że nie wszystkie drivery architektoniczne dotyczą całego systemu i większość z nich dotyczy tylko pojedynczego kontekstu. Przykładowo w naszej aplikacji do zarządzania SH dostępność oraz odporność na błędy nie były wymagane w domenie finansów, natomiast mogą okazać się kluczowe dla sprzedaży. Jeśli podczas prezentacji dla kluczowego klienta nasz system by się wysypał to nie wyglądałoby to najlepiej i mogłoby skutkować utratą dużych pieniędzy. Drivery powinniśmy rozpatrywać indywidualnie dla każdego kontekstu.

Co więcej przykładowo zapewnienie bezpieczeństwa oraz spójności na najwyższym poziomie w całym systemie byłoby bardzo kosztowne i czasochłonne, a być może przyniosłoby tylko wartość w jednej części, a w innych wprowadziło niepotrzebny kłopot.

Drivery mogą się również zmieniać w czasie. Na początku istnienia firmy najważniejszym driverem mógłby być ograniczony czas oraz budżet. Żeby dostać pieniądze od inwestorów musimy dowozić rzeczy we wcześniej obiecanym terminie. Natomiast później jeśli firma złapałaby już trakcję oraz klientów to mogłoby się okazać, że ważniejsze jest teraz zapewnienie dostępności i skalowalności, ponieważ bez tego nasz system nie jest w stanie poprawnie funkcjonować.

Sposoby zbierania informacji oraz umiejętności miękkie to temat na zupełnie osobny artykuł, ale podam Wam kilka pomocnych technik:

  • Event storming
  • Product vision board
  • User stories
  • Domain Storytelling
  • Value Steam Mapping
  • Cynefin

Dokumentacja

Tak jak wcześniej wspomniałem nie dość, że drivery architektoniczne z reguły są per kontekst, a nie cały system to jeszcze zmieniają się w czasie. Nie sposób ich zapamiętać, szczególnie jeśli nad aplikacją pracujemy kilka lat i w międzyczasie zespół zdążył zmienić się kilkukrotnie. Aby wiedza, która została zebrana, a wręcz bohatersko wyszarpana od ludzi biznesu nie została zapomniana oraz żeby nowi developerzy nie mówili „kto Was uczył?! kto Was robił?!” to warto tę wiedzę udokumentować.

Sposobem na dokumentowanie driverów architektonicznych oraz wybranych rozwiązań jest tzw. ADR – Architecture Decision Record. Jest to bardzo prosty plik z następującą strukturą:

Tytuł: Krótki opis czego dotyczyła decyzja?
Data: Czas podjęcia decyzji

Kontekst:
Znane drivery architektoniczne w tym krótki opis sytuacji panującej w firmie oraz priorytetów

Decyzja:
Wybrane rozwiązanie oraz uzasadnienie

Konsekwencje:
Skutki, które powoduje wybrana decyzja

Inne możliwe rozwiązania:
Alternatywy. Zawsze warto wypisać kilka alternatyw z uzasadnieniem dlaczego nie zostały wybrane. Znacznie ograniczy to czas potrzebny na główkowanie, dlaczego nie wybraliśmy rozwiązania X zamiast Y. 

Dobrą praktyką jest aby każdy plik dokumentował tylko jedną decyzję architektoniczną i był niemutowalny. Jeśli chcemy zmienić wcześniej podjętą decyzję to powinniśmy utworzyć nowy plik. Ułatwi to późniejsze wyszukiwanie oraz śledzenie historii zmian aplikacji. Takie pliki najfajniej jest trzymać w repozytorium, blisko kodu. Ułatwia to ich tworzenie, wyszukiwanie oraz znacznie zwiększa ich dostępność. Dodatkowo wtedy w kodzie można robić bardzo łatwe odniesienia do ADR.

Zbiór ADR to tzw. ADL – Architecture Decision Log.

Finalnie to czy nasze decyzje będziemy trzymać w repozytorium, czy zewnętrznym systemie nie ma dużego znaczenia. Ważne jest to aby każdy członek zespołu miał do nich dostęp oraz żeby łatwo można było je wyszukać oraz śledzić ich historię.

Podsumowanie

Kluczem do napisania „dobrego kodziku” i stworzenia „dobrej aplikacji” jest poznanie kontekstu oraz driverów architektonicznych, które go dotyczą. Warto sobie uzmysłowić, że technologia to tylko narzędzie, którym posługujemy się aby spełnić jakiś cel biznesowy.

Część z Was pewnie pomyśli, ale ja nie jestem architektem, nie rozmawiam z biznesem więc na co mi to? Prawda jest taka, że ta wiedza przydaje się wszędzie. Niezależnie od poziomu na którym się znajdujemy. Znajomość kontekstu jest ważna również w życiu codziennym. Nawet jako zwykły developer znając kontekst i drivery jesteśmy w stanie lepiej dobrać technologię, użyć wzorców, które dadzą nam więcej korzyści lub uniknąć zbędnego pisania testów w kontekstach w których nie są one potrzebne.

Na przykład mi osobiście taka znajomość zaoszczędziła robienia refactoru w module, którego nikt nie zmieniał od 6 lat. No bo przecież po co poprawiać kod, którego nikt nie ma zamiaru modyfikować. Wam może to zaoszczędzić wielu niepotrzebnych konfliktów z biznesem oraz innymi developerami, szczególnie na forach. Pisania niepotrzebnego kodu w celu zrobienia architektury heksagonalnej w aplikacji typu CRUD lub niedostarczenia funkcjonalności w terminie ze względu na przekombinowanie architektoniczne.

A więc zanim uznamy, że kodzik/technologia/podejście jest bez sensu lub, że praktyki dobrego programowania powinny być przestrzegane wszędzie warto najpierw poznać konteks w którym się znajdujemy 🙂

Źródła:
https://edu.devstyle.pl/products/droga-nowoczesnego-architekta
https://leanpub.com/software-architecture-for-developers