O tym, że komunikacja odgrywa w każdym projekcie kluczową rolę i przekłada się na jakość oraz terminowość, nie trzeba nikogo przekonywać. Wpływa na to też współpraca, systematyczne spotkania oraz informacje o postępach prac. Za ważne elementy można też uznać wartościowe przekazywanie wiedzy, zbieranie feedbacku oraz informowanie o potencjalnych zagrożeniach, które mogą utrudnić realizację celu. Nie bez znaczenia są także pytania o potrzeby biznesowe, bo te mogą się zmieniać w trakcie trwania projektu. Czy to jednak wszystko? Postanowiliśmy opisać jeszcze kilka innych.

Dostosowywanie do różnych wymagań

W merce.com realizujemy w ramach jednego projektu potrzeby wielu Klientów. Zdarza się, że jedną funkcjonalność projektujemy dla kilku Partnerów, z myślą o używaniu ich przez kolejnych w przyszłości. Naturalnym procesem jest też to, że z czasem dodajemy obsługę dla specyficznego przypadku. Pomaga w tym:

  • Wysokiej jakości kod, który rozdziela na warstwy to, co ma się wykonać, od warunków, które tę akcję inicjują. 
  • Unikanie realizacji logiki biznesowej na zewnętrznych warstwach systemu. 
  • Konsultacje wewnątrz kontekstowe pokazujące, jakie elementy mogą w przyszłości ulec zmianie i na których z nich warto bazować, a w przypadku których zachować większą wątpliwość. 
  • System rozbudowanej konfiguracji, pozwalający decydować o tym, co i kiedy ma się wykonywać. Istotne jest to, że dzięki temu nowi Klienci już na starcie otrzymują większe możliwości co do tego, jak wybrana funkcjonalność ma być wykonywana, a mimo to wciąż mogą zaproponować inne rozwiązanie, którego wdrożenie nie będzie już tak kosztowne. 

W skrócie oznacza to, że dzięki rozdzieleniu reguł od akcji, wyodrębnianiu zasad biznesowych i wskazaniu reguł mniej i bardziej podatnych na zmiany, zyskujemy na elastyczności.

Pojęcia specjalistyczne i uogólnienia

Programowanie znacząco różni się od świata rzeczywistego pod względem tego, w jaki sposób opisywane są znane nam zjawiska, procesy i przedmioty. Programowanie operuje na abstrakcyjnych modelach, zbiorach liter i liczb, które odpowiednio rozumiane i przekształcane skutkują tym, że elementy takie jak: produkt, płatność i wysyłka istnieją i łączą się w proces zakupowy.

Jednakże to, czy wysyłamy coś kurierem, czy też dostarczymy do paczkomatu lub placówki w jednej z wielu galerii, lub też umożliwimy odbiór osobisty, jest bez większego znaczenia — istotne jest, że to tylko kilka z wielu sposobów odpowiedzialnych za przekazanie własności towaru ze sklepu na dostawcę, a następnie na odbiorcę końcowego. 

Idźmy dalej, jakie są produkty? Można wymieniać różny asortyment, ale to trochę zbędny krok dla zespołu programistów. Nawet podział na produkty wirtualne i fizyczne może być zbyt ograniczającym, zbyt odległym od abstrakcyjnego określenia czegoś jako produkt. Jeśli jest coś, co można jakoś opisać, wrzucić do koszyka, zamówić, a następnie odebrać, spełnia funkcję produktu i to wystarczy.

Sposób wysyłki, jaki by nie był, w większości realizowany jest przez ten sam kod, który reaguje na pewne akcje i legitymuje się przede wszystkim stanem odbioru, gdzie aktualnie znajduje się paczka i jaki posiada numer. Podobnie jest z produktem — posiada konkretny identyfikator, jednoznacznie go określający, może być elementem zestawu i być powiązany z innymi produktami. Co to konkretnie jest — to nieistotne, ponieważ opis zawsze można poszerzyć, a ważniejsze jest to, co można z tym zrobić i w jakich warunkach. W programowaniu istotne są właśnie akcje możliwe do wykonania i procesy, które je nadzorują. Istotniejsze jest to, co można z produktem lub wysyłką zrobić, jakie zdarzenia między różnymi elementami systemu zachodzą, niż wiedza o tym jakie dokładnie byty reprezentują.

Dlatego też trzymanie się sztywnych ram: że coś chcemy wysyłać tylko kurierem, podpisujemy umowę tylko z konkretnym dostawcą, sprzedawać chcemy tylko odzież, interesuje nas płatność tylko kartą przez własny formularz — nie jest pomocne. Oczywiście, takie cele można realizować w naszym oprogramowaniu, ale z takimi celami nie piszemy w nim kodu. Jako programiści nie chcemy zagłębiać się w każdym miejscu kodu w takie szczegóły, lecz chcemy szukać wspólnych cech wśród dostawców tej samej usługi, a także dokładnie wiedzieć, jakie interakcje z tymi elementami będą zachodzić. Dlatego też istotne w komunikacji z programistami jest informowanie o tym, jakie procesy chcemy realizować i zadbać o to, że zostały dokładnie omówione.

Przykładem niespójnego używania pojęć jest sytuacja, w której opis modelu programistycznego różni się od modelu, który opisaliby analitycy. Różnice mogą wynikać z paru przyczyn: 

  • model opisujący projekt jest w którymś z zespołów zbyt uogólniony lub różni się w paru kwestiach,
  • jeden z zespołów źle zrozumiał lub niepoprawnie opisał założenia projektowe,
  • programiści realizujący zadanie posłużyli się w kodzie nietrafionymi nazwami.

Taki stan rzeczy sprawia, że rozwijanie projektu jest trudne i wymaga ciągłych sprostowań.

Konkluzją jest to, że jesteśmy elastyczni wobec operatora płatności i wysyłki oraz umożliwiamy sprzedaż dowolnych produktów. Te elementy mogą zmieniać się w czasie, np. istniejące już w systemie produkty można w dowolnym czasie zacząć wysyłać na nowe sposoby, a istniejące już zamówienia pozwolić na opłacenie nową metodą płatności. Co więcej, takim podejściem, w którym przywiązujemy uwagę do różnego sposobu wykonywania akcji, możemy spełnić wytyczne, które w przyszłości dopiero będą dla nas wyznaczone. Przykładem mogą być nowe zdarzenia i interakcje dotyczące produktów, przekazywania wysyłek czy generowania raportów i faktur.

Schematy postępowania i hierarchia wykonywania akcji

Bywa, że nagminne trzymanie się schematu prowadzi do niemożności zmian i zatrzymania jego dalszego rozwoju oprogramowania. Świat IT zna wiele takich przypadków, na mniejszą lub większą skalę. Schematy objawiają się wtedy, gdy niepotrzebnie łączymy pewne rzeczy lub nadajemy im zbyt oczywiste znaczenie, pomijając etap na ich lepsze zrozumienie. Dzieje się tak również wtedy, gdy uzależniamy je w sposób jednokierunkowy, bo w konkretnym badanym zastosowaniu tak jest od nas wymagane. A co, jeśli zechcemy zmienić to, bo przy nieco innych założeniach biznesowych lub przy innej specyfice produktowej, taka zmiana będzie korzystniejsza lub co gorsza — niezbędna?

Przykładowo — inny sposób dostawy przedmiotów dla wirtualnych i fizycznych, usługi wykonywane na miejscu, opłacane po wykonaniu i z niesprecyzowaną kwotą do momentu wykonania. Pomijanie płatności za przedmioty gratisowe, które lojalny klient może wybierać z puli tak, jakby zwyczajowo przeglądał jedną z kategorii produktów. Te przykłady można nazwać wyjątkowymi sytuacjami, lecz to właśnie obsługa tych wyjątkowych sytuacji decyduje o sile oprogramowania i o wachlarzu jego możliwości. Te przypadki sprawiają też, że pewne akcje niekoniecznie powinny być ze sobą złączone w specyficzną kolejność — istotne jest, aby takich połączeń nawet nie tyle nie tworzyć w tych kluczowych miejscach, ile pójść o krok dalej i nadać możliwość sterowania ich kolejnością.

Zbyt abstrakcyjne obszary

Powyższe akapity eksponują wartość abstrakcyjnej, uogólnionej formuły kodu i prezentują jako najlepszą drogę. Potrzebny jest jednak złoty środek. Rozwiązaniem jest tworzenie tych obszarów abstrakcji, z których faktycznie zamierzamy skorzystać. Pisanie rozległych metod na zapas, pozwalających na różnorodne wykorzystywanie zasobów jest zbędne, ale porzucanie przy tym wzorców projektowych nie jest już tak zasadne. 

Przykładowo — wywoływanie się kilku usług zewnętrznych bezpośrednio po sobie, bez nadzorowania tej kolejności i weryfikacji zmian zachodzących w każdym z etapów, skutkuje nie tylko prostym kodem o małym poziomie abstrakcji, ale świadczy też o dużej ufności i uzależnieniu się od usług zewnętrznych oraz o niemożności dobrego prześledzenia zmian w danych.

Sytuacją odwrotną z kolei może być działanie, w którym ważniejsza warstwa jest modelowana pod mniej istotną. Ta słabsza może posiadać już jakąś część funkcjonalności, ale jest w ciągłym rozwoju, a nawet planowaniu, lecz już ten mglisty projekt staje się wyznacznikiem rozwoju modułu istotniejszego. Prócz tego, że w takim wypadku narażamy się na łamanie zasady jednokierunkowej zależności, to jeszcze pomysły rozwojowe traktujemy jako pewne źródło wiedzy. W efekcie możemy dostać wysokiej jakości model abstrakcyjny, który przygotowany jest pod scenariusze niezaimplementowane w finalnie powstałej usłudze towarzyszącej.

Konkluzją jest to, że abstrakcja nie może być “cudownym wypełniaczem” na luki implementacyjne lub spełniać plany rozwojowe. Każda linia kodu powinna zawsze mieć możliwość bycia wykorzystaną przez jakiś istniejący już scenariusz testowy. Jednocześnie śledzenie tej ścieżki wykonywania się kodu, powinno pozwolić na wychwycenie krok po kroku zmian, które się odbywają i umożliwiać pominięcie któregoś z kroków lub zastąpienie innym. Ot, złoty środek, który może być ciężki do osiągnięcia, lecz samo dążenie do niego jest już sukcesem.

Negatywne scenariusze

Budzące niepokój sprzeczne komunikaty, akcje wykonane bez odpowiedzi, zamówienia posiadające niezrozumiałe stany i niezrozumiałe liczby lub elementy pojawiające się tam, gdzie się ich nie spodziewano. Tak zachowuje się projekt, który posiada obsługę zbyt małej ilości scenariuszy negatywnych.

O negatywnych scenariuszach ciężko jest dyskutować. Dla biznesu to dyskusja o tym, co przynosi straty, bo zawsze dotyczy niepowodzenia w jakimś obszarze. Dla analityków są to rzeczy, które najczęściej wychodzą na późniejszym etapie projektu, gdy rozpoczęła się już jego realizacja. Dla testerów to z kolei pomocna lista wyjątkowych sytuacji, na podstawie której określają jakość wykonanego przez programistów produktu. Dla programistów zaś są to wielokrotne rozgałęzienia i niejednoznacznie określone przez typ dane, na których muszą w dalszej kolejności budować akcje możliwe do wykonania. Dlatego też czym wcześniej podejmą o nich dyskusje, tym łatwiej przyjdzie się z nimi zmierzyć. A o ile programista jest w stanie wykazać, w którym momencie pojawia się negatywny, błędny przypadek, to sposób dalszego postępowania nie zawsze jest oczywisty — często ma bowiem odzwierciedlenie w decyzjach biznesowych.

Konieczność przygotowania się na różne ewentualności sprawia, że dokładanie w przyszłości kolejnych jest prostsze i czytelne. A to się zdarza i wówczas szybka możliwość reakcji, dopisanie dodatkowego warunku i dodatkowego testu przychodzą łatwiej i sprawniej.

Rozwiązania wspomagające

Posługiwanie się językiem ujednolicającym podobne pojęcia i wykładającym wspólne ich cechy, ułatwia rozmowy z działem technicznym. Pisanie o negatywnych scenariuszach, zwłaszcza jeśli oczekiwane są specyficzne skutki w przypadku niepowodzeń (np. wysyłanie konkretnych mailów, rezerwacji produktów w zapasowych magazynach), pozwala na lepsze zrozumienie programistom biznesowej wartości wdrażanej funkcji i daje pogląd na to, jak istotne są do wykonania konkretne etapy wdrażanej funkcjonalności.

Dobrze prowadzony projekt charakteryzuje się zrozumiałymi opisami, sporządzaniem specyfikacji, komunikowaniem w pojedynczych zadaniach celów, które są oczekiwane do spełnienia. Nie ma alternatywy dla takiego podejścia, gdyż są to cechy efektywnie rozwijanego systemu.

W metodologii Scrum istotna jest wymiana wiedzy, realizowana przez częste i cykliczne spotkania. Częste rozmowy, obfitujące w używanie specyficznych pojęć i opisujące akcje na nich budowane, pozwalają na lepsze zrozumienie modelu biznesowego i spójne realizowanie go za pomocą kodu.

W inżynierii oprogramowania istnieje technika projektowania domenowego (Domain Driven Design, DDD), która polega na wyznaczeniu w ramach projektu lub jego części rygorystycznych pojęć, których znaczenie każdy członek zespołu rozumie tak samo. Istotne jest wówczas to, że te pojęcia powinny mieć ograniczony zasięg występowania, aby uniknąć zbyt wielkiej struktury pojęć, gdyż wtedy ich znaczenia ulegają zniekształceniu. DDD sprzyja unikaniu większości z poruszanych w tym artykule problemów, lecz nie stanowi uniwersalnego rozwiązania. Nie sprawdzi się tam, gdzie pula pojęć jest niewielka, założenia biznesowe są proste i czytelne, a nawet wprowadzi w te obszary niepotrzebny element skomplikowania i złożoność trudną do zmiany.

Z kolei generyczny sposób programowania jest otwarty na rozwój, na dokładanie kolejnych implementacji, które zachowują się w znany już sposób, lecz posiadają inną złożoność wewnętrzną. Im wyższa generyczność, tym większy poziom abstrakcji i złożoność kodu.

Test-Driven-Development to technika tworzenia oprogramowania, która zaczyna proces od szczególnego przypadku i kończy na kodzie spełniającym jego założenia. Pozwala na uniknięcie kodu, który “nic nie robi” i umożliwia implementację wielu negatywnych scenariuszy. Przynosi w zamian negatywne skutki, takie jak rozdrobnienie procesu na wiele składowych oraz nie jest odporna na zmiany w projekcie, gdyż wówczas konieczna jest mocna ingerencja w napisane już połowicznie rozwiązanie.

Czy istnieje idealne rozwiązanie?

Nie istnieje jedno dobre podejście do pisania kodu — jego wybór zależny jest od sytuacji, w której się znajdujemy, a im lepiej zespół developerski ją rozumie, tym łatwiej dokonać dobrego wyboru. Można też w projekcie łączyć kilka rozwiązań ze sobą. Inżynieria oprogramowania oferuje wiele, a zasilana jest dobrej jakości komunikacją i szczegółowym projektem — bez tych rzeczy jest błądzeniem we mgle, a nie dziełem specjalistów.

Artykuł przygotował:
Kamil Baliński