Original Repository: ryanmcdermott/clean-code-javascript
- Вступ
- Змінні
- Функції
- Об'єкти та структури даних
- Класи
- SOLID
- Тестування
- Асинхронність
- Обробка помилок
- Форматування
- Коментарі
- Переклад
Принципи програмної інженерії з книги Роберта С. Мартіна Clean Code, адаптовані для JavaScript. Це не гайд зі стилю кода. Це посібник для розробки програмного забезпечення на JavaScript, котре легко читати, повторно використовувати і рефакторити.
Не кожен згаданий тут принцип обов'язковий до дотримання, і лише деякі з них не викличуть розбіжностей. Це поради і нічого більше, але їх документували на основі багаторічного колективного досвіду авторів Clean Code.
Нашому ремеслу програмної інженерії трохи більше 50 років, і ми все ще багато чого пізнаємо. Коли архітектура програмного забезпечення буде такою ж старою, як і сама архітектура, можливо тоді в нас будуть більш жорсткі правила до дотримання. А зараз нехай ці поради слугують критерієм для оцінки JavaScript коду, який створюєте ви і ваша команда.
Ще одна річ: знання цих принципів не зробить вас кращим розробником миттєво, і багаторічна праця з ними не значить, що ви не будете робити помилок. Кожен фрагмент коду починається з чорнового варіанту, подібно мокрій глині, що набуває своєї кінцевої форми. По завершенню, ми винищуємо недоліки, коли рецензуємо код з колегами. Не коріть себе за перші чорнові версії коду, що потребують поліпшення. Поліпшуйте код замість цього!
Погано:
constyyyymmdstr=moment().format("YYYY/MM/DD");Добре:
constcurrentDate=moment().format("YYYY/MM/DD");Погано:
getUserInfo();getClientData();getCustomerRecord();Добре:
getUser();Ми прочитаємо більше коду, ніж коли-небудь напишемо. Важливо, щоб наш код був легким для читання і пошуку. Не використовуючи імена для змінних, що є важливими для розуміння нашої програми, ми шкодимо тим, хто читає наш код. Робіть імена доступними для пошуку. Такі засоби, як buddy.js та ESLint можуть допомогти у знаходженні безіменних констант.
Погано:
// Якого біса означає 86400000?setTimeout(blastOff,86400000);Добре:
// Оголошуйте їх у якості іменованих констант з верхнім регістром.constMILLISECONDS_IN_A_DAY=86_400_000;setTimeout(blastOff,MILLISECONDS_IN_A_DAY);Погано:
constaddress="One Infinite Loop, Cupertino 95014";constcityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;saveCityZipCode(address.match(cityZipCodeRegex)[1],address.match(cityZipCodeRegex)[2]);Добре:
constaddress="One Infinite Loop, Cupertino 95014";constcityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;const[_,city,zipCode]=address.match(cityZipCodeRegex)||[];saveCityZipCode(city,zipCode);Явне краще за неявне.
Погано:
constlocations=["Austin","New York","San Francisco"];locations.forEach(l=>{doStuff();doSomeOtherStuff();// ...// ...// ...// Почекайте, що там означає `l`?dispatch(l);});Добре:
constlocations=["Austin","New York","San Francisco"];locations.forEach(location=>{doStuff();doSomeOtherStuff();// ...// ...// ...dispatch(location);});Якщо ім'я вашого класу/об'єкта щось говорить вам, не повторюйте це у назві змінної.
Погано:
constCar={carMake: "Honda",carModel: "Accord",carColor: "Blue"};functionpaintCar(car){car.carColor="Red";}Добре:
constCar={make: "Honda",model: "Accord",color: "Blue"};functionpaintCar(car){car.color="Red";}Аргументи за замовчуванням часто є більш чистими, ніж коротке обчислення. Майте на увазі, що при використанні аргументів за замовчуванням ваша функція надасть значення за замовчуванням тільки для undefined аргументів. Інші "хибні" (falsy) значення, як-то '', "", false, null, 0, і NaN, не будуть замінені значенням за замовчуванням.
Погано:
functioncreateMicrobrewery(name){constbreweryName=name||"Hipster Brew Co.";// ...}Добре:
functioncreateMicrobrewery(name="Hipster Brew Co."){// ...}Обмеження кількості параметрів функції надзвичайно важливо, тому що це робить тестування вашої функції простішим. Наявність більше трьох параметрів призводить до комбінаторного вибуху, коли вам потрібно тестувати безліч різноманітних ситуацій з кожним окремим аргументом.
Один або два аргументи є ідеальним випадком, трьох аргументів слід уникати по можливості. Якщо аргументів більше, їх слід об'єднати. Зазвичай, якщо у вас більше двох аргументів, ваша функція намагається виконувати забагато дій. В тих випадках, коли це не так, майже завжди буде достатньо об'єкта вищого рівня.
Так як JavaScript дозволяє вам створювати об'єкти на льоту, без використання синтаксису класів, ви можете використовувати об'єкт, якщо потребуєте багатьох аргументів.
Щоб було очевидно, які властивості функція очікує, ви можете використовувати ES2015/ES6 синтаксис деструктуризації. Такий підхід має декілька переваг:
- Коли хтось дивиться на сигнатуру функції, одразу зрозуміло, які властивості використовуються.
- Деструктуризацію можна використовувати, щоб імітувати іменовані параметри.
- Деструктуризація, окрім того, клонує вказані примітивні значення об'єкту аргумента, переданого у функцію. Це може допомогти запобіганню побічним ефектам. Примітка: об'єкти та масиви, які деструктурували з об'єкта аргументу, НЕ клонуються.
- Лінтери можуть попередити вас щодо невикористаних властивостей, що було б неможливим без деструктуризації.
Погано:
functioncreateMenu(title,body,buttonText,cancellable){// ...}createMenu("Foo","Bar","Baz",true);Добре:
functioncreateMenu({ title, body, buttonText, cancellable }){// ...}createMenu({title: "Foo",body: "Bar",buttonText: "Baz",cancellable: true});Це, безумовно, найважливіше правило в програмній інженерії. Коли функції виконують більше одної дії, їх важко поєднувати, тестувати та обґрунтовувати. Коли ви можете обмежити функцію до тільки одної дії, її можна легко рефакторити і ваш код буде набагато чистішим. Навіть якщо ви засвоїте з цього посібника тільки цю пораду, ви будете попереду багатьох розробників.
Погано:
functionemailClients(clients){clients.forEach(client=>{constclientRecord=database.lookup(client);if(clientRecord.isActive()){email(client);}});}Добре:
functionemailActiveClients(clients){clients.filter(isActiveClient).forEach(email);}functionisActiveClient(client){constclientRecord=database.lookup(client);returnclientRecord.isActive();}Погано:
functionaddToDate(date,month){// ...}constdate=newDate();// По імені функції важко сказати, що саме додаєтьсяaddToDate(date,1);Добре:
functionaddMonthToDate(month,date){// ...}constdate=newDate();addMonthToDate(1,date);Наявність більше одного рівня абстракції зазвичай означає, що ваша функція виконує забагато дій. Розділення функцій призводить до повторного використання та більш легкого тестування.
Погано:
functionparseBetterJSAlternative(code){constREGEXES=[// ...];conststatements=code.split(" ");consttokens=[];REGEXES.forEach(REGEX=>{statements.forEach(statement=>{// ...});});constast=[];tokens.forEach(token=>{// правило...});ast.forEach(node=>{// парсинг...});}Добре:
functionparseBetterJSAlternative(code){consttokens=tokenize(code);constsyntaxTree=parse(tokens);syntaxTree.forEach(node=>{// парсинг...});}functiontokenize(code){constREGEXES=[// ...];conststatements=code.split(" ");consttokens=[];REGEXES.forEach(REGEX=>{statements.forEach(statement=>{tokens.push(/* ... */);});});returntokens;}functionparse(tokens){constsyntaxTree=[];tokens.forEach(token=>{syntaxTree.push(/* ... */);});returnsyntaxTree;}Робіть все можливе, щоб уникнути повторюваного коду. Повторюваний код поганий тому, що означає наявність більше одного місця для змін у разі, якщо вам потрібно буде змінити деяку логіку.
Уявіть, що ви керуєте рестораном і стежите за своїм інвентарем: помідори, цибуля, часник, спеції тощо. Якщо у вас є кілька списків, у яких ви все зберігаєте, тоді кожен з них потрібно оновлювати, коли ви подаєте страву з помідорами. Якщо у вас є лише один список, є лише одне місце для оновлення!
Часто у вас є повторюваний код, коли ви маєте дві або більше трохи різних сутностей, які мають багато спільного, але їх відмінності змушують вас мати дві або більше окремих функцій, які виконують багато однакових дій. Видалення повторюваного коду означає створення абстракції, яка може обробляти цю множину різних сутностей лише однією функцією/модулем/класом.
Правильне розуміння абстракції є критично важливим, саме тому ви повинні слідувати принципам SOLID, викладеним в розділі Класи. Погані абстракції можуть бути гіршими за повторюваний код, тому будьте обережні! Маючи це на увазі, якщо ви можете зробити гарну абстракцію, зробіть її! Не повторюйте себе, інакше вам доведеться оновлювати декілька місць, коли ви захочете змінити лише одне.
Погано:
functionshowDeveloperList(developers){developers.forEach(developer=>{constexpectedSalary=developer.calculateExpectedSalary();constexperience=developer.getExperience();constgithubLink=developer.getGithubLink();constdata={ expectedSalary, experience, githubLink };render(data);});}functionshowManagerList(managers){managers.forEach(manager=>{constexpectedSalary=manager.calculateExpectedSalary();constexperience=manager.getExperience();constportfolio=manager.getMBAProjects();constdata={ expectedSalary, experience, portfolio };render(data);});}Добре:
functionshowEmployeeList(employees){employees.forEach(employee=>{constexpectedSalary=employee.calculateExpectedSalary();constexperience=employee.getExperience();constdata={ expectedSalary, experience };switch(employee.type){case"manager": data.portfolio=employee.getMBAProjects();break;case"developer": data.githubLink=employee.getGithubLink();break;}render(data);});}Погано:
constmenuConfig={title: null,body: "Bar",buttonText: null,cancellable: true};functioncreateMenu(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);Добре:
constmenuConfig={title: "Order",// Користувач не додав ключ 'body'buttonText: "Send",cancellable: true};functioncreateMenu(config){config=Object.assign({title: "Foo",body: "Bar",buttonText: "Baz",cancellable: true},config);// config тепер дорівнює:{title: "Order", body: "Bar", buttonText: "Send", cancellable: true}// ...}createMenu(menuConfig);Флаги повідомляють користувачу, що ця функція виконує більше однієї дії. Функції повинні виконувати лише одну дію. Розділіть функції, якщо вони дотримуються різних кодових шляхів на основі булевої змінної.
Погано:
functioncreateFile(name,temp){if(temp){fs.create(`./temp/${name}`);}else{fs.create(name);}}Добре:
functioncreateFile(name){fs.create(name);}functioncreateTempFile(name){createFile(`./temp/${name}`);}Функція створює побічний ефект, якщо робить щось інше, окрім приймання вхідного значення і повернення іншого значення або значень. Побічним ефектом може бути запис у файл, зміна якоїсь глобальної змінної або випадкове пересилання всіх ваших грошей незнайомцю.
Проте, час від часу вам потрібно мати побічні ефекти в програмі. Як у попередньому прикладі, вам може знадобитися запис у файл. Що вам потрібно зробити - це централізувати місце, де ви виконуєте таку логіку. Не створюйте декількох функцій та класів, які роблять запис у певний файл. Майте один сервіс, який це робить. Один і тільки один.
Основна думка тут - це уникання поширених помилок, таких, як розділення стану між об'єктами без будь-якої структури, використання змінних типів даних, в які можна записати що завгодно, і відсутність централізування у місцях виникнення побічних ефектів. Якщо ви зможете це зробити, ви будете щасливішими за переважну більшість інших програмістів.
Погано:
// Глобальна змінна, на яку посилається наступна функція.// Якщо б у нас була ще одна функція, що використовує це ім'я, то зараз ця змінна була б масивом// і функція могла б зламати змінну.letname="Ryan McDermott";functionsplitIntoFirstAndLastName(){name=name.split(" ");}splitIntoFirstAndLastName();console.log(name);// ['Ryan', 'McDermott'];Добре:
functionsplitIntoFirstAndLastName(name){returnname.split(" ");}constname="Ryan McDermott";constnewName=splitIntoFirstAndLastName(name);console.log(name);// 'Ryan McDermott';console.log(newName);// ['Ryan', 'McDermott'];У JavaScript примітиви передаються за значенням, а об'єкти/масиви передаються за посиланням. У випадку об'єктів і масивів, якщо ваша функція вносить зміни у масив кошика для покупок, наприклад, додаючи товар для придбання, тоді це додавання вплине на будь-яку іншу функцію, яка використовує масив cart. Це може бути чудово, проте це може бути і погано. Давайте уявимо погану ситуацію:
Користувач натискає кнопку "Придбати", кнопка викликає функцію purchase, що створює мережевий запит і надсилає масив cart на сервер. Через погане мережеве з'єднання, функція purchase повинна створювати повторний запит. А якщо тим часом користувач випадково натисне "Додати в кошик" на товарі, який йому не потрібен, до початку мережевого запиту? Якщо це трапляється і мережевий запит починається, то функція придбання відправить випадково доданий товар, оскільки він має посилання на масив кошика покупок, котрий функція addItemToCart модифікувала додаванням небажаного товару.
Чудовим рішенням для addItemToCart було б завжди клонувати cart, редагувати його і повертати клон. Це гарантує, що сторонні зміни не вплинуть на жодну функцію, що посилається на кошик для покупок.
Два застереження, які слід згадати при такому підході:
Можуть бути випадки, коли ви дійсно хочете змінити об'єкт на вході, але коли ви застосуєте цю практику програмування, ви виявите, що ці випадки є досить рідкісними. Більшість речей можна відрефакторити так, щоб уникнути побічних ефектів!
Клонування великих об'єктів може бути дуже дорогим з точки зору продуктивності. На щастя, це не є великою проблемою на практиці, оскільки є чудові бібліотеки, що дозволяють такому підходу програмування бути швидким і не таким вибагливим до пам'яті, як клонування об’єктів та масивів вручну.
Погано:
constaddItemToCart=(cart,item)=>{cart.push({ item,date: Date.now()});};Добре:
constaddItemToCart=(cart,item)=>{return[...cart,{ item,date: Date.now()}];};Забруднення глобальних змінних є поганою практикою в JavaScript, оскільки у вас може виникнути конфлікт з іншою бібліотекою, і користувач вашого API не буде цього знати, доки не отримає виняток у продакшені. Давайте подумаємо про приклад: що, якби ви хотіли розширити нативний метод масиву JavaScript так, щоб Array мав метод diff, який міг би показати різницю між двома масивами? Ви можете записати свою нову функцію в Array.prototype, але вона може вступити в конфлікт з іншою бібліотекою, яка намагається робити те саме. А що як та інша бібліотека просто використовувала diff для пошуку різниці між першим та останнім елементами масиву? Ось чому було б набагато краще просто використовувати ES2015 / ES6 класи і просто розширити глобальний об'єкт Array.
Погано:
Array.prototype.diff=functiondiff(comparisonArray){consthash=newSet(comparisonArray);returnthis.filter(elem=>!hash.has(elem));};Добре:
classSuperArrayextendsArray{diff(comparisonArray){consthash=newSet(comparisonArray);returnthis.filter(elem=>!hash.has(elem));}}JavaScript не є функціональною мовою у сенсі Haskell, але має функціональний присмак. Функціональні мови можуть бути більш чистими та легшими для тестування. Віддавайте перевагу цьому стилю програмування, коли можете.
Погано:
constprogrammerOutput=[{name: "Uncle Bobby",linesOfCode: 500},{name: "Suzie Q",linesOfCode: 1500},{name: "Jimmy Gosling",linesOfCode: 150},{name: "Gracie Hopper",linesOfCode: 1000}];lettotalOutput=0;for(leti=0;i<programmerOutput.length;i++){totalOutput+=programmerOutput[i].linesOfCode;}Добре:
constprogrammerOutput=[{name: "Uncle Bobby",linesOfCode: 500},{name: "Suzie Q",linesOfCode: 1500},{name: "Jimmy Gosling",linesOfCode: 150},{name: "Gracie Hopper",linesOfCode: 1000}];consttotalOutput=programmerOutput.reduce((totalLines,output)=>totalLines+output.linesOfCode,0);Погано:
if(fsm.state==="fetching"&&isEmpty(listNode)){// ...}Добре:
functionshouldShowSpinner(fsm,listNode){returnfsm.state==="fetching"&&isEmpty(listNode);}if(shouldShowSpinner(fsmInstance,listNodeInstance)){// ...}Погано:
functionisDOMNodeNotPresent(node){// ...}if(!isDOMNodeNotPresent(node)){// ...}Добре:
functionisDOMNodePresent(node){// ...}if(isDOMNodePresent(node)){// ...}Це здається неможливим завданням. Більшість людей, почувши це, говорять: "як я можу зробити хоч щось без if виразу?" Відповідь полягає в тому, що ви можете використовувати поліморфізм для досягнення однієї і тієї ж мети у багатьох випадках. Друге питання, як правило: "ну це чудово, але чому я хотів би це зробити?" Відповідь - це попередня концепція чистого коду, яку ми дізналися: функція повинна виконувати лише одну дію. Коли у вас є класи та функції, у яких наявні if вирази, ви говорите вашому користувачеві, що ваша функція виконує більше однієї дії. Пам'ятайте, виконуйте тільки одну дію.
Погано:
classAirplane{// ...getCruisingAltitude(){switch(this.type){case"777": returnthis.getMaxAltitude()-this.getPassengerCount();case"Air Force One": returnthis.getMaxAltitude();case"Cessna": returnthis.getMaxAltitude()-this.getFuelExpenditure();}}}Добре:
classAirplane{// ...}classBoeing777extendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getPassengerCount();}}classAirForceOneextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude();}}classCessnaextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getFuelExpenditure();}}JavaScript є слабо типізованою мовою - це означає, що ваші функції можуть приймати аргументи будь-якого типу. Іноді вам незручна ця свобода, і здається спокусливим зробити перевірку типів у ваших функціях. Є багато способів уникнути такої необхідності. Перше, що слід врахувати, - це послідовні API.
Погано:
functiontravelToTexas(vehicle){if(vehicleinstanceofBicycle){vehicle.pedal(this.currentLocation,newLocation("texas"));}elseif(vehicleinstanceofCar){vehicle.drive(this.currentLocation,newLocation("texas"));}}Добре:
functiontravelToTexas(vehicle){vehicle.move(this.currentLocation,newLocation("texas"));}Якщо ви працюєте з базовими примітивними значеннями, такими як рядки та цілі числа, і ви не можете використовувати поліморфізм, але все ще відчуваєте потребу перевірити тип, вам слід розглянути можливість використання TypeScript. Це відмінна альтернатива звичайному JavaScript, оскільки вона надає вам статичну типізацію над стандартним JavaScript синтаксисом. Проблема з ручною перевіркою типовів у звичайному JavaScript полягає в тому, що для такої перевірки потрібно стільки додаткового коду, що отримана вами штучна "безпека типів" не компенсує втрату читабельності. Слідкуйте за чистотою вашого JavaScript, пишіть хороші тести та проводьте якісне рецензування коду. В іншому випадку перевіряйте типи, але з TypeScript (який, як я вже сказав, є чудовою альтернативою!).
Погано:
functioncombine(val1,val2){if((typeofval1==="number"&&typeofval2==="number")||(typeofval1==="string"&&typeofval2==="string")){returnval1+val2;}thrownewError("Must be of type String or Number");}Добре:
functioncombine(val1,val2){returnval1+val2;}Сучасні браузери роблять багато оптимізації під капотом. Здебільшого, якщо ви оптимізуєте, то ви просто витрачаєте свій час. Є хороші ресурси, що показують, де оптимізації не вистачає. Використовуйте їх до тих пір, доки ситуація не покращиться.
Погано:
// У старих браузерах кожна ітерація `list.length` без кешування буде дорогою// через перерахунок `list.length`. У сучасних браузерах це оптимізовано.for(leti=0,len=list.length;i<len;i++){// ...}Добре:
for(leti=0;i<list.length;i++){// ...}Мертвий код так само поганий, як і повторюваний. Немає жодних підстав тримати його у вашій кодовій базі. Якщо його не викликають, позбудьтесь його! Він все ще буде у безпеці в історії версій, якщо вам знадобиться.
Погано:
functionoldRequestModule(url){// ...}functionnewRequestModule(url){// ...}constreq=newRequestModule;inventoryTracker("apples",req,"www.inventory-awesome.io");Добре:
functionnewRequestModule(url){// ...}constreq=newRequestModule;inventoryTracker("apples",req,"www.inventory-awesome.io");Використання геттерів та сеттерів для доступу до даних об'єктів може бути кращим за просте отримання властивості об'єкта. "Чому?" - ви можете запитати. Ось перелік причин:
- Коли ви хочете зробити більше, ніж отримати властивість об’єкта, вам не потрібно шукати та змінювати кожне місце доступу до властивості об'єкта.
- Робить валідацію простою при роботі з
set. - Інкапсулює внутрішнє представлення.
- Легко додавати логування та обробку помилок під час отримання та встановлення властивостей.
- Ви можете ліниво завантажити властивості об'єкта, скажімо, отримуючи їх з сервера.
Погано:
functionmakeBankAccount(){// ...return{balance: 0// ...};}constaccount=makeBankAccount();account.balance=100;Добре:
functionmakeBankAccount(){// ця властивість приватнаletbalance=0;// "геттер", є публічним через повернутий об’єкт нижчеfunctiongetBalance(){returnbalance;}// "сеттер", є публічним через повернутий об’єкт нижчеfunctionsetBalance(amount){// ... валідація перед оновленням балансуbalance=amount;}return{// ... getBalance, setBalance };}constaccount=makeBankAccount();account.setBalance(100);Цього можна досягти за допомогою замикань (для ES5 і нижче).
Погано:
constEmployee=function(name){this.name=name;};Employee.prototype.getName=functiongetName(){returnthis.name;};constemployee=newEmployee("John Doe");console.log(`Employee name: ${employee.getName()}`);// Employee name: John Doedeleteemployee.name;console.log(`Employee name: ${employee.getName()}`);// Employee name: undefinedДобре:
functionmakeEmployee(name){return{getName(){returnname;}};}constemployee=makeEmployee("John Doe");console.log(`Employee name: ${employee.getName()}`);// Employee name: John Doedeleteemployee.name;console.log(`Employee name: ${employee.getName()}`);// Employee name: John DoeДуже важко отримати читабельне наслідування класів, їх побудову та визначення методів у класичних ES5 класах. Якщо вам потрібне наслідування (майте на увазі, що це може бути не так), тоді віддайте перевагу ES2015 / ES6 класам. Однак віддавайте перевагу невеликим функціям над класами, доки вам не знадобляться більш великі і складні об'єкти.
Погано:
constAnimal=function(age){if(!(thisinstanceofAnimal)){thrownewError("Instantiate Animal with `new`");}this.age=age;};Animal.prototype.move=functionmove(){};constMammal=function(age,furColor){if(!(thisinstanceofMammal)){thrownewError("Instantiate Mammal with `new`");}Animal.call(this,age);this.furColor=furColor;};Mammal.prototype=Object.create(Animal.prototype);Mammal.prototype.constructor=Mammal;Mammal.prototype.liveBirth=functionliveBirth(){};constHuman=function(age,furColor,languageSpoken){if(!(thisinstanceofHuman)){thrownewError("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=functionspeak(){};Добре:
classAnimal{constructor(age){this.age=age;}move(){/* ... */}}classMammalextendsAnimal{constructor(age,furColor){super(age);this.furColor=furColor;}liveBirth(){/* ... */}}classHumanextendsMammal{constructor(age,furColor,languageSpoken){super(age,furColor);this.languageSpoken=languageSpoken;}speak(){/* ... */}}Цей паттерн дуже корисний у JavaScript, і ви спостерігаєте його у багатьох бібліотеках, таких як jQuery і Lodash. Прив'язка методів дозволяє вашому коду бути виразним і менш багатослівним. Спробуйте використати прив'язку методів і погляньте, наскільки чистим буде ваш код. У функціях класу просто поверніть this в кінці кожної функції, і ви можете прив'язувати до нього подальші методи класу.
Погано:
classCar{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);}}constcar=newCar("Ford","F-150","red");car.setColor("pink");car.save();Добре:
classCar{constructor(make,model,color){this.make=make;this.model=model;this.color=color;}setMake(make){this.make=make;// ПРИМІТКА: Повертаємо this для прив'язуванняreturnthis;}setModel(model){this.model=model;// ПРИМІТКА: Повертаємо this для прив'язуванняreturnthis;}setColor(color){this.color=color;// ПРИМІТКА: Повертаємо this для прив'язуванняreturnthis;}save(){console.log(this.make,this.model,this.color);// ПРИМІТКА: Повертаємо this для прив'язуванняreturnthis;}}constcar=newCar("Ford","F-150","red").setColor("pink").save();Як відомо зазначено в книзі Design Patterns Бандою Чотирьох, вам слід віддати перевагу композиції над наслідуванням, де це можливо. Є багато вагомих причин використовувати наслідування і є безліч вагомих причин використовувати композицію. Основним моментом цієї максими є те, що якщо ваш розум інстинктивно дотримується наслідування, спробуйте подумати, чи композиція могла б краще моделювати вашу проблему. У деяких випадках вона могла б.
Тоді вам може бути цікаво: "коли я повинен використовувати наслідування?" Це залежить від вашої поточної проблеми, але це пристойний перелік ситуацій, коли наслідування має більше сенсу, ніж композиція:
- Ваше наслідування представляє відносини типу "є чимось", а не "має щось" (Людина->Тварина проти Користувач->ДеталіКористувача).
- Ви можете повторно використовувати код з базових класів (Люди можуть рухатися, як і всі тварини).
- Ви хочете внести глобальні зміни до похідних класів, змінивши базовий клас. (Зміна витрати калорій всіх тварин, коли вони рухаються).
Погано:
classEmployee{constructor(name,email){this.name=name;this.email=email;}// ...}// Погано, тому що Співробітники "мають" податкові дані. EmployeeTaxData не є типом EmployeeclassEmployeeTaxDataextendsEmployee{constructor(ssn,salary){super();this.ssn=ssn;this.salary=salary;}// ...}Добре:
classEmployeeTaxData{constructor(ssn,salary){this.ssn=ssn;this.salary=salary;}// ...}classEmployee{constructor(name,email){this.name=name;this.email=email;}setTaxData(ssn,salary){this.taxData=newEmployeeTaxData(ssn,salary);}// ...}Як зазначено в Чистому коді: "Ніколи не повинно бути більше однієї причини для зміни класу". Привабливо наповнити клас великою кількістю функціоналу, як у ситуації, коли ви можете взяти лише одну валізу у свій рейс. Проблема в тому, що ваш клас не буде концептуально єдиним, і це дасть йому багато причин для змін. Мінімізування кількості змін класу - це важливо. Це важливо тому, що якщо в одному класі забагато функціоналу і ви модифікуєте його частину, може бути важко зрозуміти, як це вплине на інші залежні модулі у вашій кодовій базі.
Погано:
classUserSettings{constructor(user){this.user=user;}changeSettings(settings){if(this.verifyCredentials()){// ...}}verifyCredentials(){// ...}}Добре:
classUserAuth{constructor(user){this.user=user;}verifyCredentials(){// ...}}classUserSettings{constructor(user){this.user=user;this.auth=newUserAuth(user);}changeSettings(settings){if(this.auth.verifyCredentials()){// ...}}}Як зазначає Бертран Меєр: "програмні об'єкти (класи, модулі, функції, тощо) мають бути відкритими для розширення, але закритими для внесення змін". Що мається на увазі? Загалом, цей принцип говорить, що ви повинні дозволити користувачам додавати новий функціонал без зміни існуючого коду.
Погано:
classAjaxAdapterextendsAdapter{constructor(){super();this.name="ajaxAdapter";}}classNodeAdapterextendsAdapter{constructor(){super();this.name="nodeAdapter";}}classHttpRequester{constructor(adapter){this.adapter=adapter;}fetch(url){if(this.adapter.name==="ajaxAdapter"){returnmakeAjaxCall(url).then(response=>{// трансформувати відповідь і повернути її});}elseif(this.adapter.name==="nodeAdapter"){returnmakeHttpCall(url).then(response=>{// трансформувати відповідь і повернути її});}}}functionmakeAjaxCall(url){// виконати запит і повернути проміс}functionmakeHttpCall(url){// виконати запит і повернути проміс}Добре:
classAjaxAdapterextendsAdapter{constructor(){super();this.name="ajaxAdapter";}request(url){// виконати запит і повернути проміс}}classNodeAdapterextendsAdapter{constructor(){super();this.name="nodeAdapter";}request(url){// виконати запит і повернути проміс}}classHttpRequester{constructor(adapter){this.adapter=adapter;}fetch(url){returnthis.adapter.request(url).then(response=>{// трансформувати відповідь і повернути її});}}Це страшний термін для дуже простої концепції. Формально вона визначена так: "Якщо S є підтипом T, тоді об'єкти типу T можуть бути замінені об'єктами типу S (тобто об'єкти типу S можуть підставлятися замість об'єктів типу T), не змінюючи жодної важливої властивості програми (коректність, виконання завдань, тощо)". Це ще страшніше визначення.
Найкраще пояснення цьому - якщо у вас є батьківський клас та дочірній клас, то їх можна використовувати взаємозамінно, не отримуючи неправильних результатів. Це все ще може бентежити, тому давайте подивимось на класичний приклад Квадрат-Прямокутник. Математично квадрат є прямокутником, але якщо ви змоделюєте це за допомогою відносини "є чимось" шляхом наслідування, ви швидко отримаєте проблему.
Погано:
classRectangle{constructor(){this.width=0;this.height=0;}setColor(color){// ...}render(area){// ...}setWidth(width){this.width=width;}setHeight(height){this.height=height;}getArea(){returnthis.width*this.height;}}classSquareextendsRectangle{setWidth(width){this.width=width;this.height=width;}setHeight(height){this.width=height;this.height=height;}}functionrenderLargeRectangles(rectangles){rectangles.forEach(rectangle=>{rectangle.setWidth(4);rectangle.setHeight(5);constarea=rectangle.getArea();// ПОГАНО: Повертає 25 для Квадрату. Повинно бути 20.rectangle.render(area);});}constrectangles=[newRectangle(),newRectangle(),newSquare()];renderLargeRectangles(rectangles);Добре:
classShape{setColor(color){// ...}render(area){// ...}}classRectangleextendsShape{constructor(width,height){super();this.width=width;this.height=height;}getArea(){returnthis.width*this.height;}}classSquareextendsShape{constructor(length){super();this.length=length;}getArea(){returnthis.length*this.length;}}functionrenderLargeShapes(shapes){shapes.forEach(shape=>{constarea=shape.getArea();shape.render(area);});}constshapes=[newRectangle(4,5),newRectangle(4,5),newSquare(5)];renderLargeShapes(shapes);У JavaScript немає інтерфейсів, тому цей принцип не застосовується так суворо, як інші. Однак він є важливим і актуальним навіть при відсутності системи типів у JavaScript.
ISP зазначає: "Клієнти не повинні залежати від інтерфейсів, які вони не використовують." Інтерфейси є неявними контрактами в JavaScript через качину типізацію (duck typing).
Хороший приклад, що демонструє цей принцип в JavaScript - це класи, які потребують великих об'єктів налаштувань. Не вимагати від клієнтів встановлення величезної кількості варіантів вигідно, тому що більшу частину часу вони не потребують всіх налаштувань. Якщо зробити налаштування необов’язковими, це допоможе запобігти появі "жирного інтерфейсу".
Погано:
classDOMTraverser{constructor(settings){this.settings=settings;this.setup();}setup(){this.rootNode=this.settings.rootNode;this.animationModule.setup();}traverse(){// ...}}const$=newDOMTraverser({rootNode: document.getElementsByTagName("body"),animationModule(){}// Більшу частину часу ми не потребуємо анімації під час обходу DOM.// ...});Добре:
classDOMTraverser{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$=newDOMTraverser({rootNode: document.getElementsByTagName("body"),options: {animationModule(){}}});Цей принцип визначає дві важливі речі:
- Модулі високого рівня не повинні залежати від модулів низького рівня. І ті й інші повинні залежати від абстракцій.
- Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Спочатку це може бути важко зрозуміти, але якщо ви працювали з AngularJS, ви бачили реалізацію цього принципу у формі впровадження залежностей (DI). Хоча вони не є ідентичними поняттями, DIP утримує модулі високого рівня від знання деталей модулів низького рівня та їх налаштування. Цього можна досягти за допомогою DI. Величезна перевага впровадження залежностей полягає в тому, що воно зменшує зв'язування між модулями. Зв'язування - це дуже поганий паттерн розробки, оскільки це робить ваш код важким для рефакторингу.
Як було сказано раніше, у JavaScript немає інтерфейсів, тому абстракції є неявними контрактами. Тобто методами та властивостями, які об'єкт/клас показує іншому об'єкту/класу. У наведеному нижче прикладі неявний контракт полягає в тому, що будь-який модуль запиту для InventoryTracker матиме метод requestItems.
Погано:
classInventoryRequester{constructor(){this.REQ_METHODS=["HTTP"];}requestItem(item){// ...}}classInventoryTracker{constructor(items){this.items=items;// ПОГАНО: Ми створили залежність від реалізації конкретного запиту.// Треба щоб requestItems залежав тільки від методу запиту: `request`this.requester=newInventoryRequester();}requestItems(){this.items.forEach(item=>{this.requester.requestItem(item);});}}constinventoryTracker=newInventoryTracker(["apples","bananas"]);inventoryTracker.requestItems();Добре:
classInventoryTracker{constructor(items,requester){this.items=items;this.requester=requester;}requestItems(){this.items.forEach(item=>{this.requester.requestItem(item);});}}classInventoryRequesterV1{constructor(){this.REQ_METHODS=["HTTP"];}requestItem(item){// ...}}classInventoryRequesterV2{constructor(){this.REQ_METHODS=["WS"];}requestItem(item){// ...}}// Побудувавши залежності зовні та впровадивши їх, ми можемо легко// замінити наш модуль запиту на новий та модний, що використовує вебсокети.constinventoryTracker=newInventoryTracker(["apples","bananas"],newInventoryRequesterV2());inventoryTracker.requestItems();Тестування важливіше, ніж доставка коду. Якщо у вас немає тестів або їх кількість недостатня, то кожного разу, надсилаючи код, ви не будете впевнені, що нічого не зламали. Питання про те, що становить адекватну кількість тестів, вирішується вашою командою, але 100% покриття (усі вирази та гілки) - це те, як ви досягаєте дуже високої впевненості та спокою як розробник. Це означає, що окрім чудового фреймворку тестування, вам також потрібно використовувати гарний інструмент покриття.
Немає приводу не писати тестів. Існує безліч хороших JS фреймворків, тож знайдіть такий, якому віддає перевагу ваша команда. Коли ви знайдете такий, що підходить вашій команді, намагайтесь завжди писати тести для кожної нової функції/модуля, який ви вводите. Якщо вашим улюбленим методом є розробка через тестування (TDD), це чудово, але головне - просто переконайтеся, що ви досягаєте своїх планів з покриття тестами, перш ніж запускати будь-яку функцію або рефакторити існуючу.
Погано:
importassertfrom"assert";describe("MomentJS",()=>{it("handles date boundaries",()=>{letdate;date=newMomentJS("1/1/2015");date.addDays(30);assert.equal("1/31/2015",date);date=newMomentJS("2/1/2016");date.addDays(28);assert.equal("02/29/2016",date);date=newMomentJS("2/1/2015");date.addDays(28);assert.equal("03/01/2015",date);});});Добре:
importassertfrom"assert";describe("MomentJS",()=>{it("handles 30-day months",()=>{constdate=newMomentJS("1/1/2015");date.addDays(30);assert.equal("1/31/2015",date);});it("handles leap year",()=>{constdate=newMomentJS("2/1/2016");date.addDays(28);assert.equal("02/29/2016",date);});it("handles non-leap year",()=>{constdate=newMomentJS("2/1/2015");date.addDays(28);assert.equal("03/01/2015",date);});});Колбеки не є чистими, і вони викликають надмірну кількість вкладеності. З ES2015 / ES6, проміси - це вбудований глобальний тип. Використовуйте їх!
Погано:
import{get}from"request";import{writeFile}from"fs";get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin",(requestErr,response,body)=>{if(requestErr){console.error(requestErr);}else{writeFile("article.html",body,writeErr=>{if(writeErr){console.error(writeErr);}else{console.log("File written");}});}});Добре:
import{get}from"request-promise";import{writeFile}from"fs-extra";get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin").then(body=>{returnwriteFile("article.html",body);}).then(()=>{console.log("File written");}).catch(err=>{console.error(err);});Проміси є дуже чистою альтернативою колбекам, але ES2017 / ES8 приносить async та await, які пропонують ще більш чисте рішення. Все, що вам потрібно, - це функція, яка має в префіксі ключове слово async, і тоді ви можете імперативно писати логіку без прив'язки функцій до then.
Погано:
import{get}from"request-promise";import{writeFile}from"fs-extra";get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin").then(body=>{returnwriteFile("article.html",body);}).then(()=>{console.log("File written");}).catch(err=>{console.error(err);});Добре:
import{get}from"request-promise";import{writeFile}from"fs-extra";asyncfunctiongetCleanCodeArticle(){try{constbody=awaitget("https://en.wikipedia.org/wiki/Robert_Cecil_Martin");awaitwriteFile("article.html",body);console.log("File written");}catch(err){console.error(err);}}getCleanCodeArticle()Викидання помилок - це гарна річ! Воно означає, що під час виконання программа успішно ідентифікувала, коли щось пішло не так, і дозволяє вам знати про це, зупиняючи виконання функції на поточному стеку, вбиваючи процесс (у Node) та повідомляючи вас трасировкою стеку у консолі.
Якщо нічого не робити із перехопленою помилкою, ви не зможете її виправити або зреагувати на неї. Логування помилки у консолі (console.log) не набагато краще, оскільки часто цей лог може загубитися в морі того, що друкується у консолі. Якщо ви вкладаєте фрагмент коду в try/catch, це означає, що ви передбачаєте там помилку, і тому ви повинні мати план або створити шлях коду, коли помилка виникає.
Погано:
try{functionThatMightThrow();}catch(error){console.log(error);}Добре:
try{functionThatMightThrow();}catch(error){// Один варіант (більш помітний, ніж console.log):console.error(error);// Інший варіант:notifyUserOfError(error);// Інший варіант:reportErrorToService(error);// АБО використовуйте всі три!}З тих причин, що і перехоплені помилки з try/catch.
Погано:
getdata().then(data=>{functionThatMightThrow(data);}).catch(error=>{console.log(error);});Добре:
getdata().then(data=>{functionThatMightThrow(data);}).catch(error=>{// Один варіант (більш помітний, ніж console.log):console.error(error);// Інший варіант:notifyUserOfError(error);// Інший варіант:reportErrorToService(error);// АБО використовуйте всі три!});Форматування суб'єктивне. Подібно до багатьох правил тут - не існує жорсткого припису, якого потрібно дотримуватися. Основний момент - НЕ СПЕРЕЧАЙТЕСЬ над форматуванням. Існує безліч інструментів для його автоматизації. Використовуйте один з них! Марно витрачати час і гроші, щоб інженери сперечалися з приводу форматування.
Для ситуацій, які не підпадають під автоматичне форматування (відступи, табуляції проти пробілів, подвійні проти одиничних лапок тощо), тут наявні декілька вказівок.
JavaScript не типізований, тому регістр багато говорить про ваші змінні, функції тощо. Ці правила є суб'єктивними, тому ваша команда може вибрати будь-які. Головне - незалежно від того, що ви обираєте, будьте послідовними.
Погано:
constDAYS_IN_WEEK=7;constdaysInMonth=30;constsongs=["Back In Black","Stairway to Heaven","Hey Jude"];constArtists=["ACDC","Led Zeppelin","The Beatles"];functioneraseDatabase(){}functionrestore_database(){}classanimal{}classAlpaca{}Добре:
constDAYS_IN_WEEK=7;constDAYS_IN_MONTH=30;constSONGS=["Back In Black","Stairway to Heaven","Hey Jude"];constARTISTS=["ACDC","Led Zeppelin","The Beatles"];functioneraseDatabase(){}functionrestoreDatabase(){}classAnimal{}classAlpaca{}Якщо функція викликає іншу, тримайте ці функції вертикально близько у вихідному файлі. В ідеалі тримайте викликаючу функцію прямо над тою, котру викликають. Ми схильні читати код зверху вниз, як газету. Тому зробіть так, щоб ваш код читався таким чином.
Погано:
classPerformanceReview{constructor(employee){this.employee=employee;}lookupPeers(){returndb.lookup(this.employee,"peers");}lookupManager(){returndb.lookup(this.employee,"manager");}getPeerReviews(){constpeers=this.lookupPeers();// ...}perfReview(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();}getManagerReview(){constmanager=this.lookupManager();}getSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.perfReview();Добре:
classPerformanceReview{constructor(employee){this.employee=employee;}perfReview(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();}getPeerReviews(){constpeers=this.lookupPeers();// ...}lookupPeers(){returndb.lookup(this.employee,"peers");}getManagerReview(){constmanager=this.lookupManager();}lookupManager(){returndb.lookup(this.employee,"manager");}getSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.perfReview();Коментарі - це вибачення, а не вимога. Якісний код переважно документує себе сам.
Погано:
functionhashIt(data){// Хешlethash=0;// Довжина рядкаconstlength=data.length;// Проходимось циклом через кожний символ у данихfor(leti=0;i<length;i++){// Отримуємо код символу.constchar=data.charCodeAt(i);// Створюємо хешhash=(hash<<5)-hash+char;// Конвертуємо в 32-бітне ціле числоhash&=hash;}}Добре:
functionhashIt(data){lethash=0;constlength=data.length;for(leti=0;i<length;i++){constchar=data.charCodeAt(i);hash=(hash<<5)-hash+char;// Конвертуємо в 32-бітне ціле числоhash&=hash;}}Контроль версій існує не просто так. Залиште старий код в історії.
Погано:
doStuff();// doOtherStuff();// doSomeMoreStuff();// doSoMuchStuff();Добре:
doStuff();Пам'ятайте - використовуйте контроль версій! Немає необхідності в мертвому коді, закоментованому коді, і особливо в журнальних коментарях. Використовуйте git log, щоб отримати історію!
Погано:
/** * 2016-12-20: Видалив монади, не зрозумів їх (RM) * 2016-10-01: Покращив використання спеціальних монад (JP) * 2016-02-03: Видалив перевірку типів (LI) * 2015-03-14: Додав combine з перевіркою типів (JR) */functioncombine(a,b){returna+b;}Добре:
functioncombine(a,b){returna+b;}Зазвичай вони просто додають шум. Нехай функції та назви змінних разом із належними відступами та форматуванням надають візуальну структуру вашому коду.
Погано:
////////////////////////////////////////////////////////////////////////////////// Ініціалізація властивості model об'єкта $scope////////////////////////////////////////////////////////////////////////////////$scope.model={menu: "foo",nav: "bar"};////////////////////////////////////////////////////////////////////////////////// Встановлення екшену////////////////////////////////////////////////////////////////////////////////constactions=function(){// ...};Добре:
$scope.model={menu: "foo",nav: "bar"};constactions=function(){// ...};Цей посібник також доступний на інших мовах:
Вірменська: hanumanum/clean-code-javascript/
Бенгальська: InsomniacSabbir/clean-code-javascript/
Бразильська португальська: fesnt/clean-code-javascript
Спрощена китайська:
Традиційна китайська: AllJointTW/clean-code-javascript
Французька: GavBaros/clean-code-javascript-fr
Німецька: marcbruederlin/clean-code-javascript
Індонезійська: andirkh/clean-code-javascript/
Італійська: frappacchio/clean-code-javascript/
Японська: mitsuruog/clean-code-javascript/
Корейська: qkraudghgh/clean-code-javascript-ko
Польська: greg-dev/clean-code-javascript-pl
Російська:
Іспанська: tureey/clean-code-javascript
Іспанська: andersontr15/clean-code-javascript
Турецька: bsonmez/clean-code-javascript
В'єтнамська: hienvd/clean-code-javascript/
