Czytelny JavaScript na przykładzie: Dziesięć refaktoryzacji typu „przed i po”
Te niewielkie modyfikacje w JavaScript i React – od nazewnictwa oraz obiektów parametrów po obsługę błędów i formatowanie – sprawiają, że kod jest łatwiejszy do odczytania i modyfikacji dla kolejnego programisty.
Źródła: 10 Clean Code Principles I Wish I Knew When I Started Coding
Zapewnienie działania danej funkcji to pierwsza połowa pracy. Druga połowa polega na tym, aby kolega z zespołu lub ty sam po sześciu miesiącach potrafił przeczytać ten kod, bezpiecznie go zmienić i nie błędnie go zinterpretować. Ten artykuł przedstawia dziesięć konkretnych przykładów refaktoryzacji typu „przed i po” w JavaScript i React, z wyjaśnieniem uzasadnień oraz momentem, w którym rady przestają być pomocne.
1. Nadawaj nazwy wartościom zgodnie z tym, co reprezentują
Krótkie, tajemnicze nazwy zmuszają czytelnika do szukania informacji gdzie indziej. W tym fragmencie nic nie wskazuje, czym jest d ani jaką właściwość posiada a.
const d = 30;
const x = users.filter(u => u.a);
Przeniesienie nazw w obu miejscach sprawia, że intencja staje się widoczna tam, gdzie są one używane. Liczba staje się długością próby:
const trialPeriodDays = 30;
A filtr teraz brzmi jak zdanie opisujące aktywnych użytkowników:
const activeUsers = users.filter(user => user.isActive);
Dobry nazwa mówi, co reprezentuje wartość; nazwy ogólne takie jak data prawie nic nie mówią.
const data = getData();
Nazwa, która określa koncepcję z danej dziedziny, jest znacznie bardziej informacyjna:
const customerOrders = getCustomerOrders();
Jeśli czujesz potrzebę dodania komentarza wyjaśniającego zmienną, to zazwyczaj oznacza, że nazwa powinna sama zawierać tę informację.
2. Nadaj każdej funkcji jedną rolę
Funkcja, która waliduje dane wejściowe, przekształca je, zapisuje w bazie danych, wysyła e-maile i rejestruje działania, jest trudna do zrozumienia i jeszcze trudniejsza do testowania, ponieważ każdy test musi jednocześnie obsłużyć wszystkie te zadania. Poniższy zarys pokazuje taką funkcję z każdą odpowiedzialnością włączoną bezpośrednio.
function createUser(user) {
// validate user
// format data
// save to database
// send email
// log activity
}
Rozbicie jej na nazwane kroki przekształca funkcję najwyższego poziomu w czytelną syntezę. Zaczyna się od walidacji:
function createUser(user) {
validateUser(user);
i kontynuuje się formatowaniem, zapisem oraz wysyłką e-maila powitalnego – każda z tych czynności jest przypisana do osobnej funkcji:
const formattedUser = formatUser(user); saveUser(formattedUser); sendWelcomeEmail(formattedUser);
}
Teraz createUser() przypomina listę kroków, a każda z funkcji pomocniczych może być testowana oddzielnie. Wadą jest pośredniość: osoba, która potrzebuje szczegółów, musi zaglądać do każdej z funkcji pomocniczych, dlatego ich nazwy muszą być na tyle precyzyjne, by rzadko to konieczne było.
3. Usuń duplikaty o tym samym powodzie zmiany
Powtarzająca się logika nadaje się do wyodrębnienia. W tym przypadku ta sama formuła podatkowa pojawia się trzy razy.
const total1 = price1 + price1 * 0.15;
const total2 = price2 + price2 * 0.15;
const total3 = price3 + price3 * 0.15;
Umieszczenie tego w funkcji daje regule nazwę oraz jedno konkretne miejsce zastosowania:
function calculatePriceWithTax(price) {
return price + price * 0.15;
}
Każde miejsce wywołania następnie po prostu stosuje tę regułę:
const total1 = calculatePriceWithTax(price1);
const total2 = calculatePriceWithTax(price2);
const total3 = calculatePriceWithTax(price3);
Jeśli stopa zmienia się z 15% na 18%, wystarczy zmienić jedną linię zamiast przeszukiwać cały projekt. Ważne jest, aby usuwać rzeczywiste duplikaty, a nie każdą parę linii, która przypadkowo wygląda podobnie. Dwa fragmenty kodu, które są identyczne obecnie, ale zmieniają się z różnych powodów, lepiej pozostawić oddzielne; ich łączenie powiązuje elementy, które powinny rozwijać się niezależnie. Artykuł na temat kiedy powinien istnieć wspólny kod szczegółowo omawia ten kompromis.
4. Zastąp długie listy parametrów pozycyjnych obiektem
Argumenty pozycyjne ukrywają swoje znaczenie w miejscu wywołania. Patrząc na to wywołanie, nie można stwierdzić, co oznaczają 25 lub true, bez otwarcia definicji funkcji.
createUser(
"Shayan",
"shayan@example.com",
25,
"developer",
true,
"Pakistan"
);
Przekazywanie pojedynczego obiektu z nazwami właściwości sprawia, że każda wartość jest samoodnosząca się:
createUser({
name: "Shayan",
email: "shayan@example.com",
age: 25,
role: "developer",
isActive: true,
country: "Pakistan"
});
Taki styl jest już standardem w treściach API, obiektach konfiguracyjnych oraz propach React, więc w większych aplikacjach wydaje się naturalny.
5. Wolimy oczywisty kod od sprytnego kodu
Kompresowanie logiki na jedną linię może utrudnić jej odczyt. Poniższa linia łączy opcjonalne wywołania za pomocą skróconych nazw właściwości.
const result = users?.filter(u => u.a)?.map(u => u.n);
Rozbicie tego na dwa kroki z opisowymi nazwami sprawia, że każda transformacja jest wyraźna. Najpierw wybierzmy aktywnych użytkowników:
const activeUsers = users.filter(user => user.isActive);
Następnie wydobyjmy ich nazwy:
const activeUserNames = activeUsers.map(user => user.name);
To wymaga dodatkowego wiersza, ale czytelnik rozumie to od razu. Zwróć uwagę na jedną różnicę w zachowaniu: oryginał używał łańcuchowania opcjonalnego, więc tolerował sytuację, gdy users miało wartość null lub undefined, podczas gdy wersja przepisana rzuca błąd w takim przypadku. Jeśli users może faktycznie być nieobecne, należy to obsłużyć wprost (na przykład używając wartości domyślnej []) zamiast ukrywać to w łańcuchu ?.. Zasadą jest, aby kod był przejrzysty, a nie imponujący.
6. Planuj awarie, a nie tylko pomyślne scenariusze
Prawdziwe systemy zawodzą: API zwracają błędy, użytkownicy wysyłają nieważne dane, sieci się rozrywają, bazy danych tracą połączenie. Poniższy kod zakłada, że wszystko przebiega pomyślnie.
const response = await fetch("/api/users");
const users = await response.json();
Kluczowym szczegółem jest to, że fetch odrzuca żądanie tylko w przypadku awarii sieci; błędy HTTP 404 lub 500 są nadal rozwiązywane. Ulepszona wersja otacza żądanie blokiem try:
try {
const response = await fetch("/api/users");
a następnie sprawdza response.ok, rzuca wyjątek przy nieudanym stanie, parsuje treść i łapie wszystkie błędy:
if (!response.ok) {
throw new Error("Failed to fetch users");
} const users = await response.json(); return users;
} catch (error) {
console.error("Unable to load users:", error);
}
Dzięki temu awarie stają się widoczne i znacznie łatwiejsze do debugowania. Należy pamiętać, że blok catch tutaj loguje błąd, a następnie domyślnie zwraca undefined, więc wywołujący nie mogą odróżnić awarii od pustego wyniku. W prawdziwym kodzie należy albo ponownie rzucić wyjątek po zalogowaniu, albo zwrócić wyraźną wartość, którą można sprawdzić, aby błąd mógł zostać obsłużony tam, gdzie faktycznie można z nim poradzić sobie.
7. Komentuj dlaczego, a nie co
Komentarze są przydatne; te zbędne stanowią szum. Ten komentarz dokładnie powtarza to, co robi dana linia kodu.
// Increment counter by 1
counter++;
Dobry komentarz wyjaśnia decyzję, której kod sam w sobie nie może oddać, na przykład dlaczego żądanie jest celowo opóźniane:
// Delay the request to avoid triggering the API rate limit.
setTimeout(fetchUsers, 1000);
Praktyczna zasada: niech kod mówi o tym, co robi, a komentarze wyjaśniają, dlaczego to robi.
8. Unikaj umieszczania reguł biznesowych w JSX
W komponentach React łatwo jest bezpośrednio włączyć filtrację i obliczenia do kodu markup. Ten wynik renderowania stosuje dwa filtry bezpośrednio przed przetwarzaniem produktów.
<div>
{products
.filter(product => product.stock > 0)
.filter(product => product.price < 100)
.map(product => (
<ProductCard product={product} />
))}
</div>
Przeniesienie tej reguły do zmiennej o nazwie oddziela to, co stanowi dostępny produkt, od sposobu jego wyświetlania:
const availableProducts = products.filter(
product => product.stock > 0 && product.price < 100
);
Wtedy JSX opisuje jedynie interfejs:
<div>
{availableProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
Zauważ, że wersja po refaktoryzacji dodaje również właściwość key, której brakowało w oryginale i która jest potrzebna React do prawidłowego śledzenia elementów listy.
9. Niech narzędzia zapewniają spójne formatowanie
Formatowanie wydaje się błahostką, dopóki zbiór kodu nie łączy różnych stylów. Tutaj ta sama zmienna jest deklarowana na trzy różne sposoby przez trzech programistów: camelCase,
const userName = "Shayan";
snake_case,
const user_name = "Shayan";
i PascalCase bez odstępów ani średnika:
const UserName="Shayan"
Wszystkie trzy wersje działają, ale ta niespójność sprawia problemy za każdym razem, gdy ktoś czyta lub szuka w kodzie. Narzędzia takie jak Prettier i ESLint eliminują ten problem: Prettier automatycznie formatuje kod, a ESLint może egzekwować zasady nazywania elementów i wskazywać częste błędy.
10. Pisz tak, aby było to zrozumiałe dla następnego administratora
To zasada łączy pozostałe ze sobą. Osoba, która otworzy twój plik, może być nowym kolegą, współpracownikiem lub twoim przyszłym ja – a twoje przyszłe ja zapomniało już o wszystkim, co myślałeś dzisiaj.
Zanim zatwierdzisz zmiany, zastanów się, czy po sześciu miesiącach, otwierając plik od nowa, byłbyś w stanie go zrozumieć. Jeśli nie, uproszcz go: przemień nazwy zmylających zmiennych, podziel duże funkcje, usuń duplikaty, które mają ten sam powód do zmiany, oraz udoskonal strukturę. Podstawowym celem jest zmniejszenie ilości myślenia potrzebnego do zrozumienia kodu. Aby poznać więcej nawyków w tym duchu, sprawdź nawyki, które ułatwiają następnemu programiście modyfikację kodu.
Podsumowanie
Czysty kod nie oznacza doskonałego kodu. Terminy, zmieniające się wymagania oraz rozwijające się projekty oznaczają, że każdy zespół pisze trochę nieuporządkowanego kodu. Ważna jest kierunek: każda zmiana powinna sprawić, że kod stanie się nieco jaśniejszy, prostszy i łatwiejszy w utrzymaniu.
- Nazwy powinny wskazywać, co reprezentuje dana wartość.