Dziewięć nawyków na poziomie kodu, które ułatwiają budowanie zaufania do pracy seniorowych inżynierów
Omawia dziewięć konkretnych praktyk programowania – od klauzul ochronnych po ścisłe modelowanie danych – które sprawiają, że kod jest bardziej odporny, łatwiezy do odczytania i prostszy w debugowaniu pod presją.
Przez długi czas wydawało się, że różnica pomiędzy najbardziej doświadczonymi inżynierami w zespole a wszystkimi innymi sprowadza się do merytorycznej wiedzy — jakiegoś mało znanej triku z frameworkiem, ukrytej API lub skrótu, o którym nikt inny jeszcze nie dowiedział się.
Obserwowanie pracy doświadczonych inżynierów z bliska, poprzez sesje współpracy, przeglądy kodu i rozmowy w sytuacjach awaryjnych, ujawnia coś mniej imponującego. Niekoniecznie są oni mądrzejsi. Po prostu piszą kod, który jest odporny na przypadkowe błędy, a jego naprawa jest znacznie łatwiejsza, gdy coś się psuje.
Dziewięć konkretnych nawyków pojawia się na tyle często, że warto je celowo przyjąć.
1. Klauzule ochronne zamiast piramid zagłady
Częstym błędem na początku jest układanie logiki walidacji warstwami, aż powstaje „schodkowa” struktura wcięć:
function processWithdrawal(account, amount) {
if (account.isActive) {
if (amount > 0) {
if (account.balance >= amount) {
return debit(account, amount);
}
}
}
throw new Error("Withdrawal failed");
}
To działa, ale po trzecim warunku tekst staje się nieczytelny, a rzeczywista logika wypłaty znajduje się głęboko, na trzecim poziomie. Bardziej dojrzały podejście odwraca ten porządek: najpierw odrzuca nieważne przypadki, a dopiero potem pozwala prawdziwej logice działać na najwyższym poziomie, bez wcięć:
function processWithdrawal(account, amount) {
if (!account.isActive) throw new AccountInactiveError();
if (amount <= 0) throw new InvalidAmountError();
if (account.balance < amount) throw new InsufficientFundsError();
return debit(account, amount);
}
Tutaj nie dzieje się nic wyjątkowego. Po prostu jest łatwy do odczytania w sytuacjach kryzysowych, co jest najważniejsze właśnie wtedy, gdy go potrzebujesz – podczas incydentu, o pierwszej w nocy, gdy jesteś na wpół śpiący i próbujesz ustalić, który z pięciu warunków w nawiasach wprowadza cię w błąd.
2. Nazwy opisujące biznes, a nie typ danych
Nazywanie zmiennych od ich kształtu, a nie znaczenia — data, res, obj, list — jest zrozumiałe w pojedynczym pliku. Okazuje się jednak katastrofalne, gdy baza kodu składa się z czterdziestu plików, w których każdy definiuje data jako coś innego.
data = fetch(order_id)
if data["status"] == "done":
process(data)
w porównaniu z:
order = fetch_order(order_id)
if order.is_fulfilled:
archive_order(order)
Druga wersja informuje o tym, co reprezentuje dany obiekt i która warunek jest faktycznie istotna, bez konieczności wracania do funkcji, z której pochodzi. Kosztuje to zaledwie kilka dodatkowych znaków, ale oszczędza osobie debugującej ten kod konieczności przeglądania trzech niepowiązanych ze sobą plików.
3. Jedna spójna granica pomiędzy twoim kodem a światem zewnętrznym
API dostarczane przez strony trzecie są niepewnymi źródłami informacji. Nazwy pól zmieniają się, struktury zagnieżdżone ulegają modyfikacjom, pola opcjonalne stają się obowiązkowe, a potem znowu opcjonalne. Pozwalanie na bezpośrednie wprowadzanie surowych odpowiedzi API do logiki biznesowej zamienia każdą z tych zmian w poszukiwania w całym kodzie.
// scattered everywhere
const price = apiResponse.line_items[0].unit_price_cents / 100;
Lepszym podejściem jest przetłumaczenie odpowiedzi dokładnie raz, bezpośrednio na granicy:
function toLineItem(raw) {
return {
label: raw.description,
priceInDollars: raw.unit_price_cents / 100,
};
}
Jeśli dostawca później zmieni nazwę unit_price_cents na price, musi ulec zmianie dokładnie jedna funkcja. Wszystko poniżej pozostaje nietknięte i w ogóle nie zauważa różnicy.
4. Modele danych, które nie mogą kłamać
Typ składający się z tuzina pól opcjonalnych to typ, który przestał próbować dokładnie reprezentować rzeczywistość:
type Ticket = {
id?: string;
assignee?: string;
resolvedAt?: Date;
resolution?: string;
};
Ta forma umożliwia tworzenie „rozwiązanej” zgłoszenia bez żadnego rozwiązania lub „przydzielonego” zgłoszenia bez osoby odpowiedzialnej — stany, które powinny być niemożliwe, kompilują się bez żadnych problemów. Podział typu według rzeczywistego stanu eliminuje całą tę kategorię błędów:
type OpenTicket = { id: string; assignee?: string };
type ResolvedTicket = { id: string; assignee: string; resolvedAt: Date; resolution: string };
Funkcja, która wysyła e-mail z podsumowaniem rozwiązania, może teraz wymagać wyraźnie argumentu typu ResolvedTicket, a kompilator gwarantuje, że nic nieukończonego nigdy do niej nie dotrze.
5. Oddzielenie pytania od polecenia
Gdy reguły biznesowe i efekty uboczne splatają się ze sobą, testowanie staje się trudne — a gdy testowanie jest trudne, reguły w ogóle przestają być testowane:
def promote_employee(employee_id):
emp = get_employee(employee_id)
if emp.tenure_months < 12:
raise Error("Not eligible yet")
if emp.current_rating < 3:
raise Error("Rating too low")
give_raise(emp)
notify_hr(emp)
log_promotion(emp)
Wyodrębnienie sprawdzenia warunków dostępności do osobnej, samodzielnej funkcji oznacza, że można przetestować tę zasadę przy użyciu zwykłego obiektu, bez konieczności korzystania z bazy danych czy usług e-mail:
def promotion_eligibility(emp):
if emp.tenure_months < 12:
return Ineligible("Not enough tenure")
if emp.current_rating < 3:
return Ineligible("Rating too low")
return Eligible()
Gdy sprawdzanie tej zasady staje się proste, ludzie rzeczywiście ją stosują, a przypadki krawędziowe przestają umykać uwadze po kilku miesiącach, gdy ktoś zmienia wymagania dotyczące czasu trwania.
6. Komentarze powinny wyjaśniać dlaczego, a nie co
Komentarz, który po prostu powtarza to, co już mówi kod, jest zbędnym zamuleniem:
// increment the counter
counter++;
Komentarz, który przedstawia uzasadnienie decyzji, jest tym, którego warto zachować:
// Retry once — the vendor's webhook occasionally arrives before
// the payment record finishes committing on their end.
retryOnce(processWebhook, payload);
Doświadczeni inżynierowie zazwyczaj piszą znacznie mniej komentarzy niż ci młodsi — nie z lenistwa, ale dlatego, że zdali sobie sprawę, iż większość komentarzy służy do uzupełnienia kodu, który sam w sobie nie jest wystarczająco wyjaśniający. Te komentarze, które pozostają, zawierają informacje, których kod sam w sobie po prostu nie może przekazać: uzasadnienie, kompromis lub ostrzeżenie dotyczące czegoś, co nie jest oczywiste na pierwszy rzut oka.
7. Błędy, które wskazują na coś przydatnego
Wiadomość o błędzie taka jak "Nieprawidłowy wprowadzony danych" nie daje kolejnemu programiście żadnych podstaw do działania. W jakim sensie nieprawidłowy? Które dane? Przydatny błąd zawiera wystarczająco dużo szczegółów, aby ktoś mógł na jego podstawie podjąć działania:
{
"code": "INVALID_DATE_RANGE",
"message": "End date must be after start date.",
"field": "endDate"
}
Nie jest rzadkością znalezienie kodu frontendowego, który analizuje tekst błędu, aby ustalić, jaką wiadomość wyświetlić — na przykład if (err.message.includes("date")). Taki wzorzec jest z natury kruchy. Gdy tylko ktoś zmieni sformułowanie wiadomości z backendu, logika interfejsu w tle przestaje działać. Kod istnieje po to, by maszyny mogły na jego podstawie podejmować decyzje; wiadomości istnieją po to, by ludzie mogli je czytać. Trzymanie tych dwóch elementów oddzielnie oznacza, że żaden z nich nie musi niewygodnie pełnić roli drugiego.
8. Jeden pull request, jedna idea
Pull request o tytule typu „naprawa kwestii z fakturowaniem”, obejmujący tuzin plików i zawierający sześć niepowiązanych ze sobą zmian, jest praktycznie niemożliwy do przejrzenia. Osoba, która go sprawdza, albo zatwierdza go bez rzeczywistej weryfikacji, albo traci godzinę na próby ustalenia, która zmiana spowodowała dany efekt.
Podejście oparte na dyscyplinie może wydawać się niemal nadmiernie ostrożne: przemianuj pole w jednym PR, wprowadź nową walidację w drugim, a połącz ją z procesem w trzecim. Taki sposób działania wydaje się w momencie realizacji wolniejszy. Jednak sprawia to znacznie większe możliwości przeglądania, a gdy później coś się zepsuje w produkcji, git log dostarcza rzetelnej odpowiedzi zamiast zmuszania do przeglądania 400-wierszowego diffu.
9. Traktuj pierwszy szkic jako szkic
To ostatnie nawyki mają mniej zwiąku z samym kodem, a bardziej z ego. Programiści na początku kariery często traktują pierwszą działającą wersję jako produkt gotowy – skoro działa, to jest gotowa do wydania. Doświadczeni inżynierowie piszą tę pierwszą wersję, zakładając już, że przeczytają ją ponownie z krytycznym spojrzeniem, zanim dotrze ona choćby w pobliże środowiska produkcyjnego.
To właśnie podczas tej drugiej przeglądu warunki nawarstwione są sprowadzane do zdań kontrolnych, niejasne nazwy są przemieniane, a rzekomo „niemożliwy” stan jest wykrywany zanim klient w ogóle na niego natrafi. To niewielki nawyk – zatrzymanie się, ponowne przeczytanie i zastanowienie się, czy to mogłoby wprowadzić w błąd kogoś bez żadnego kontekstu – ale to właśnie on sprawia, że pozostałe osiem nawyków faktycznie się realizuje w praktyce.
Wspólny mianownik
U podstaw tego wszystkiego leży w rzeczywistości jeden powtarzany ruch: przeniesienie złożoności, która w przeciwnym razie zostałaby później uwięziona w głowie kogoś innego, i umieszczenie jej gdzieś widocznego już teraz – w nazwie, granicy, typie lub małej, skoncentrowanej różnicy. Nic z tego nie wymaga wyjątkowych umiejętności. Wymaga jedynie konsekwentnego podejmowania decyzji, że każdy, kto następnie przeczyta ten kod, zasługuje na prawdziwą szansę na jego zrozumienie.
Literatura pokrewna
- Te ponawiające się nawyki JavaScript, które cicho niszczą twoją bazę kodu — Wyjaśnia dziesięć powszechnych pułapek w JavaScript i TypeScript, od luźnej równości po modyfikację stanu, oraz pokazuje bezpieczniejsze wzorce zastępujące każdą z nich.
- Powszechne pułapki JavaScript i TypeScript, które cicho niszczą kod — Opisuje subtelne problemy w JavaScript i TypeScript – od porównań z NaN po synchronizację asynchroniczną i przymusową konwersję typów – które powodują błędy mimo pozornie poprawnego działania.