Siedem pytań, które należy rozstrzygnąć przed napisaniem pierwszego wersu artykułu reportażowego
Lista kontrolna przed wdrożeniem obejmująca rzeczywisty problem użytkownika, gwarancje ukończenia pracy, odpowiedzialność za zasady, dane z systemów starszych, próby ponowne i współbieżność działania, możliwość obserwacji oraz bezpieczeństwo wdrożenia.
Naj szybszym sposobem na poczucie produktywności przy nowym zleceniu jest otwarcie edytora i rozpoczęcie pracy: dodanie punktu końcowego, kolumny oraz komponentu. Problem polega na tym, że pierwsza implementacja w tle odpowiada na każde pytanie, którego nikt nie zadał, a te odpowiedzi przekształcają się w schematy, umowy API oraz testy, których modyfikacja jest kosztowna. Ten przewodnik omawia siedem pytań, na które doświadczeni inżynierowie odpowiadają przed rozpoczęciem kodowania, co się dzieje, gdy je pomijają, oraz jak zapewnić, by praca pozostała umiarkowana i przyspieszyła realizację zamiast ją spowalniać.
Dlaczego pierwsza implementacja ma tak wielkie znaczenie
Gdy przychodzi żądanie, rozpoczęcie od kodu sprawia, że abstrakcyjne zadanie wydaje się konkretniejsze i mniejsze. Jednak otwarte pytania nie zniknęły. Ktoś nadal musi zdecydować, kto jest odpowiedzialny za regułę biznesową, co oznacza „zakończenie” operacji wieloetapowej oraz co stanie się z rekordami utworzonymi przed zmianą. Jeśli nikt tego nie decyduje, kod podejmuje tę decyzję przypadkowo.
Ta przypadkowa decyzja rzadko ogranicza się do jednego miejsca. Kształt pierwszej wersji zazwyczaj staje się układem tabeli, formatem odpowiedzi, wspólnym narzędziem, które wszyscy importują, oraz modelem stanu, od którego zależy interfejs użytkownika. Gdy inny kod go wywołuje, a dane produkcyjne są do niego zgodne, zmiana kursu oznacza konieczność migracji, dodawania elementów kompatybilnościowych oraz koordynowanych wypuszczeń. Spędzenie godziny na odpowiednich pytaniach od samego początku jest w porównaniu z tym niewielkim kosztem.
Z zewnątrz może to wyglądać na wahanie: czytanie istniejących procesów pracy, pytanie o to, czego użytkownik tak naprawdę chce osiągnąć, sprawdzanie zachowania starych danych, omawianie częściowych niepowodzeń. W praktyce jest to ten sam proces rozwiązywania problemów, który kod i tak musiałby przeprowadzić, tylko że odbywa się on wtedy, gdy zmiana zdania nie kosztuje zbyt wiele.
1. Oddziel żądany funkcjonalność od podstawowego problemu
Często zgłoszenia przychodzą w postaci rozwiązań: dodanie przycisku, filtru, funkcji eksportu lub nowego statusu. Żądanie może być całkowicie uzasadnione, ale opisuje to, co ktoś wyobraża sobie jako rozwiązanie, a nie frustrację lub efekt biznesowy stojący za nim.
Uzyskanie świadomości tej podstawowej potrzeby zmienia to, co tworzymy. Prośba o eksport w formacie CSV może pochodzić od menedżerów, którzy nie potrafią porównywać tygodniowych danych pomiędzy działami. Eksport pomaga, ale jednocześnie generuje powtarzające się ręczne prace z arkuszy kalkulacyjnych, których można całkowicie uniknąć dzięki zapisanemu raportowi lub zaplanowanemu podsumowaniu. Prośba o dodatkową wartość statusu może ujawnić, że jedno pole jest już przeciążone i pełni jednocześnie funkcje płatności, zatwierdzenia oraz realizacji zamówienia. Dodanie tej wartości domyka sprawę, ale jednocześnie utrudnia zrozumienie modelu danych.
To wcale nie oznacza konieczności badania każdej małej prośby ani przekształcania drobnej zmiany w szeroko zakrojoną pracę nad produktem. Celem jest poznanie wystarczająco dużo informacji o sytuacji użytkownika, aby ocenić, czy proponowana zmiana rzeczywiście poprawi wynik. Zazwyczaj wystarczy kilka precyzyjnych pytań:
- Kto nie może dziś dokończyć swojej pracy?
Pomijając ten krok, inżynierowie naturalnie optymalizują dokładnie to, o co proszono. Przycisk jest prosty w użyciu, komponent można wykorzystać wielokrotnie, API jest uporządkowane, a mimo to funkcja zawodzi, ponieważ rozwiązuje tylko konkretny problem, a nie całą sytuację. Chodzi nie o opóźnianie pisania kodu, lecz o upewnienie się, że kod jest właściwą odpowiedzią.
2. Określ, co oznacza sukces w całej operacji
Wiele wymagań opisuje konkretne działanie, ale nie określa, kiedy jest ono zakończone. Składanie aplikacji, zatwierdzanie rekordu, kopiowanie projektu czy synchronizacja danych wydają się proste, dopóki nie są zaangażowane kilka kroków i jeden z nich się nie powiedzie.
Zastosuj proces zatwierdzania, który zachowuje zmianę, zapisuje wiersz w dzienniku audytu, wysyła zdarzenie i powiadamia osobę, która o to poprosiła. Jeśli zapis się powiedzie, a powiadomienie nie, czy oznacza to sukces zatwierdzenia? Jeśli użytkownik spróbuje ponownie, czy rekord może zostać zatwierdzony dwukrotnie? Jeśli wpis do dziennika audytu nie może zostać zapisany, czy zmiana statusu powinna zostać cofnięta? Jeśli zdarzenie zostanie opóźnione, czy zatwierdzenie jest już zakończone, czy nadal w toku? To nie są błahe kwestie implementacyjne. One określają to, co produkt obiecuje swoim użytkownikom.
Korzystnym ćwiczeniem jest narysowanie granicy zakończenia przed napisaniem procesu pracy:
- Efekty, które muszą się powieść lub nie powieść razem, ponieważ częściowy wynik stanowiłby nieważny stan, powinny należeć do jednej transakcji.
To samo pytanie pojawia się przy małych funkcjach. Gdy ktoś rozpoczyna eksport, czy jest on pomyślny po powstaniu pliku i przyjęciu zadania, czy też link pojawi się później? Gdy interfejs pokazuje „Zapisano”, czy serwer potwierdził trwałe przechowywanie danych, czy zmieniła się tylko lokalna stan?
Zostaw to niejasne, a każda warstwa wymyśli własną definicję. Interfejs użytkownika pokazuje sukces, podczas gdy serwer w tle nadal pracuje; procesor próbuje ponownie coś, w co użytkownik już wierzy, że się nie udało, a system monitoringu zgłasza prawidłową prośbę, mimo że istotny efekt uboczny zniknął. Najpierw ustalenie gwarancji sprawia, że implementacja staje się prostsza, ponieważ każdy krok ma teraz jasne zadanie. To również kształtuje umowę odpowiedzi: API, które zwraca „accepted”, różni się od tego, które zwraca „done”, a klienci muszą wiedzieć, co otrzymują.
3. Zdecyduj, która warstwa odpowiada za każdą decyzję
Funkcja może dawać poprawne wyniki, ale nadal być niebezpieczna, jeśli decyzja znajduje się w niewłaściwej warstwie. Przykłady:
- Frontend ukrywa przycisk przed użytkownikami bez ich zgody, ale API akceptuje tę prośbę, gdy jest wysyłana bezpośrednio.
W każdym przypadku reguła istnieje, ale nie każda ścieżka musi przez nią przechodzić.
Rozwiązaniem jest znalezienie warstwy o wystarczających uprawnieniach do zarządzania tą regułą. Interfejs użytkownika może odzwierciedlać uprawnienia dla ułatwienia korzystania, ale nigdy nie stanowi granicy bezpieczeństwa. Kontroler jest odpowiednim miejscem do weryfikacji struktury żądania HTTP, natomiast reguły biznesowe zazwyczaj muszą znajdować się na głębszym poziomie, aby zadania w tle i wewnętrzne wywołania zachowywały się identycznie. Weryfikacja unikalności na poziomie aplikacji dostarcza przyjaznych błędów, ale tylko ograniczenie w bazie danych faktycznie chroni niezmiennik podczas jednoczesnych zapisów.
Własność dotyczy zarówno stanu, jak i reguł. Filtry, które muszą być udostępnialne i przetrwać odświeżenie, naturalnie pasują do URL. Tymczasowe dane wejściowe należą do formularza. Uprawnienia oraz utrwalony stan pochodzą od serwera, a klient nie powinien tworzyć własnej wersji na podstawie lokalnych domysłów.
Gdy autorytet nie jest jasny, pojawia się problem z koordynacją: powtarzające się sprawdzania na kilku poziomach, kopie tego samego wartości, które muszą być zsynchronizowane, oraz zmiany, które przeradzają się w żmudne poszukiwania i zastępowanie. Aktualizacja polityki wpływa wtedy na interfejs użytkownika, kontroler, usługę, procesor oraz jedną lub dwie zapytania, bez żadnych gwarancji, że każda kopia nadal oznacza to samo. Każdej ważnej decyzji należy przydzielić jednego, wyraźnie określonego odpowiedzialnego. Inne warstwy mogą wyświetlać, przechowywać w pamięci podręcznej, egzekwować lub przekazywać wynik, ale nie powinny go redefiniować. Dzięki temu zmniejsza się zarówno ryzyko bezpieczeństwa, jak i koszty konserwacji, ponieważ wszyscy wiedzą, gdzie znajduje się źródło prawdy.
4. Uwzględnienie już istniejących danych
Dla modelu, który chcemy używać, pisze się nowy kod. Dane produkcyjne jednak zawierają również ślady wszystkich wcześniejszych modeli.
Ustawienie pola jako obowiązkowego jest proste dla rekordów utworzonych po wprowadzeniu zmian, ale tysiące starszych wierszy może tego nie posiadać. Przeprojektowany model stanów może dobrze opisywać przyszłe procesy pracy, pozostawiając jednak starsze rekordy w stanach, których nowy kod już nie rozpoznaje. Nowo ustanowiona zależność obowiązkowa może odnosić się do entity, która po prostu nie istniała w momencie tworzenia starszych wierszy.
Zanim dodasz mechanizmy walidacji lub zmienisz schemat, zastanów się nad następującymi kwestiami:
- Czy istniejące rekordy można prawdziwie przenieść?
- Czy potrzebny jest tymczasowy stan „nieznany”?
- Czy ta zmiana przepisuje historię, czy tylko modyfikuje przyszłe zachowanie?
Słowo „prawda” jest kluczowe. Wypełnianie każdej luki wygodną domyślną wartością może spełnić wymóg NOT NULL, jednocześnie wprowadzając fałszywe dane. Jeśli departament odpowiedzialny za stary rekord nigdy nie został zapisany, oznaczenie go obecnym departamentem upraszcza zapytania, ale sprawia, że raporty historyczne tracą wiarygodność. Czasami uczciwy schemat pozwala na użycie wartości takich jak „nieznane” lub „legacy”, ponieważ brak informacji stanowi rzeczywistą część historii danego rekordu.
Stare formaty istnieją również poza bazą danych. Starsze klienty mogą nadal wysyłać wcześniejsze formaty danych, zadań planowanych może zależeć od wartości stanu, które nowy system chce usunąć, a raporty mogą interpretować kolumny według zasad zmienionych dawno temu.
Nie musisz wiecznie wspierać każdego dawnych zachowań, ale decyzja o migracji powinna być jasna. Niektóre dane można bezpiecznie przekształcić, inne wymagają ręcznej weryfikacji, niektórzy stari klienci zasługują na okno kompatybilności, a inne można celowo wycofać. Ignorowanie tego problemu nie sprawia, że zniknie – pojawia się ponownie w postaci rozproszonych rozwiązań awaryjnych, pól, których wartość może być null i których nikt nie rozumie, nieudanych migracji oraz incydentów serwisowych po wdrożeniu. Wczesna decyzja umożliwia zespołowi spójną transformację zamiast wielu lokalnych domysłów.
5. Załóż, że praca będzie się powtarzać i będzie konkurować
Opisy funkcji zazwyczaj zakładają jednego użytkownika, jeden kliknięcie i prostą sekwencję: żądanie przychodzi raz, w międzyczasie nic innego nie dotyka rekordu, a odpowiedź dociera do klienta. Środowisko produkcyjne nie gwarantuje żadnej z tych rzeczy.
- Użytkownik kliknie ponownie, ponieważ strona wydaje się zablokowana.
Najważniejsze pytanie to to, czy wykonanie tej operacji więcej niż raz oraz jednoczesne jej wykonywanie są bezpieczne. Jeśli powtórzenie nie ma żadnych negatywnych skutków, dodatkowe mechanizmy mogą być niepotrzebne. Jeśli powtórzenie powoduje utworzenie drugiej płatności, zaproszenia, pliku lub rezerwacji towaru, system musi mieć sposób na rozpoznanie, że kilka prób stanowi jedną logiczną akcję.
Narzędzie to obejmuje klucze idempotencji, unikalne ograniczenia, warunkowe aktualizacje, kolumny wersji do blokowania optymistycznego, transakcje oraz tabelę z identyfikatorami przetworzonych wiadomości. To, co się nadaje, zależy od tego, gdzie leży ryzyko. To, co nigdy nie działa, to stwierdzenie „sprawdziliśmy najpierw”, jakby żaden inny proces nie mógł działać pomiędzy sprawdzeniem a zapisem.
Błędy konkurencji są szczególnie niebezpieczne, ponieważ każda z operacji wydaje się poprawna podczas przeglądu. Defekt kryje się w przerwie między odczytem a zapisem: dwa żądania odczytują identyczny, ważny zrzut stanu, każde przechodzi swoje sprawdzenia i każde rejestruje wynik, który powinien wystąpić tylko raz. O wiele lepiej jest, gdy baza danych odrzuci jedną z dwóch konkurencyjnych operacji ze widocznym konfliktem, niż przechowuje dwie sprzeczne informacje, które ktoś musi później ręcznie uporządkować.
6. Zaplanuj, w jaki sposób funkcja będzie się tłumaczyć w środowisku produkcyjnym
Na twoim komputerze masz punkty przerwania, możliwość powtórzenia działania oraz świeżą pamięć projektu. W środowisku produkcyjnym zespół może otrzymać jedynie wiadomość wsparcia informującą, że coś nie zadziałało.
Załóż więc dochodzenie przed napisaniem kodu. Jeśli operacja się nie powiedzie, jak można będzie stwierdzić, który krok poszedł nie tak? Czy pojedyncze żądanie można prześledzić między różnymi usługami? Czy logi pokażą, czy działanie zostało wykonywane raz, czy trzy razy? Czy można odróżnić stany „nigdy nie rozpoczęto”, „w trakcie”, „częściowo zakończone” i „nieudane”?
To nie jest wezwanie do rejestrowania wszystkiego. Nieuporządkowana ilość danych utrudnia dochodzenia, a nie ułatwia je. Przydatna możliwość obserwowalności obejmuje dokładnie tyle informacji, ile potrzeba do odtworzenia historii pojedynczej ważnej operacji:
- ID żądania lub identyfikator korelacji
- ID zasobu i nazwa operacji
- Czas trwania
Należy również zachować sens niepowodzenia. Nieudana zapytanie nie powinno w milczeniu zamienić się w pustą listę. Przekroczenie czasu oczekiwania dostawcy nie powinno ignorować faktu, że strona zdalna mogła już zakończyć pracę. Szeroki blok przechwytywania nie powinien sprowadzać każdej przyczyny do jednego ogólnego komunikatu przed dotarciem do mechanizmu rejestracji.
Razem z tym należy pomyśleć o odzyskiwaniu. Czy bezpieczne jest ponowne uruchomienie zadania, które zawiodło? Czy obsługa może sprawdzić aktualny stan operacji bez ręcznego przeglądania kilku tabel? Czy użytkownik może bezpiecznie spróbować ponownie, a czy ktoś może dostarczyć mu dokładny opis wyniku?
Cechy nieprzezroczyste stają się kosztowne w momencie wystąpienia błędu, a dodawanie funkcji logowania później często przychodzi za późno, ponieważ odpowiedni kontekst istniał tylko w trakcie wykonywania operacji. Projektowanie dowodów z góry sprawia, że stają się one częścią samej funkcji, a nie tylko tymczasowym naprawieniem po incydencie.
7. Zdecyduj, w jaki sposób udowodnisz, że zmiana jest bezpieczna
Gdy testy są pisane po kodzie, zazwyczaj odzwierciedlają jego obecną strukturę. Istnieje pomocnik, więc test sprawdza, czy został on wywołany; istnieje rozwiązanie awaryjne, więc test to zabezpiecza; repozytorium jest symulowane, więc test potwierdza, że symulacja zwróciła to, co jej polecono. Takie testy przechodzą, ale niewiele udowadniają.
Decydowanie najpierw o sposobie weryfikacji często ujawnia słabości projektu. Jeśli operacja musi być idempotentna, test powinien ją wykonać kilka razy. Jeśli jednoczesne zatwierdzenie tego samego rekordu musi być niemożliwe, test wymaga rzeczywistych konkurencyjnych aktualizacji. Jeśli „nie znaleziono” i „błąd” to różne wyniki, kontrakt musi umożliwić ich obserwację. Jeśli zasada jest egzekwowana za pomocą ograniczenia bazy danych, żaden test jednostkowy nie może potwierdzić jej przestrzegania.
To nie oznacza, że każda funkcjonalność wymaga rozbudowanej serii testów end-to-end. Wybierz poziom testowania w zależności od warstwy, która faktycznie zapewnia gwarancję:
- Czystą transformację można bezpośrednio przetestować jako test jednostkowy.
- Kontrakt API zazwyczaj wymaga testu integracyjnego.
- Inwariancja egzekwowana w bazie danych musi być sprawdzana na rzeczywistej bazie danych.
W tej samej rozmowie powinna pojawić się kwestia odwracalności. Jeśli zmiana zachowa się niewłaściwie, czy można ją wyłączyć lub cofnąć bez utraty danych napisanych w międzyczasie? Czy poprzednia wersja aplikacji nadal będzie działać po zmianie schematu, czy migracja wymaga sekwencji rozszerzania i kurczenia? Czy można wypuścić produkt dla małej grupy, zanim wszyscy od niego będą zależeć?
Jeśli projekt jest trudny do przetestowania lub trudny do odwrócenia, to często jest to oznaka, że jedna operacja obejmuje zbyt wiele obowiązków lub że zmiana wymaga mniejszego kroku pośredniego. Celem nie jest uzyskanie absolutnego dowodu, ponieważ oprogramowanie zawsze wiąże się z niepewnością. Chodzi o to, aby kluczowe gwarancje były widoczne w testach i metrykach, a niebezpieczne decyzje można było cofnąć, dzięki czemu błąd staje się lekcją zamiast trwałymi szkodami.
Zachowanie proporcji w przygotowaniach
Te pytania nie stanowią argumentu za tym, że planowanie zawsze przewyższa działanie. Nadmierna analiza może opóźnić proste prace i doprowadzić do stworzenia architektury przeciwko ryzykom, które nigdy się nie urzeczywistnią. Rozsądny filtr to skupienie się wyłącznie na decyzjach, które będą kosztowne w przypadku błędu: wszystko to, co dotyczy danych trwałych, pieniędzy, uprawnień, zewnętrznych skutków ubocznych lub umów publicznych. Zmiana kopii lub wewnętrzna refaktoryzacja za pomocą stabilnej interfejsu rzadko wymaga pełnego wykazu.
W uproszczonej formie lista kontrolna mieści się w opisie zgłoszenia:
- Prawdziwy problem, w jednym zdaniu, oraz kto go ma
- To, co oznacza „zrobione”, oraz które skutki są drugorzędne
- Jedyny właściciel każdej reguły biznesowej
- Plan dotyczący istniejących danych i starych klientów
- Zachowanie przy ponownej próbie oraz przy jednoczesnym dostępie
- To, co jest rejestrowane, oraz jak naprawiać błędy
Podsumowanie
Gdy na te pytania znajdą się odpowiedzi, kod zazwyczaj staje się wyraźnie prostszy w strukturze. Model stanu zawiera mniej niemożliwych kombinacji, każda zasada ma określone miejsce, baza danych egzekwuje niezmienności, odpowiedzi wskazują, czy praca została ukończona, czy jedynie zaakceptowana, a testy skupiają się na gwarancji, a nie na obecnej strukturze funkcji.
Dlatego staranni inżynierowie mogą wydawać się powolni na początku zadania, a mimo to je szybciej zakończyć: odmawiają pozwolenia, by niejasności produktu, dane z przeszłości, konkurencja i braki w procesach operacyjnych zostały cicho przekształcone w trwałe decyzje techniczne. Odpowiednim momentem na rozwiązanie tych problemów jest czas przed tym, zanim pierwsza wygodna implementacja zostanie użyta w przypadkach wezwania funkcji, testów czy danych produkcyjnych. Pisanie samej funkcji rzadko stanowi trudną część; trudniejsze jest ustalenie, co ma ona oznaczać.
Literatura pokrewna
- Pułapki architektury backendu, które utrudniają pracę zespołom React skupionym na frontendzie — Opisuje pięć typowych błędów w projektowaniu backendu występujących w projektach opartych na React – od niewłaściwego wykorzystania paradygmatu API po kruche implementacje – oraz rozwiązania architektoniczne zapewniające niezawodność na poziomie produkcyjnym.