Original Repository: ryanmcdermott/clean-code-javascript
- Wprowadzenie
- Zmienne
- Funkcje
- Obiekty i struktury danych
- Klasy
- SOLID
- Testowanie
- Współbieżność
- Obsługa błędów
- Formatowanie
- Komentarze
- Tłumaczenie
Zasady inżynierii oprogramowania z książki Roberta C. Martina Czysty kod, dostosowane do języka JavaScript. Nie są to wytyczne dotyczące stylu. To wytyczne do tworzenia czytelnego, prostego w refaktoryzacji i wielokrotnym użyciu oprogramowania w języku JavaScript.
Nie każda z podanych tu zasad musi być ściśle przestrzegana i nie wszystkie z nich będą powszechnie przyjęte. To nic więcej, niż wskazówki zebrane dzięki wieloletniemu doświadczeniu autorów Czystego kodu.
Inżynieria oprogramowania ma trochę ponad 50 lat i nadal wiele się uczymy. Gdy architektura oprogramowania będzie tak stara, jak sama architektura, wtedy może będziemy mieli trudniejsze zasady do przestrzegania. Na razie niech te wytyczne służą jako podstawa do oceny jakości kodu JavaScript, który Ty i Twój zespół tworzycie.
Jeszcze jedno: poznanie zasad nie zrobi z Ciebie lepszego programisty w mgnieniu oka, a wieloletnia praca zgodnie z nimi nie sprawi, że przestaniesz popełniać błędy. Każdy kawałek kodu zaczyna się od wstępnego szkicu i jest jak mokra glina formowana do ostatecznego kształtu. Wreszcie niczym rzeźbiarz dłutem usuwamy wszelkie niedoskonałości podczas przeglądu kodu wspólnie z kolegami. Nie zadręczaj się wstępnymi szkicami wymagającymi poprawek. Zamiast tego męcz kod!
Źle:
const yyyymmdstr = moment().format('YYYY/MM/DD');
Dobrze:
const currentDate = moment().format('YYYY/MM/DD');
Źle:
getUserInfo();
getClientData();
getCustomerRecord();
Dobrze:
getUser();
Będziemy czytać więcej kodu, niż kiedykolwiek napiszemy. Ważne jest, aby nasz kod był czytelny i przeszukiwalny. Nie nazywając zmiennych, mających znaczenie dla zrozumienia naszego programu, krzywdzimy czytających. Spraw, by Twoje nazwy były odnajdywalne. Narzędzia takie jak buddy.js i ESLint mogą pomóc zidentyfikować nienazwane stałe.
Źle:
// Czym do diaska jest 86400000?
setTimeout(blastOff, 86400000);
Dobrze:
// Zadeklaruj ją jako nazwaną stałą, używając wielkich liter.
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
Źle:
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2]);
Dobrze:
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
Jasne jest lepsze niż niejasne.
Źle:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Czekaj, czym było `l`?
dispatch(l);
});
Dobrze:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
Jeśli nazwa Twojej klasy/obiektu coś mówi, nie powtarzaj tego w nazwie zmiennej.
Źle:
const Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
Dobrze:
const Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
Domyślne wartości argumentów są zwykle jaśniejsze niż wykonania warunkowe. Pamiętaj, że jeśli
ich użyjesz, Twoja funkcja dostarczy domyślne wartości tylko dla argumentów niezdefiniowanych (undefined
). Inne "fałszywe" wartości, takie jak ''
, ""
, false
, null
, 0
i
NaN
, nie będą zastąpione wartością domyślną.
Źle:
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
Dobrze:
function createMicrobrewery(breweryName = 'Hipster Brew Co.') {
// ...
}
Ograniczanie ilości parametrów funkcji jest niezwykle ważne, gdyż czyni testowanie Twojej funkcji prostszym. Mając więcej niż trzy, prowadzisz do eksplozji kombinatorycznej, w której musisz przetestować masę różnych przypadków osobno z każdym kolejnym parametrem.
Najlepiej, jeśli jest jeden lub dwa parametry, trzy powinny być już w miarę możliwości unikane. Większa ilość powinna być skonsolidowana. Zwykle, gdy masz więcej niż dwa parametry, Twoja funkcja próbuje zrobić za dużo. W przypadkach, gdy tak nie jest, zazwyczaj obiekt wyższego rzędu będzie wystarczającym parametrem.
Jako że JavaScript pozwala tworzyć obiekty w locie i bez konieczności użycia kodu związanego z klasami, zawsze możesz użyć obiektu, gdy czujesz, że potrzebujesz dużo parametrów.
Aby było oczywistym, jakich parametrów oczekuje funkcja, możesz użyć destrukturyzacji wprowadzonej w wersji ES2015/ES6. Ma to kilka zalet:
- Gdy ktoś popatrzy na sygnaturę funkcji, od razu będzie mieć jasność, jakie właściwości będą wykorzystywane.
- Destrukturyzacja klonuje określone prymitywne wartości argumentów obiektu przekazywanego do funkcji. Może to pomóc w uniknięciu efektów ubocznych. Uwaga: obiekty i tablice będące wynikiem destrukturyzacji argumentów obiektu NIE będą klonowane.
- Lintery mogą ostrzec Cię przed nieużywanymi zmiennymi, co będzie niemożliwe bez destrukturyzacji.
Źle:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
Dobrze:
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
Jest to zdecydowanie najważniejsza zasada w inżynierii oprogramowania. Gdy funkcje wykonują więcej niż jedno zadanie, są trudniejsze w kompozycji, testowaniu i zrozumieniu. Jeśli możesz ograniczyć działanie funkcji do tylko jednego zadania, będzie ona prostsza w refaktoryzacji, a Twój kod czytelniejszy. Nawet jeśli nie wyniesiesz z tego przewodnika nic więcej poza tym, to i tak będziesz do przodu w stosunku do wielu deweloperów.
Źle:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Dobrze:
function emailActiveClients(clients) {
clients
.filter(isActiveClient)
.forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Źle:
function addToDate(date, month) {
// ...
}
const date = new Date();
// Z nazwy funkcji trudno wywnioskować, co jest dodawane
addToDate(date, 1);
Dobrze:
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
Gdy masz więcej niż jeden poziom abstrakcji, Twoja funkcja zwykle robi za dużo. Podzielenie funkcji umożliwi wielokrotne użycie kodu i ułatwi jego testowanie.
Źle:
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
});
}
Dobrze:
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// parse...
});
}
Rób wszystko, co tylko możesz, aby uniknąć powielania kodu. Powielony kod jest zły, gdyż oznacza, że jest więcej niż jedno miejsce do zmodyfikowania, gdy potrzebujesz zmienić trochę logiki.
Wyobraź sobie, że prowadzisz restaurację i sprawujesz nadzór nad swoimi zapasami: wszystkie pomidory, cebule, czosnek, przyprawy itd. Jeśli masz je spisane w wielu miejscach, to wszystkie z nich muszą zostać uaktualnione, gdy serwujesz danie z pomidorami. Mając jedną listę, do uaktualnienia jest tylko jedno miejsce!
Częstokroć powielasz kod, gdyż musisz rozwiązać dwa lub więcej nieznacznie różnych problemów, mających ze sobą wiele wspólnego, a ich różnice wymuszają na Tobie posiadanie dwóch lub więcej oddzielnych funkcji robiących wiele tego samego. Usunięcie powielonego kodu oznacza utworzenie abstrakcji, mogącej obsłużyć ten zestaw różnych problemów za pomocą tylko jednej funkcji/modułu/klasy.
Poprawne użycie abstrakcji jest istotne, dlatego też powinieneś przestrzegać zasad SOLID, znajdujących się w rozdziale Klasy. Zła abstrakcja może być gorsza, niż powielony kod, bądź więc ostrożny! Jeśli możesz zastosować dobrą abstrakcję, zrób to! Nie powtarzaj się, w przeciwnym razie skończysz uaktualniając wiele miejsc za każdym razem, gdy chcesz zmienić jedną rzecz.
Źle:
function showDeveloperList(developers) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Dobrze:
function showEmployeeList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case 'manager':
data.portfolio = employee.getMBAProjects();
break;
case 'developer':
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
Źle:
const menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);
Dobrze:
const menuConfig = {
title: 'Order',
// Użytkownik nie uwzględnił klucza 'body'
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// config ma teraz wartość: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
Flagi mówią użytkownikowi, że dana funkcja robi więcej niż jedną rzecz. Funkcje powinny robić jedną rzecz. Rozdziel swoje funkcje, jeśli ich kod podąża innymi ścieżkami zależnie od zmiennej boolowskiej.
Źle:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Dobrze:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
Funkcja daje skutki uboczne, gdy robi cokolwiek innego niż pobranie jednej wartości i zwrócenie innej lub innych. Skutkiem ubocznym może być zapis do pliku, zmodyfikowanie jakiejś zmiennej globalnej, lub też przypadkowe przelanie wszystkich Twoich pieniędzy nieznajomemu.
Czasem potrzebujesz skutków ubocznych w programie. Jak w poprzednim przykładzie możesz potrzebować zapisu do pliku. Tym, co chcesz zrobić, jest znalezienie jednego miejsca, gdzie go umieścisz. Nie miej kilku funkcji i klas, które zapisują do poszczególnych plików. Miej jedną usługę, która to robi. Jedną i tylko jedną.
Główną kwestią jest unikanie powszechnych pułapek, takich jak dzielenie stanu między obiektami bez jakiejkolwiek struktury, użycie mutowalnych typów danych, które mogą być nadpisane przez cokolwiek i nieokreślenie jednego miejsca dającego skutki uboczne. Jeśli możesz to zrobić, będziesz szczęśliwszy, niż zdecydowana większość programistów.
Źle:
// Globalna zmienna po której następuja funkcja, która się do niej odnosi
// Jeśli będziemy mieć kolejną funckję używającą tej nazwy, teraz będzie ona tablicą i może spowodować błąd.
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
Dobrze:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
W języku JavaScript typy proste przekazywane są przez wartość, a obiekty/tablice przez
referencję. W przypadku obiektów i tablic, jeśli funkcja dokona zmiany
w tablicy koszyka z zakupami, na przykład przez dodanie produktu,
to inna funkcja używająca tej tablicy koszka cart
będzie tą zmianą dotknięta.
Może to być dobre, jednak może też być i złe. Wyobraź sobie złą sytuację:
Użytkownik klika przycisk "Kup" wywołujący funkcję purchase
, która tworzy
żądanie i wysyła tablicę cart
do serwera. Z powodu
słabego połączenia sieciowego, funkcja purchase
musi powtarzać żądanie.
Co jeśli w międzyczasie użytkownik przypadkowo kliknie przycisk "Dodaj do koszyka"
na produkcie, który nie był dodany wcześniej?
Jeśli tak się wydarzy i żądanie zostanie wysłane, wtedy funkcja kupująca
wyśle przypadkowo dodany produkt, gdyż posiada ona referencję do tablicy koszyka
zakupów zmodyfikowaną przez funkcję addItemToCart
poprzez dodanie niechcianego
produktu.
Świetnym rozwiązaniem byłoby, aby addItemToCart
zawsze klonowała cart
,
edytowała i zwracała sklonowaną tablicę. To zapewnia, że żadna inna funkcja
przechowująca referencję do koszyka zakupów będzie dotknięta jakąkolwiek zmianą.
Dwa zastrzeżenia do tego podejścia:
-
Mogą występować przypadki, w których rzeczywiście chcesz zmodyfikować wejściowy obiekt, ale gdy zaczniesz stosować tę praktykę w programowaniu, przekonasz się, że są one dość rzadkie. Większość może być zrefaktoryzowana bez skutków ubocznych!
-
Klonowanie dużych obiektów może być kosztowne pod względem wydajności. Na szczęście w praktyce nie jest to duży problem, gdyż mamy świetne biblioteki pozwalające takiemu podejściu do programowania być szybkim i nieobciążającym pamięci aż tak, jak by to było w przypadku ręcznego klonowania obiektów i tablic.
Źle:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
Dobrze:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
Zanieczyszczanie globalnej przestrzeni jest złą praktyką w języku JavaScript,
gdyż możesz kolidować z inną biblioteką i użytkownik Twojego API może być niczego nieświadomym, dopóki wyjątek nie pojawi się na produkcji. Pomyślmy o takim przykładzie:
co jeśli chciałbyś rozszerzyć natywny obiekt Array, aby miał metodę diff
, która
może pokazać różnicę między dwiema tablicami? Mógłbyś przypisać swoją nową funkcję
do Array.prototype
, ale może to kolidować z inną biblioteką, która próbowała
zrobić to samo. Co jeśli inna biblioteka używała diff
do znalezienia
różnicy między pierwszym i ostatnim elementem tablicy? Właśnie dlatego
byłoby dużo lepiej używać po prostu klas wprowadzonych w wersji ES2015/ES6
i zwyczajnie rozszerzyć globalną Array
.
Źle:
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
Dobrze:
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
JavaScript nie jest językiem funkcyjnym w takim stopniu, jak Haskell, ale zawiera trochę funkcyjnego aromatu. Języki funkcyjne mogą być czystsze i prostsze w testowaniu. Preferuj ten styl programowania, kiedy tylko możesz.
Źle:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
Dobrze:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = programmerOutput
.map(output => output.linesOfCode)
.reduce((totalLines, lines) => totalLines + lines);
Źle:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
Dobrze:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
Źle:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
Dobrze:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
Wydaje się to być zadaniem niewykonalnym. Większość ludzi słysząc to pierwszy raz, powie:
"Jak mam zrobić cokolwiek bez instrukcji if
?" Odpowiedzią jest, że
możesz użyć polimorfizmu, aby osiągnąć to samo w wielu przypadkach. Drugim
pytaniem jest zwykle: "Dobrze, to świetnie, ale dlaczego chciałbym to zrobić?"
Odpowiedzią jest poprzednio poznana koncepcja czystego kodu: funkcja powinna robić
tylko jedną rzecz. Gdy masz klasy i funkcje zawierające instrukcje if
,
mówisz użytkownikowi, że Twoja funkcja robi więcej, niż jedną rzecz. Pamiętaj,
rób po prostu jedną rzecz.
Źle:
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
Dobrze:
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
JavaScript jest typowany dynamicznie, co oznacza, że Twoje funkcje mogą przyjmować argumenty dowolnego typu. Czasami ta wolność jest uciążliwa i sprawdzanie typów w Twoich funkcjach okazuje się kuszące. Jest wiele sposobów, aby tego uniknąć. Pierwszym jest rozważenie zwartych API.
Źle:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
Dobrze:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
Jeśli pracujesz z podstawowymi wartościami jak ciągi znaków i tablice i nie możesz użyć polimorfizmu, a nadal czujesz potrzebę sprawdzania typów, powinieneś rozważyć użycie języka TypeScript. Jest on świetną alternatywą dla normalnego języka JavaScript, gdyż dostarcza statycznego typowania do jego standardowej składni. Problemem z ręcznym sprawdzaniem typów w normalnym JavaScript jest to, że używanie go poprawnie wymaga na tyle dużo dodatkowej rozwlekłości, iż otrzymane sztuczne bezpieczeństwo typologiczne nie wynagradza utraty czytelności. Utrzymuj Twój JavaScript czystym, pisz dobre testy i miej dobre przeglądy kodu. W przeciwnym razie rób wszystko to samo, ale używając TypeScript (który, jak powiedziałem, jest świetną alternatywą!).
Źle:
function combine(val1, val2) {
if (typeof val1 === 'number' && typeof val2 === 'number' ||
typeof val1 === 'string' && typeof val2 === 'string') {
return val1 + val2;
}
throw new Error('Must be of type String or Number');
}
Dobrze:
function combine(val1, val2) {
return val1 + val2;
}
Nowoczesne przeglądarki w czasie wykonania dokonują wielu optymalizacji "pod maską". Często gdy optymalizujesz, po prostu tracisz swój czas. Tu są dobre źródła pokazująca niedostatki optymalizacji. Postaw je sobie za cel w międzyczasie, dopóki nie będą poprawione, jeśli mogą być.
Źle:
// W starych przeglądarkach każda iteracja z niebuforowanym `list.length` byłaby kosztowna
// w związku z ponownym obliczeniem `list.length`. W nowoczesnych przeglądarkach jest to zoptymalizowane.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Dobrze:
for (let i = 0; i < list.length; i++) {
// ...
}
Martwy kod jest po prostu tak samo zły, jak powielony kod. Nie ma powodu, aby go trzymać. Jeśli nie będzie wywoływany, pozbądź się go! Będzie nadal bezpieczny w historii Twojego systemu wersjonowania, jeśli ciągle go potrzebujesz.
Źle:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Dobrze:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Używanie getterów i setterów, aby uzyskać dostęp do danych obiektów, może być lepsze, niż zwykłe sprawdzanie właściwości obiektu. Możesz spytać: "Dlaczego?". Hmmm... oto niektóre z powodów:
- Jeśli chcesz robić coś ponad pobieranie właściwości obiektu, nie musisz sprawdzać i zmieniać każdego akcesora w swoim kodzie.
- Dodanie walidacji jest proste podczas wykonywania
set
. - Hermetyzuje wewnętrzną reprezentację.
- Łatwo dodać logowanie i obsługę błędów podczas pobierania i ustawiania.
- Możesz zastosować leniwe ładowanie właściwości Twojego obiektu, przykładowo, pobierając je z serwera.
Źle:
function makeBankAccount() {
// ...
return {
balance: 0,
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
Dobrze:
function makeBankAccount() {
// ta jest prywatna
let balance = 0;
// "getter" udostępniony publicznie przez zwrócenie obiektu poniżej
function getBalance() {
return balance;
}
// "setter" udostępniony publicznie przez zwrócenie obiektu poniżej
function setBalance(amount) {
// ... walidacja przed uaktualnieniem zmiennej "balance"
balance = amount;
}
return {
// ...
getBalance,
setBalance,
};
}
const account = makeBankAccount();
account.setBalance(100);
Można to osiągnąć dzięki domknięciom (dla wersji ES5 i niższych).
Źle:
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee('John Doe');
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
Dobrze:
function makeEmployee(name) {
return {
getName() {
return name;
},
};
}
const employee = makeEmployee('John Doe');
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
Trudno uzyskać czytelne dziedziczenie, konstrukcję i definicje metod klasycznymi technikami dostępnymi w ES5. Gdy potrzebujesz dziedziczenia (a bądź świadom, że może nie musisz), wykorzystuj klasy wprowadzone w ES2015/ES6. Niemniej jednak przedkładaj małe funkcje ponad klasy, dopóki nie będziesz potrzebował większych i bardziej złożonych obiektów.
Źle:
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error('Instantiate Animal with `new`');
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error('Instantiate Mammal with `new`');
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error('Instantiate Human with `new`');
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};
Dobrze:
class Animal {
constructor(age) {
this.age = age;
}
move() { /* ... */ }
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() { /* ... */ }
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() { /* ... */ }
}
Wzorzec ten jest bardzo przydatny w języku JavaScript i widać to w wielu bibliotekach takich jak
jQuery i Lodash. Pozwala on uzyskać kod ekspresywny i mniej rozwlekły.
W związku z tym mówię - użyj łańcuchowania metod i popatrz, jak czysty będzie Twój kod.
W funkcjach Twoich klas zwyczajnie zwracaj this
na końcu każdej funkcji
i będziesz mógł doczepić do nich (jak ogniwa łańcucha) inne metody klasy.
Źle:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car('Ford','F-150','red');
car.setColor('pink');
car.save();
Dobrze:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// ZAUWAŻ: Zwracamy this dla łańcuchowania
return this;
}
setModel(model) {
this.model = model;
// ZAUWAŻ: Zwracamy this dla łańcuchowania
return this;
}
setColor(color) {
this.color = color;
// ZAUWAŻ: Zwracamy this dla łańcuchowania
return this;
}
save() {
console.log(this.make, this.model, this.color);
// ZAUWAŻ: Zwracamy this dla łańcuchowania
return this;
}
}
const car = new Car('Ford','F-150','red')
.setColor('pink')
.save();
Jak stwierdzono głośno we Wzorcach projektowych Bandy Czterech, powinieneś przedkładać kompozycję ponad dziedziczenie tam, gdzie tylko możesz. Jest wiele dobrych powodów, aby używać dziedziczenia i wiele dobrych powodów, aby używać kompozycji. Głównym punktem tej maksymy jest, aby gdy w myślach instynktownie skłaniasz się ku dziedziczeniu, próbować zastanowić się, czy kompozycja może odwzorować problem lepiej. W niektórych przypadkach może.
Możesz się zastanawiać: "kiedy powinienem użyć dziedziczenia?". To zależy od Twojego problemu, ale tu jest skromna lista przypadków, gdy dziedziczenie ma więcej sensu niż kompozycja:
- Twoje dziedziczenie reprezentuje relację "x-jest-y" a nie "x-posiada-y" (Człowiek->Zwierzę kontra Użytkownik->SzczegółyUżytkownika).
- Możesz wykorzystać ponownie kod ze zbioru swoich klas (ludzie mogą poruszać się jak wszystkie zwierzęta).
- Chcesz dokonać globalnych zmian w klasach pochodnych zmieniając klasę podstawową (zmienić zużycie kalorii podczas poruszania dla wszystkich zwierząt).
Źle:
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Źle, gdyż Pracownicy "posiadają" dane podatkowe. DanePodatkowePracownika nie są typem Pracownika
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
Dobrze:
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
Jak stwierdzono w Czystym Kodzie, "Nigdy nie powinno być więcej niż jednego powodu do modyfikacji klasy". Kuszącym jest zapakowanie w klasę wielu funkcjonalności, tak jak wtedy, gdy możesz zabrać tylko jedną walizkę podczas lotu. Problemem jest tutaj to, że Twoja klasa nie będzie koncepcyjnie spójna i da jej to wiele powodów do zmian. Minimalizacja ilości sytuacji, w których musisz zmienić klasę, jest istotna. Jest istotna, gdyż jeśli jedna klasa zawiera zbyt dużo funkcjonalności i modyfikujesz część z nich, może stać się trudnym do zrozumienia, jak wpłynie to na inne zależne moduły w Twoim kodzie.
Źle:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Dobrze:
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
Jak stwierdził Bertrand Meyer, "Encje (klasy, moduły, funkcje itd.) powinny być otwarte na rozszerzenie, ale zamknięte na modyfikacje". Co więc to oznacza? Ta zasada po prostu stwierdza, że powinieneś umożliwić użytkownikom dodanie nowych funkcjonalności bez zmiany istniejącego kodu.
Źle:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return makeAjaxCall(url).then((response) => {
// przekształć odpowiedź i zwróć
});
} else if (this.adapter.name === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// przekształć odpowiedź i zwróć
});
}
}
}
function makeAjaxCall(url) {
// żądanie i zwrócenie obietnicy
}
function makeHttpCall(url) {
// żądanie i zwrócenie obietnicy
}
Dobrze:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
request(url) {
// żądanie i zwrócenie obietnicy
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
request(url) {
// żądanie i zwrócenie obietnicy
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then((response) => {
// przekształć odpowiedź i zwróć
});
}
}
Jest to przerażająca nazwa dla bardzo prostego pojęcia. Jest formalnie zdefiniowana jako "Jeśli S jest podtypem T, wtedy obiekty typu T mogą być wymienione z obiektami typu S (np. obiekty typu S mogą zastąpić obiekty typu T) bez zmieniania żadnych pożądanych właściwości tego programu (poprawność, zadanie wykonane itd.)". To jeszcze bardziej przerażająca definicja.
Najlepszym wyjaśnieniem tego będzie, jeśli masz klasę bazową i klasę potomną, wtedy klasy bazowa i potomna mogą zostać użyte wymiennie bez otrzymania niepoprawnych wyników. Może to być nadal pogmatwane, spójrzmy więc na klasyczny przykład: Kwadrat-Prostokąt. Matematycznie kwadrat jest prostokątem, ale jeśli modelujesz to używając relacji "x-jest-y" przez dziedziczenie, szybko wpadniesz w kłopoty.
Źle:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach((rectangle) => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // ŹLE: Zwraca 25 dla Kwadratu. Powinno być 20
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Dobrze:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
JavaScript nie posiada interfejsów, więc ta zasada nie ma tu tak restrykcyjnego zastosowania, jak pozostałe. Mimo tego jest ważna i istotna nawet przy braku typowania w JavaScript.
ISP stwierdza, że "Na klientach nie powinna być wymuszana zależność od interfejsów, których oni nie używają". Interfejsy w JavaScript są niejawnymi kontraktami przez kacze typowanie.
Dobrym przykładem demonstrującym tę zasadę w JavaScript są klasy, które wymagają dużych obiektów z opcjami. Nie wymaganie od klientów instalowania ogromnych ilości ustawień jest korzystne, gdyż przez większość czasu nie będą oni potrzebować wszystkich tych ustawień. Uczynienie ich opcjonalnymi pomoże zapobiec otrzymaniu "grubego interfejsu".
Źle:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule() {} // W większości przypadków nie będziemy musieli animować podczas trawersowania.
// ...
});
Dobrze:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule() {}
}
});
Ta zasada określa dwie istotne rzeczy:
- Wysokopoziomowe moduły nie powinny zależeć od modułów niskopoziomowych. Jedne i drugie powinny zależeć od abstrakcji.
- Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.
Z początku może to być trudne do zrozumienia, ale jeśli pracowałeś z AngularJS, widziałeś implementację tej zasady w formie Wstrzykiwania zależności (DI). Podczas gdy nie są one identycznymi pojęciami, Zasada odwrócenia zależności trzyma wysokopoziomowe moduły z dala od wiedzy na temat szczegółów ich niskopoziomowych modułów i ich ustawiania. Może to być osiągnięte dzięki Wstrzykiwaniu zależności. Ogromną korzyścią jest, iż redukuje to zależności między modułami. Zależność jest bardzo złym wzorcem, gdyż czyni Twój kod trudniejszym do zrefaktoryzowania.
Jak wcześniej zaznaczono, JavaScript nie posiada interfejsów, więc abstrakcje
będące zależnymi, są niejawnymi kontraktami. Oznacza to metody
i właściwości, które obiekt/klasa wystawia dla innego obiektu/klasy.
W przykładzie poniżej niejawnym kontraktem jest to, że dowolny moduł Request dla
InventoryTracker
będzie posiadał metodę requestItems
.
Źle:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// ŹLE: Utworzyliśmy zależność od specyficznej implementacji żądania.
// Powinniśmy po prostu uczynić requestItems zależnym od metody: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();
Dobrze:
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ['WS'];
}
requestItem(item) {
// ...
}
}
// Przez skonstruowanie naszych zależności na zewnątrz i wstrzyknięcie ich, możemy łatwo
// zastąpić nasz moduł żądania nowym, fantazyjnym, używającym WebSockets.
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();
Testowanie jest ważniejsze niż dostarczanie. Jeśli nie masz testów albo jest ich nieodpowiednia ilość, to za każdym razem dostarczając swój kod, nie będziesz pewnym, że czegoś nie popsułeś. Decyzja o tym, jaka ilość testów jest odpowiednia, należy do Twojego zespołu, ale pokrycie w 100% (wszystkie instrukcje i gałęzie) jest tym, co pozwoli osiągnąć wysoką pewność i święty spokój dewelopera. Oznacza to, że jako dodatek do posiadanego świetnego frameworka do testowania, musisz jeszcze użyć dobrego narzędzia pokrycia.
Nie ma usprawiedliwienia dla niepisania testów. Jest [mnóstwo dobrych frameworków testowych] (http:https://jstherightway.org/#testing-tools), znajdź więc ten, który Twój zespół preferuje. Kiedy go znajdziesz, wtedy postaw sobie za cel, aby zawsze pisać testy dla każdej nowej funkcjonalności/modułu, który wprowadzasz. Jeśli preferowaną przez Ciebie metodą jest Test Driven Development (TDD), to świetnie, ale istotą jest po prostu upewnienie się, że osiągasz swoje cele dotyczące pokrycia przed wypuszczeniem jakiejkolwiek funkcjonalności albo refaktoryzacji już istniejącej.
Źle:
import assert from 'assert';
describe('MakeMomentJSGreatAgain', () => {
it('handles date boundaries', () => {
let date;
date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
Dobrze:
import assert from 'assert';
describe('MakeMomentJSGreatAgain', () => {
it('handles 30-day months', () => {
const date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
});
it('handles leap year', () => {
const date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
});
it('handles non-leap year', () => {
const date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
Wywołania zwrotne nie są czyste i powodują nadmierne ilości zagnieżdżeń. W ES2015/ES6 Obietnice są wbudowanym, globalnym typem. Używaj ich!
Źle:
import { get } from 'request';
import { writeFile } from 'fs';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile('article.html', response.body, (writeErr) => {
if (writeErr) {
console.error(writeErr);
} else {
console.log('File written');
}
});
}
});
Dobrze:
import { get } from 'request';
import { writeFile } from 'fs';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
Obietnice są bardzo czystą alternatywą dla wywołań zwrotnych, ale ES2017/ES8
wprowadza async i await, które oferują jeszcze czystsze rozwiązanie.
Wszystkim, czego potrzebujesz, jest funkcja poprzedzona słowem kluczowym
async
i wtedy możesz pisać swoją logikę imperatywnie bez łańcucha funkcji
z then
. Używaj tego, jeśli możesz skorzystać z funkcjonalności ES2017/ES8 już dziś!
Źle:
import { get } from 'request-promise';
import { writeFile } from 'fs-promise';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
Dobrze:
import { get } from 'request-promise';
import { writeFile } from 'fs-promise';
async function getCleanCodeArticle() {
try {
const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
await writeFile('article.html', response);
console.log('File written');
} catch(err) {
console.error(err);
}
}
Wyrzucane błędy są czymś dobrym! Oznaczają, że w czasie wykonania zostało poprawnie zidentyfikowane coś, co poszło źle w Twoim programie i daje Ci znać, aby zatrzymać wykonywanie funkcji na obecnym stosie, zamknąć proces (w Node) i poinformować Cię w konsoli ze śladem stosu.
Nie zrobienie niczego z przechwyconym błędem nie daje Ci możliwości naprawienia
albo zareagowania na ten błąd. Logowanie błędu do konsoli (console.log
)
nie jest dużo lepsze, gdyż może on zaginąć w morzu rzeczy informacji do konsoli.
Jeśli zawrzesz jakikolwiek kawałek kodu w bloku try/catch
, oznacza to, że
myślisz o błędzie mogącym tam wystąpić i w związku z tym powinieneś mieć plan
albo utworzyć ścieżkę kodu do miejsca, w którym wystąpi.
Źle:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
Dobrze:
try {
functionThatMightThrow();
} catch (error) {
// Jedna z opcji (głośniejsza niż console.log):
console.error(error);
// Inna opcja:
notifyUserOfError(error);
// Inna opcja:
reportErrorToService(error);
// ALBO zastosuj wszystkie trzy!
}
Z tych samych powodów, dla których nie powinieneś ignorować przechwyconych błędów
w try/catch
.
Źle:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
console.log(error);
});
Dobrze:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
// Jedna z opcji (głośniejsza niż console.log):
console.error(error);
// Inna opcja:
notifyUserOfError(error);
// Inna opcja:
reportErrorToService(error);
// ALBO zastosuj wszystkie trzy!
});
Formatowanie jest subiektywne. Jak w wielu innych tu przypadkach, nie ma sztywnej i szybkiej zasady, którą musisz przyjąć. Najważniejsze, abyś NIE SPIERAŁ SIĘ o formatowanie. Są tony narzędzi, by to zautomatyzować. Użyj jednego! Spieranie się o formatowanie jest stratą czasu i pieniędzy dla inżynierów.
W sprawach, które nie są objęte automatycznym formatowaniem (wcięcia, tabulacje kontra spacje, podwójne kontra pojedyncze cudzysłowy itd.) zaglądnij tu po trochę wskazówek.
JavaScript jest typowany dynamicznie, więc wielkie litery powiedzą Ci dużo o Twoich zmiennych, funkcjach itd. Te zasady są subiektywne, więc Twój zespół może wybrać cokolwiek chce. Chodzi o to, żebyś bez względu na to, co wybierzecie, był konsekwentnym.
Źle:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}
Dobrze:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}
Jeśli funkcja wywołuje inną, trzymaj te funkcje wertykalnie blisko w pliku źródłowym. Najlepiej umieść wywołanie zaraz powyżej wywołanej. Mamy tendencję do czytania kodu z góry na dół jak gazetę. Dlatego też spraw, aby Twój kod był czytany w ten sposób.
Źle:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
lookupPeers() {
return db.lookup(this.employee, 'peers');
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getManagerReview() {
const manager = this.lookupManager();
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
Dobrze:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
lookupPeers() {
return db.lookup(this.employee, 'peers');
}
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
Komentarze są przeprosinami, nie wymogiem. Dobry kod przeważnie dokumentuje się sam.
Źle:
function hashIt(data) {
// Hash
let hash = 0;
// Długość łańcucha znaków
const length = data.length;
// Pętla po literach w zmiennej data
for (let i = 0; i < length; i++) {
// Pobierz kod litery.
const char = data.charCodeAt(i);
// Utwórz hash
hash = ((hash << 5) - hash) + char;
// Przekonwertuj na liczbę 32-bitową
hash &= hash;
}
}
Dobrze:
function hashIt(data) {
let hash = 0;
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
// Przekonwertuj na liczbę 32-bitową
hash &= hash;
}
}
Kontrola wersji istnieje nie bez powodu. Pozostaw stary kod w Twojej historii.
Źle:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
Dobrze:
doStuff();
Pamiętaj, używaj kontroli wersji! Nie jest potrzebny martwy kod, zakomentowany kod
i przede wszystkim komentarze będące dziennikiem. Używaj git log
, by sprawdzić historię!
Źle:
/**
* 2016-12-20: Usunąłem monady, nie rozumiałem ich (RM)
* 2016-10-01: Ulepszyłem użycie specjalnych monad (JP)
* 2016-02-03: Usunąłem sprawdzanie typów (LI)
* 2015-03-14: Dodałem funkcję combine ze sprawdzaniem typów (JR)
*/
function combine(a, b) {
return a + b;
}
Dobrze:
function combine(a, b) {
return a + b;
}
Zwykle dodają one tylko szum. Pozwól funkcjom i nazwom zmiennych wraz z poprawnymi wcięciami i formatowaniem dać wizualną strukturę Twojemu kodowi.
Źle:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: 'foo',
nav: 'bar'
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
Dobrze:
$scope.model = {
menu: 'foo',
nav: 'bar'
};
const actions = function() {
// ...
};
Ten dokument dostępny jest również w innych językach:
Brazilian Portuguese: fesnt/clean-code-javascript
Spanish: andersontr15/clean-code-javascript
Chinese:
German: marcbruederlin/clean-code-javascript
Korean: qkraudghgh/clean-code-javascript-ko
Polish: greg-dev/clean-code-javascript-pl
Russian:
Vietnamese: hienvd/clean-code-javascript/
Japanese: mitsuruog/clean-code-javascript/
Indonesia: andirkh/clean-code-javascript/