logo

Spacerujące szkielety 💀

Długość wpisu: 8 min

Mantra Kenta Becka “Make it work. Make it right. Make it fast.” będzie najprawdopodobniej cytowana przez programistów nawet za sto lat.

To podejście mówi o tym, żeby zaczynać od najważniejszej rzeczy, czyli od czegoś działającego, a potem to ulepszać.

Zanim jednak sprawimy, że coś zacznie działać, warto zweryfikować, czy nasz pomysł, ma w ogóle sens i czy dopasuje się do tego, co już mamy zbudowane.

Wiele razy łapałem się na tym, że fragment kodu, nad którym spędziłem dużo czasu i ugłaskałem, jak tylko się dało, nie pasował potem do kolejnych partii kodu. W takich sytuacjach zaorać to, co już miałem, i przemyśleć implementację jeszcze raz.

Dobrze dobrane nazwy, odpowiednie warstwy abstrakcji, powydzielane funkcje i zastosowane wzorce projektowe nie ratowały przed tym, że kod nie pasował do tego, co trzeba było zakodzić, albo przed tym, że klient zmienił zdanie po zobaczeniu fragmentu funkcjonalności na demo.

Z tym problemem i różnymi jego wcieleniami muszą radzić sobie programiści w każdej z gałęzi IT.

Spacerujący szkielet

Jeden ze sposobów, które pomagają zapobiec takim sytuacjom i jednocześnie wpisują się w mantrę Kenta Becka, jest “Walking Skeleton”.

Cytując https://wiki.c2.com/?WalkingSkeleton:

“A Walking Skeleton is a tiny implementation of the system that performs a small end-to-end function. It need not use the final architecture, but it should link together the main architectural components. The architecture and the functionality can then evolve in parallel.”

Celem takiego spacerującego/chodzącego szkieletu jest szybka weryfikacja pomysłu, sprawdzenie jak łączy się z tym, co już mamy zaimplementowane i postawienie fundamentów pod bardziej szczegółową implementację.

W ebooku, który możesz pobrać tutaj, podaję taki przykład:

Załóżmy, że implementujesz wyszukiwarkę w aplikacji webowej, która filtruje dane w tabelce.

  1. Zaimplementuj proste pudełko, do którego można wpisać tekst. Zapomnij o stylowaniu, sanityzacji inputu, walidacji i wszelkiego rodzaju wodotryskach.
  2. Zaimplementuj najprostsze filtrowanie, które wyświetli w tabelce oczekiwany rezultat po wpisaniu tekstu i jednocześnie połączy ze sobą wszystkie potrzebne komponenty w kodzie.
  3. Jeśli działa, to masz dowód na to, że Twoje podejście do implementacji jest ok i przy okazji masz spore pojęcie o tym, jak łączy się z obecnym kodem.
  4. Pora, żeby zrobić komita i dodawać kolejne bajery.

Przykładów jest więcej:

W dokumentacji Reacta, taki chodzący szkielet to mock - statyczna wersja fragmentu aplikacji.

W grach są to takzwane gray boxy, które pozwalają ograć fragmenty lub całe etapy. Pozwalają przetestować “flow”, zanim zostaną uzupełnione o tekstury, motion capture i wszystkie inne bajery.

Przykład tutaj i tutaj.

W rozwiązaniach devopsowych może to być postawienie całego continuous delivery pipeline przy wykorzystaniu prostego serwera.

W biznesie często jest to rozwiazanie no-code, które pozwala zweryfikować pomysł biznesowy i potem jest zastępowane (lub też nie) uszytą na miarę implementacją przy użyciu kodu.

Na poziomie koncepcyjnym, może to być też np. coś takiego jak outline artykułu, który potem został wypełniony treścią, czyli właściwą implementacją.

Spacerujący szkielet vs prototyp

W książce The Pragmatic Programmer wspomniane są dwie koncepcje, które pasują do definicji spacerującego szkieletu:

(1) Tracer Code: fragment implementacji, który ma na celu szybki feedback, sprawdzenie, czy to, co tworzymy, ma sens i jak wpasowuje się w całość aplikacji. Tracer code ma za zadanie postawienia fundamentów pod następne fragmenty kodu. Kolejne części będą do niego doklejane.

Technicznie możemy zobrazować to jako wertykalny przekrój przez aplikację: kawałek frontendu, backendu i bazy danych.

Panowie Hunt i Thomas wykorzystują tu analogię do Tracer Bullets, czyli amunicji smugowej:

Z Wikipedii:

Amunicja smugowa zawiera pocisk smugowy – czyli taki, który zawiera element zwany smugaczem (traser, od ang. tracer). Smugacz zapalany jest przez gazy prochowe w przewodzie lufy.

Spalający się smugacz pozostawia dobrze widoczny ślad wizualny w postaci smugi dymu lub jasnego światła. Ułatwia to korygowanie ognia i może poprawiać jego celność.

Jest to zwykły nabój, który ma dodatkową funkcję wskazywania lotu pocisku.

Tracer Bullets

(2) Prototypes: fragment implementacji, który również ma na celu szybki feedback, ale sprawdza tylko jakiś konkretny aspekt lub wycinek aplikacji. Prototyp jest bardzo uproszczony, nie pokazuje, jak zachowuje się całość aplikacji i najczęściej jest na tyle prosty, że po prostu się go wyrzuca i przepisuje od nowa.

Technicznie prototyp jest tutaj fragmentem rozwiązania, który nie musi mieć bazy, backendu, lub [wytnij sobie dowolną warstwę aplikacji].

Muszę przyznać, że rozdziały o tych dwóch koncepcjach czytałem po 5 razy, żeby zrozumieć różnicę.

Puryści, którzy lubią się kłócić o detale, pewnie przyrównywaliby Walking Skeleton do Tracer Bullets i mieli by rację.

W praktyce granica między prototypem a chodzącym szkieletem jest bardzo cienka i często te dwa pojęcia są po prostu nazywane chodzącym szkieletem, bez wnikania w szczegóły.

W dobie iteracyjnego dostarczania oprogramowania i narzędzi takich jak kreatory stron, mikroserwisy, chmura, czy no-code, granica między prototypem, a “właściwą impementacją” zaciera się jeszcze bardziej.

Jednocześnie potrzeba sporo dziecięcej naiwności, żeby wierzyć, że prototyp (zgodnie z definicją) zostanie wyrzucony.

Praca w IT nauczyła mnie, że zdecydowanie zbyt często prototypy i wszelkiego rodzaju proof of concept, które zostały tworzone z myślą o tym, że zostaną zastąpione “właściwą implementacją” zostają z nami znacznie dłużej, niż byśmy chcieli i działają miesiącami wdrożone na systemach produkcyjncyh.

Z tych powodów nie ma sensu marnować czasu na dyskusje o różnicach między prototypem i chodzącym szkieletem.

Osobiście wolę patrzeć na to jak na chodzące szkielety różnej wielkości:

Małe: modelujące mniejszą część aplikacji - moduł, klasę, metodę.

Większe: modelujące większą część aplikacji - zapis danych do bazy po kliknięciu przycisku na UI, architekturę aplikacji, architekturę komunikujących się ze sobą serwisów.

Każdy z nich ma za zadanie udowodnić, że nasz pomysł ma sens i jakiś fragment systemu zadziała poprawnie od początku do końca. Jakiej wielkości jest to fragment i jaką ma pełnić funkcję, zależy już od nas.

Do tej definicji będzie pasował więc też diagram narysowany na tablicy lub pseudokod w komentarzach, który potem zasąpujemy właściwą implementacją.

W obu przypadkach możesz “przejechać palcem po mapie”, wytłumaczyć każdy krok i udowodnić tym samym, że “tiny (fake) implementation” “performs a small end-to-end function” i że to wszystko ma ręce i nogi.

Przy okazji jest to podejście, które bardzo lubię:

  • narysować szkic na tablicy lub w notesie
  • zweryfikować, że ma sens
  • powtórzyć to samo przy użyciu kodu
  • zweryfikować, że ma sens
  • zaimplementować pozostałe funkcjonalności, upieknić kod, zrobić refactoring i ruszać dalej

Każdy z tych kroków pcha nas do przodu, a przy okazji zyskujemy wiele korzyści.

Korzyści

Korzyści będą zależeć od wielkości szkieletu oraz od tego, co chcemy nim zweryfikować i jest ich całkiem sporo.

Eliminuje problem “pustej kartki papieru”

Szkielet dostarcza strukturę, którą szybciej możemy wypełniać szczegółami.

Nie wiem, czy też tak masz, ale gdy podczas pracy nad interfejsem użytkownika mam przed oczami eleganckie mockupy, to znacznie szybciej przenoszę kolejne funkcjonalności na kod i wypełniam je logiką.

Nie muszę tworzyć skomplikowanej mentalnej reprezentacji, bo mam ją przed oczami.

To jest dobry przykład chodzącego szkieletu na wysokim poziomie.

Poprawa pracę w zespole

Szkiele zarysowuje interfejsy i granice modułów.

Przykładem interfejsu może być kształt danych, a granicy modułów endpointy, których potrzebujesz.

Dzięki temu dodatkowe osoby mogą wskoczyć i pomagać z kodzeniem, bo widzą mniej więcej, do czego zmierza całość.

Gdy stosuję takie podejście, to jedna osoba może pracować na frontendem, druga nad backendem, trzecia nad bazą danych i gdy mergujemy zmiany, to rzadko zdarzają się męczące konflikty.

Możemy też znacznie szybicej zacząć integrować różne części kodu ze sobą.

Pilnuje nas przez zbyt wczesnym wejściem w szczegóły

Szczegóły i detale implementacyjne mogą okazać się zbędne, gdy zaczniemy kodzić kolejne elementy.

To jest ten problem, o którym wspominałem na początku.

Może się okazać, że kolejne części kodu wymuszą inna strukturę albo że po prostu klient zażyczy sobie, że to nie tak miało działać i kod będzie do przepisania.

Zaczynając od ogółu, od szkieletu, minimalizujemy ten problem i otrzymujemy bardziej spójny kodzik. Rzadziej kręcimy się w miejscu.

Pozwala szybciej zobaczyć efekty pracy

Szkielet możesz często pokazać na demach z klientem albo nawet zdeployować użytkownikom, żeby zapoznali się z ficzerem.

Często są to funkcje aplikacji oznaczone jako “eksperymentalne”. Użytkownicy nie mogą po nich oczekiwać stabilności, ale weryfikują założenia.

Bardzo długo reactowe Context API było takim spacerującym szkieletem, aż w końcu zostało zastąpione pełnoprawnym rozwiązaniem, które używamy do teraz.

Pomaga w zrozumieniu, ile rzeczy zostało do zrobienia

Mając ogólny zarys implementacji, minimalizujesz ryzyko sytuacji, w której przez 3 tygodnie mówisz na standupach, że “już prawie skończyłeś i wszystko jest gotowe w 80%“.

Przy okazji masz poczucie, że robisz postępy, a to napędza do dalszego działania.

Pozwala zostawić pracę na jakiś czas

Czasem, gdy zmienią się priorytety lub wymagania, musimy zostawić kod na jakiś czas.

Zaczynając od zaimplementowania czegoś, co działa od początku do końca, znacznie łatwiej jest potem do tego wrócić za tydzień lub dwa. Nie trzeba się dwa dni zastanawiać nad tym, co nam chodziło wtedy po głowie.

Może się też okazać, że sam szkielet jest już na tyle kompletny, że potrzeba tylko godzinki, którą łatwo wynegocjować, żeby dowieźć kod w postaci gotowej do zmergowania albo nawet wypchnięcia na produkcję.

Jest do dla mnie jedna z największych zalet, bo bardzo nie lubię zostawiać rozgrzebanego do połowy zadania. Połączenie spacerującego szkieletu i robienia kiełbaski w kodzie pomaga mi tego uniknąć.

Kodzenie jest przyjemniejsze

To podejście pozwala również na włączenie bardzo przyjemnego trybu hakera, w którym płodzimy hurtowe ilości brzydkiego kodu, a potem jeszcze przyjemniejszego trybu refactorowania i polerowania kodu.

Brzydki kod pokazuje, że dobrze coś przemyśleliśmy, a refactorowanie pozwala zrobić z niego arcydzieło.

Na co uważać?

Trzeba pamiętać, że szkielet jest tylko szkieletem, a to oznacza, że łatwo powyjmować niektóre kości i go rozsypać. Te kości trzeba zabezpieczyć mięskiem.

Stosując to podejście, musiałem pilnować, żeby niekompletna implementacja, bez wymaganej walidacji i obsługi błędów nie została wypchnięta na produkcję. Nie chciałem naprawiać szkód, które mógł wyrządzić taki niekompletny kod.

Gdy pokazujesz takie z pozoru kompletne rozwiązanie, klienci mogą błędnie uznać, że jesteś blisko ukończenia prac. Warto na każdym kroku zaznaczać, że tak nie jest, żeby nie zaszła potrzeba tłumaczenia się później.

Warto też uważać, żeby się zbytnio nie zachwycić swoim szkieletem i nie wmówić samemu sobie, że jesteśmy blisko ukończenia prac.

The first principle is that you must not fool yourself and you are the easiest person to fool. ~Richard Feynmann

Jeśli myślisz, że po napisaniu szkieletu koniec jest blisko, to najprawdopodobniej jesteś w błędzie. Choć istnieje szansa, że wcale nie.

Technikę Walking Skeleton wykorzystuję cały czas i mam wrażenie, że jest jedną z najważniejszych technik w naszej branży. Mam nadzieję, że przyda Ci się tak samo, jak przydaje się mi. 🖖

Dzięki Adam, za zgrabne doprecyzowanie definicji szkieletu i prototypu.

Podziel się:

Instagram, Twitter, YouTube, Facebook, LinkedIn