Te nawyki architektoniczne, które zapewniają utrzymaność baz kodu frontend przez lata
Wyjaśnia strukturalne nawyki, takie jak optymalizacja pod kątem możliwości usunięcia kodu, wyraźny przepływ danych oraz izolacja logiki biznesowej, które pomagają bazom kodu pozostać utrzymywalnymi na przestrzeni lat zmian.
Każda baza kodu frontendu, która przetrwa wystarczająco długo, ostatecznie dzieli się na dwie odrębne strefy.
Pierwszą można nazwać Zoną Zagrożenia.
Jest to chaotyczna mieszanka menedżerów stanu, naprawionych hooki cyklu życia, sprytnych, lecz nieprzejrzystych abstrakcji globalnych, niedokończonych eksperymentów oraz funkcji pomocniczych napisanych lata temu, których już nikt nie potrafi w pełni wyjaśnić.
Nikt nie chce się do niej zbliżać.
Gdy w tę część aplikacji trafia nowa prośba o funkcjonalność, zespół nie ocenia jej skomplikowania na podstawie rzeczywistej trudności pracy.
Ocenia ją na podstawie tego, jak ryzykowne wydaje się modyfikowanie danego kodu.
Zmiana, która powinna zająć dwa dni, zamienia się w prace trwające dwa tygodnie, ponieważ wszyscy wiedzą, że większość czasu pójdzie na testy regresji, a nie na tworzenie nowych funkcjonalności.
Istnieje jeszcze druga strefa.
Nazwijmy ją Stabilną Podstawą.
To są moduły stworzone lata temu, które spokojnie przetrwały kilka migracji frameworków, przebudów, zmian w kierunku produktu oraz zmiany w kierownictwie inżynieryjnym.
Prawie nigdy nie powodują przerw w działaniu.
W sposobie ich napisania nie ma nic szczególnie genialnego.
A gdy do zespołu dołącza ktoś nowy, może otworzyć jeden z tych plików, zrozumieć, co robi, bez konieczności instrukcji, i wysłać pierwszą prośbę o integrację w ciągu jednego lub dwóch dni.
To właśnie jest to, na co warto zwrócić uwagę.
Kod, który przetrwa, zazwyczaj nie jest najbardziej zaawansowany w systemie.
Zwykle jest najprostszy.
Doświadczeni inżynierowie wiedzą, że oprogramowanie nigdy nie pozostaje w tych samych warunkach, w których zostało stworzone.
Wymagania się zmieniają.
Zespoły się reorganizują.
Zależności są wymieniane.
Frameworki rozwijają się dalej.
Firmy zmieniają strategię.
Ludzie opuszczają firmę.
Nowi inżynierowie pojawiają się bez żadnego kontekstu dotyczącego tego, dlaczego rzeczy zostały zbudowane w określony sposób.
Dlatego pytanie, które warto zadać, to nie:
"Jak czysty wygląda obecnie ten projekt?"
A raczej:
"Ile to będzie kosztować, jeśli chcemy to zmienić za pięć lat?"
Poniżej przedstawiono nawyki strukturalne, które umożliwiają taką długowieczność.
1. Optymalizuj pod kątem możliwości usunięcia, a nie ponownego użycia
Wiele wskazówek dotyczących architektury koncentruje się na ponownym użyciu.
Zrób tak, by twoje komponenty mogły być ponownie wykorzystywane.
Buduj usługi uniwersalne.
Dodawaj punkty rozszerzeń.
Projektuj architektury wtyczek.
Pisz abstrakcje dla implementacji, których jeszcze nie stworzyłeś.
Ponowne użycie ma swoje zastosowanie.
Ale istnieje jeszcze jedna cecha, która często ma większe znaczenie w produkcie, który stale się rozwija:
Jak łatwo jest coś usunąć.
Funkcje nie są trwałe.
Są zastępowane.
Są przeprojektowywane.
Zlewają się z innymi funkcjami.
Czasami firma po prostu traci do nich zainteresowanie.
Wyobraź sobie projekt React zorganizowany ściśle według typu pliku:
src/
components/
BillingTable.tsx
UserModal.tsx
SubscriptionCard.tsx
hooks/
useBillingData.ts
useUserData.ts
useSubscription.ts services/
billingApi.ts
userApi.ts
subscriptionApi.ts
Na pierwszy rzut oka wygląda to uporządkowanie.
Każdy typ pliku ma swoją dedykowaną folderówkę.
Ale załóżmy, że po osiemnastu miesiącach firma postanowi całkowicie zrezygnować z procesu fakturowania.
Gdzie właściwie znajduje się wszystko związane z fakturowaniem?
Będziesz musiał przeszukać kilka różnych katalogów.
Znajdujesz i usuwasz BillingTable.tsx.
Następnie natrafiasz na useBillingData.ts.
Następnie definicje typów napisane specjalnie do obsługi fakturowania.
Potem funkcja pomocnicza, do której korzysta wyłącznie moduł fakturowania.
Następnie plik stylów.
Potem wywołanie API.
Następnie przykład testu.
Potem hook, który początkowo służył do fakturowania, ale później zmienił nazwę.
Funkcja ta zniknęła z produktu, ale jej ślady wciąż istnieją w całym kodzie.
Dokładnie w ten sposób z czasem gromadzi się martwy kod.
Organizacja według funkcji sprawia, że granice stają się znacznie bardziej oczywiste:
src/
features/
billing/
components/
BillingTable.tsx
hooks/
useBillingData.ts
services/
billingApi.ts
types.ts
index.ts
Teraz fakturowanie ma jedno jasne miejsce przechowywania.
Jeśli firma zdecyduje się usunąć tę funkcję, pierwszy krok jest prosty:
src/features/billing/
Usuń folder.
TypeScript wtedy pokaże wszystko inne, co nadal od niego zależy.
To o wiele zdrowszy rodzaj zależności do obsługi.
Możliwość usunięcia to forma utrzymywalności
Modyuł staje się łatwiejszy w utrzymaniu, gdy można od razu określić, gdzie znajdują się jego obowiązki.
To jeden z powodów, dla których struktury folderów oparte na funkcjach często pojawiają się w rozmowach o skalowaniu dużych baz kodu React. Zespoły pracujące nad złożonymi aplikacjami często proponują taką organizację właśnie dlatego, że zmniejsza ona wpływ jakiejkolwiek modyfikacji.
Celem nie jest posiadanie idealnie uporządkowanego drzewa katalogów.
Celem jest umiejętność szybkiego odpowiedzi na następujące pytanie:
"Gdyby ta funkcja zniknęła jutro, co musiałbym usunąć?"
Jeśli na to pytanie trudno jest odpowiedzieć, granice poszczególnych funkcji są prawdopodobnie zbyt luźne.
2. Nie zamieniaj shared/ w kosz na śmieci
Istnieje druga pułapka, która często pojawia się, gdy zespoły przechodzą na architekturę opartą na funkcjach.
Wszystko, co wyraźnie nie należy do żadnej konkretnej funkcjonalności, trafia do katalogu shared/.
Kilka miesięcy później mamy coś w rodzaju:
shared/
utils/
helpers/
common/
services/
components/
hooks/
types/
I w tym momencie shared/ stanowi połowę całego kodu.
To powoduje swój własny rodzaj problemu z powiązaniami między komponentami.
Dobra zasada, której należy przestrzegać:
Kod powinien trafić do wspólnego foldera, ponieważ kilka funkcjonalności rzeczywiście opiera się na tym samym koncepcie, a nie dlatego, że nie można było zdecydować, gdzie indziej powinien się znaleźć.
Ogólny komponent przycisku naturalnie pasuje do systemu projektowego.
Klient autoryzacji może rozsądnie znajdować się w warstwie infrastruktury wspólnej.
Pomocnik do formatowania dat również może być udostępniany wspólnie.
Ale funkcja taka jak:
calculateEnterpriseRenewalDiscount()
prawie na pewno należy do tej funkcjonalności, która odpowiada za daną zasadę biznesową.
Opieraj się pokusie przenoszenia elementów do folderów globalnych tylko po to, by drzewo katalogów wyglądało schludniej.
Kod udostępniany nie jest darmowy, ponieważ każda funkcja, która się nim operuje, staje się potencjalnym zależnością.
Im większy staje się katalog shared/, tym trudniej jest ustalić, kto faktycznie odpowiada za dany element funkcjonowania aplikacji.
3. Twórz adaptatory obronne wokół zewnętrznych zależności
Istnieje duże prawdopodobieństwo, że twoja aplikacja będzie nadal działać długo po tym, jak niektóre z jej zależności przestaną istnieć.
Dziś możesz polegać na Axios, a jutro przejść na wbudowaną funkcję fetch.
Obecnie możesz korzystać z jednego dostawcy analiz, a za kilka lat twoja firma może przenieść się do innego.
Dziś możesz zintegrować bibliotekę autoryzacji, by później nowe wymagania bezpieczeństwa skłoniły cię do użycia innej.
Kruchością w budowaniu aplikacji jest importowanie tych zewnętrznych pakietów bezpośrednio do dziesiątek komponentów.
import axios from 'axios';
import { trackMixpanelEvent } from 'mixpanel-browser';
export function CheckoutCard() {
const handlePurchase = async () => {
await axios.post('/api/checkout', payload); trackMixpanelEvent('checkout_completed');
};
}
W tym momencie warstwa interfejsu użytkownika ma bezpośrednią wiedzę o tym, jaki klient HTTP i który dostawca analiz używamy. Gdy taki wzorzec zostanie zastosowany we czterdziestu pięciu komponentach, zamiana dostawcy przestaje być zadaniem ograniczonym do jednego elementu – staje się zmianą, która wpływa na całe repozytorium.
Warstwa graniczna zapewnia możliwość wymiany zależności. Na przykład:
// src/shared/lib/analytics.ts
import mixpanel from 'mixpanel-browser';export const analytics = {
trackCheckoutCompleted(
orderId: string,
amount: number
) {
mixpanel.track('checkout_completed', {
orderId,
amount,
});
},
};
Komponent teraz komunikuje się z koncepcją zdefiniowaną przez samą aplikację:
analytics.trackCheckoutCompleted(orderId, amount);
Nie wie on, czy pod spodem znajduje się Mixpanel, PostHog, Segment czy jakiś inny narzędzie. Jeśli dostawca się zmieni, kontrakt, od którego zależy aplikacja, może pozostać zupełnie taki sam.
Ale nie abstrahuj wszystkiego
To kwestia ma takie samo znaczenie jak poprzednia.
Otoczanie zależności adapterem nie jest samo w sobie dobrym rozwiązaniem projektowym. Jeśli stworzysz specjalną interfejs dla każdej małej biblioteki, której używasz, możesz ostatecznie napisać więcej kodu niż sama biblioteka zawiera.
Prawdziwe pytanie brzmi:
„Czy późniejsza zastąpienie tej zależności byłoby kosztowne, czy też pozwolenie jej na niekontrolowane rozprzestrzenianie się w całym kodzie byłoby ryzykowne?”
Jeśli odpowiedź brzmi tak, inwestycja w adapter jest opłacalna. W przeciwnym razie bezpośrednie wywoływanie zależności jest prawdopodobnie prostsze i wystarczające.
Doświadczony inżynier nie oznacza kładzenia warstw abstrakcji na wszystko, czego się dotykasz. Oznacza to ustanawianie granic dokładnie tam, gdzie ich pominięcie mogłoby później kosztować cię dużo.
4. Jasny przepływ danych jest lepszy od „magii”
Jednym z najszybszych sposobów na stworzenie zagmatwanej bazy kodu jest ukrywanie źródeł wartości.
Emiterzy zdarzeń globalnych to klasyczny przykład.
eventBus.emit('USER_UPDATED', {
id: user.id,
});
Ta linia informuje o wywołanym zdarzeniu. Nie mówi nic o tym, kto na nie uważa.
Moglibyśmy przeszukać całą bazę kodu i w końcu znaleźć coś takiego jak:
eventBus.on('USER_UPDATED', handler);
Ale może istnieć trzech oddzielnych słuchaczy. Jeden mógł zostać dodany dwa lata temu. Inny może modyfikować stan globalny. Trzeci może wysyłać żądanie analityczne. Nagle rzeczywiste zachowanie wywołane przez tę pierwotną funkcję jest rozproszone po całym aplikacji, zamiast znajdować się w jednym miejscu.
Porównaj to teraz z kontraktem, który jest jasno sformułowany:
interface UserCardProps {
user: User;
onUserRoleChange: (
userId: string,
newRole: Role
) => Promise<void>;
}
Tutaj komponent jasno deklaruje, jakie działania obsługuje, a komponent nadrzędny jasno określa, co dzieje się, gdy to działanie zostanie wywołane. Przepływ danych jest widoczny na stronie.
Tak, to bardziej rozwlekłe niż wywołanie anonimowego zdarzenia. Ale ta dodatkowa rozwlekłość ma sens, ponieważ pozostawia śledzalną ścieżkę w kodzie.
Gdy ktoś nieznający komponentu otworzy plik, powinien być w stanie odpowiedzieć na trzy pytania bez przeglądania reszty repozytorium:
Skąd pochodzą te dane?
Zazwyczaj są to propsy, parametry trasy, hook lub jasno zdefiniowana warstwa dostępu do danych.
Co może je zmienić?
Widoczne wywołanie funkcji, mutacja, działanie lub wyraźna aktualizacja stanu.
Co dzieje się, gdy użytkownik wykonuje to działanie?
Bezpośredni wywołanie funkcji, której implementację można prześledzić.
Im mniej zachowań ukrywasz, tym łatwiej jest zrozumieć cały system.
5. Trzymaj logikę biznesową poza cyklami życia frameworka
Frameworki nie są stałymi elementami – to jedno z bardziej wiarygodnych założeń, jakie można poczynić podczas pracy nad frontendem.
Sam React przeszedł już kilka istotnych zmian. Komponenty klasowe straciły na popularności. Hooki przeorganizowały sposób strukturyzowania logiki związanej ze stanem. W wielu projektach Create React App został zastąpiony narzędziami takimi jak Vite lub rozwiązaniami wbudowanymi w framework. Renderowanie typu server-first oraz nowsze podejścia do routingu zmieniły sposób, w jaki zespoły myślą o pobieraniu danych i określaniu granic aplikacji.
Framework, którego obecnie używasz, może wcale nie przypominać standardu za pięć lat. Twoje zasady biznesowe muszą jednak nadal funkcjonować bez względu na to.
Weźmy jako przykład obliczanie podatków. Krucha wersja ukrywa rzeczywistą logikę wewnątrz hooka React:
export function useTaxCalculator(
cartItems: CartItem[]
) {
const [tax, setTax] = useState(0);
useEffect(() => {
let calculated = 0; // 60 lines of tax calculation,
// rounding rules,
// country logic,
// exemptions... setTax(calculated);
}, [cartItems]); return tax;
}
Teraz obliczanie podatków jest powiązane z Reactem. Testowanie go oznacza uruchomienie środowiska React. Wywoływanie go z akcji serwera jest niewygodne. Uruchamianie go wewnątrz Web Workera również jest niewygodne. Przeniesienie go do innego frameworku interfejsu użytkownika stanowi kosztowne przedsięwzięcie.
Lepszym podejściem jest oddzielenie tych dwóch aspektów:
export function calculateTax(
cartItems: CartItem[],
countryCode: string
): number {
// Pure business logic
return totalTax;
}
Słój React po prostu do niego wywołuje:
const tax = calculateTax(cartItems, countryCode);
Dzięki taka strukturze logika, która faktycznie ma znaczenie, nie ma żadnej świadomości istnienia Reacta. Może być wykonywana w dowolnym środowisku. Można ją testować za pomocą zwykłych testów jednostkowych. Proces serwerowy może ją bezpośrednio ponownie wykorzystać. Ponadto przetrwa migrację na framework interfejsu użytkownika bez konieczności jej przepisywania.
Frameworki powinny znajdować się na zewnętrznych granicach
Pomocnym sposobem na zilustrowanie tego jest diagram warstwowy:
┌──────────────────────────────┐
│ UI Layer │
│ React / Next.js │
├──────────────────────────────┤
│ Application Logic │
├──────────────────────────────┤
│ Domain Logic │
│ Pure TypeScript │
├──────────────────────────────┤
│ Infrastructure │
│ APIs / DB / Vendors / SDKs │
└──────────────────────────────┘
Im bliżej element kodu znajduje się środka, tym mniej powinien polegać na konkretnym frameworku lub bibliotece dostawcy.
To nie oznacza, że każdy projekt React wymaga pełnego wdrożenia „Clean Architecture”.
Oznacza to, że musisz mieć jasność co do tego, które części twojego kodu są rzeczywiście specyficzne dla Reacta, a które reprezentują twoje rzeczywiste zasady biznesowe.
Są to dwie odrębne kategorie, a utożsamianie ich ze sobą jest źródłem problemów.
6. Unikaj przekształcania hooków w miniaplikacje
To wzorzec pojawia się wielokrotnie w kodach React.
Zwykle zaczyna się niewinnie:
function useUser() {
// fetch user
}
Następnie, z biegiem czasu, narastają wymagania.
function useUser() {
// fetch user
// loading state // error handling // permissions // analytics // transformations // caching // retry logic // business rules // notifications // feature flags
}
Niedługo potem to, co zaczęło się jako prosty hook, cicho przerodziło się w aplikację składającą się z 500 linii kodu, ukrytą pod niewinnie wyglądającą nazwą funkcji.
Hooki są rzeczywiście przydatne.
Jednak hook nie powinien stać się miejscem zbierania wszelkich problemów tylko dlatego, że ma łatwy dostęp do stanu i efektów React.
Lepszym podejściem jest przekazanie zadania hookowi mniejszym, bardziej skoncentrowanym elementom:
function useUser() {
const user = useUserQuery();
const permissions =
calculatePermissions(user.data); return {
user: user.data,
permissions,
isLoading: user.isLoading,
};
}
Dzięki taka strukturze hook staje się warstwą orkiestracji, która łączy różne elementy.
To już nie cała architektura zamknięta w jednej funkcji.
Tę granicę o wiele łatwiej jest utrzymywać z biegiem czasu.
7. Pisz dokumentację decyzji architektonicznych, a nie nieskończone wiki
Jednym z najczęstszych powodów degradacji architektury wcale nie jest bałagan w kodzie.
Jest to utracony kontekst.
Oto jak to zwykle wygląda: programista podejmuje nieoczywistą decyzję. Decyzja ta jest rozsądna, a wszyscy w zespole w tamtym czasie rozumieją uzasadnienie. Następnie ta osoba przechodzi do innych zadań.
Miesiące później nowy inżynier natrafia na tę nietypową implementację i myśli:
"Dlaczego robimy to w taki sposób? Musi istnieć prostsze rozwiązanie."
Dlatego przepisują ją, nieświadomie przywracając dokładnie ten sam problem, którego uniknięcie było celem pierwotnej decyzji.
Jako przykład weźmy panel sterowania zbudowany na bazie Server-Sent Events zamiast WebSockets.
Bez tła historycznego nowy programista mógłby uzasadnionej drogą dojść do wniosku:
"WebSockets to nowszy standard. Przejdźmy na niego."
Jednak pierwotny zespół mógł wybrać właśnie SSE dlatego, że wielu klientów korporacyjnych korzysta z restrykcyjnych proxy firmowych, które źle obsługują połączenia WebSocket.
Taki uzasadnienie jest niewidoczne, jeśli patrzy się tylko na sam kod.
To właśnie ta luka ma zostać wypełniona przez zapisy decyzji architektonicznych.
Naprzимер:
# ADR 003: Use Server-Sent Events for Dashboard Feeds
## ContextOur dashboard requires real-time metric updates.We evaluated WebSockets and Server-Sent Events.## DecisionWe chose Server-Sent Events because:1. Communication is strictly server-to-client.
2. SSE uses standard HTTP infrastructure.
3. Browser reconnection is supported natively.
4. The solution works reliably within our enterprise network environment.## ConsequencesIf we later require client-to-server
bi-directional streaming, we should
re-evaluate this decision.
Gdy istnieje taki zapis, kolejny inżynier nie musi od nowa analizować uzasadnienia.
Moi od razu widzi dlaczego.
To proste
/docs/adr/
Katalog w twoim repozytorium może przechowywać lata wiedzy instytucjonalnej, która w przeciwnym razie zniknęłaby wraz z osobą opuszczającą zespół.
Dokumentuj decyzje, a nie wszystko
Nie musisz utrzymywać rozległej wiki składającej się ze stu stron.
W większości przypadków sam kod powinien być na tyle jasny, by wyjaśnić co robi.
Dokumentacja powinna natomiast odzwierciedlać rozumowanie, którego kod sam w sobie nie może wyrazić:
- dlaczego wybrano określoną technologię
- dlaczego odrzucono bardziej oczywistą alternatywę
- dlaczego w ogóle istnieje nietypowe ograniczenie
- dlaczego rozwiązanie tymczasowe, które wydaje się niepotrzebne, jest nadal konieczne
Dokumentacja sprawdza się właśnie wtedy, gdy zachowuje kontekst, który w przeciwnym razie zniknąłby wraz z osobami, które go znawały.
8. Projektowanie dla inżyniera, który dołączy po tobie
To może być najprostszy test na sprawdzenie, czy architektura będzie trwała.
Wyobraź sobie sytuację, w której od jutra wszyscy, którzy obecnie rozumieją wewnętrzne mechanizmy systemu, natychmiast opuszczają firmę.
Czy nowy zespół będzie w stanie nadal nim zarządzać?
Jeśli twoja szczera odpowiedź brzmi „nie”, to niekoniecznie oznacza, że brakuje ci programistów. Oznacza to, że system ma ukrytą zależność od wiedzy konkretnych osób.
System zaprojektowany tak, by trwać, powinien umożliwiać samodzielne odkrycie jego kluczowych zachowań.
Ktoś nowy powinien móc otworzyć repozytorium i stopniowo znaleźć odpowiedzi na pytania takie jak:
- Gdzie w bazie kodu znajduje się ta konkretna funkcja?
- Który moduł jest odpowiedzialny za to zachowanie?
- Skąd właściwie pochodzą te dane?
Dlatego właśnie wyraźne granice mają tak wielkie znaczenie.
Człowiek nowy w firmie nie powinien musieć uczyć się całej historii przedsiębiorstwa, by zrozumieć, co robi kod.
Sama baza kodu musi przenosić ze sobą wystarczającą ilość tej historii.
9. Utrzymuj mały zasięg wpływu zmian
Dobrym sposobem na ocenę architektury jest policzenie, ile plików wymusza na nas rutynowa zmiana.
Pomyślmy o prostym żądaniu funkcjonalnym: ktoś prosi o przycisk umożliwiający użytkownikom eksport raportu fakturowego jako pliku CSV.
W silnie skupionym systemie spełnienie tego żądania może oznaczać edycję plików rozmieszczonych w różnych miejscach:
components/
hooks/
services/
utils/
types/
global state/
shared helpers/
Inżynier może być zmuszony edytować tuzin plików tylko po to, by dodać jeden przycisk.
Porównaj to z dobrze zdefiniowaną strukturą funkcjonalną:
features/
billing/
components/
hooks/
services/
utils/
Tutaj ta sama zmiana może pozostać niemal w całości w folderze właśnie funkcji obsługującej fakturowanie.
To właśnie mają na myśli ludzie, mówiąc o zmniejszaniu promienia uderzenia zmiany.
Mali promień uderzenia daje:
- Mniej regresji
- Prostsze przeglądy kodu
- Szybszą dostawę
- Mniej konfliktów łączenia
- Latwiejsze testowanie
- Bardziej bezpieczne refaktoryzacje
Aby to osiągnąć, nie potrzeba skomplikowanej architektury. Potrzebne są granice, które rzeczywiście odzwierciedlają rozwój produktu w praktyce.
10. Przestań optymalizować pod kątem diagramu architektury
Niezwykle piękny diagram architektury może nadal ukrywać bazę kodu, w której pracowanie jest nieznośne.
Mogłbyś zaznaczyć każdą z tych opcji:
- przestrzeganie struktury Clean Architecture
- stosowanie zasad projektowania SOLID
- poprawne odwrócenie zależności
- umieszczanie dostępu do danych w wzorcach repository
- wytwarzanie obiektów za pomocą wzorców factory
- łączenie elementów za pomocą zdarzeń
- kładzenie kilku warstw abstrakcji jedna na drugiej
a mimo to przekształcenie prostej funkcjonalności w pracę trwającą kilka dni.
Architektura powinna zmniejszać złożoność, a nie ją zwiększać. Jeśli warstwa architektoniczna wprowadza więcej koncepcji niż sam produkt, coś poszło nie tak.
Często najlepsza architektura to ta, o której nikt nawet nie próbuje rozmawiać, ponieważ inżynierowie mogą po prostu przeczytać kod i go śledzić. W praktyce może to wyglądać tak:
features/
billing/
checkout/
accounts/
w połączeniu z skromnym rozwiązaniem:
shared/
ui/
lib/
plus kilka funkcji czysto związanych z logiką biznesową.
Nic z tego nie brzmi imponująco. Ale jeśli po czterech latach nadal funkcjonuje i ma sens, to dokładnie to robi architektura, do czego powinna służyć.
Czego tak naprawdę dążą do optymalizacji starsi inżynierowie
Starsi inżynierowie niekoniecznie tworzą bardziej skomplikowany kod. Różnica polega na zestawie pytań, które zadają przed jego napisaniem.
Mniej doświadczony inżynier może zapytać:
"Jak sprawić, by to było wielokrotnie używalne?"
Z kolei starszy inżynier pyta:
">Czy w ogóle musi to być wielokrotnie używalne?"
Mniej doświadczony inżynier może zapytać:
">Jak powinienem to uogólnić?"
Zamiast tego starszy inżynier pyta:
„Jaki konkretny problem ma rozwiązać ta abstrakcja?”
Inżynier mniej doświadczony może zapytać:
„Gdzie powinna trafić ta funkcja pomocnicza?”
Z kolei inżynier bardziej doświadczony pyta:
„Kto tak naprawdę jest odpowiedzialny za to zachowanie?”
Inżynier mniej doświadczony może zapytać:
„Jak przygotujemy się na wszelkie przyszłe wymagania?”
Zamiast tego inżynier bardziej doświadczony pyta:
„Która przyszła zmiana jest na tyle prawdopodobna, by usprawiedliwić dodanie tej złożoności już teraz?”
A być może najważniejsze ze wszystkich pytania:
„Jak będzie wyglądał ten kod, gdy osoba, która go napisała, odejdzie?”
To właśnie od tego pytania zaczyna się długoterminowe myślenie w inżynierii.
Podsumowanie: Trwały kod często wygląda zwyczajnie
Kod, który nadal dobrze funkcjonuje po latach zmian, rzadko jest kodem stworzonym przy użyciu najnowszego frameworka, najsprytniejszego wzorca projektowego czy najbardziej eleganckiej abstrakcji. Zazwyczaj jest to po prostu kod o jasnych granicach i przemyślanych, nieefektownych rozwiązaniach — taki, w którym inny inżynier może otworzyć repozytorium i zrozumieć, co się dzieje, bez konieczności szukania osoby, która go pierwotnie napisała.
Zasady leżące u jego podstaw są proste:
- Organizuj według funkcji i odpowiedzialności. Funkcje powinny być łatwe do znalezienia, a gdy nadejdzie czas, łatwe do usunięcia.
- Nadawaj priorytet możliwości usunięcia nad maksymalnym wykorzystaniem. Nie każdy fragment logiki zasługuje na to, by stać się wspólną abstrakcją.
Najwyższym komplementem, jaki może otrzymać baza kodu, nie jest:
"Ta architektura jest niezwykle pomysłowa."
Jest nim:
"Rozumiem to."
Ponieważ po pięciu latach pierwotni inżynierowie prawdopodobnie już nie będą pracować w tym projekcie. Framework może się zmienić, projekt ulec przeobrażeniu, a produkt ewoluować. Sam biznes może wyglądać zupełnie inaczej niż dziś.
Ale dopóki granice pozostają jasne, logika prosta, a uzasadnienie kluczowych decyzji jest gdzieś zapisane, kod może nadal rozwijać się wraz ze wszystkim innym wokół niego.
Tak właśnie wygląda trwały oprogramowanie.
Powiązane materiały
- Trzy patterny TypeScript, które ulepszają architekturę aplikacji React — Dowiedz się, w jaki sposób patterny Repository, Observer i Builder wykorzystują system typów TypeScript do tworzenia czystszych i łatwiejszych w utrzymaniu kodów w React i Next.js.
- Dziesięć częstych praktyk JavaScript, które po cichu niszczą twój kod — Wyjaśnia dziesięć powszechnych problemów w JavaScript i TypeScript, od luźnej równości po modyfikację stanu, oraz pokazuje bezpieczniejsze patterny zastępujące każdy z nich.