- Notifications
You must be signed in to change notification settings - Fork75
🛁 Адаптированные для JavaScript концепции Чистого кода
License
maksugr/clean-code-javascript
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Оригинальный репозиторий:ryanmcdermott/clean-code-javascript
Актуализировано по меньшей мере по состоянию на 4 февраля 2017 года
- Введение
- Переменные
- Функции
- Объекты и структуры данных
- Классы
- Тестирование
- Асинхронность
- Отлавливание ошибок
- Форматирование
- Комментарии
- Переводы
Адаптация для JavaScript принципов разработки программного обеспечения из книги Роберта МартинаЧистый код. Это не руководство по стилю. Это руководство по написанию читабельного и пригодного для переиспользования и рефакторинга программного обеспечения на JavaScript.
Не каждый принцип здесь должен строго соблюдаться и еще меньше получит всеобщее признание. Это принципы и ничего более, но они накапливались в течение многих лет коллективным опытом авторовЧистого кода.
Искусству написания программного обеспечения немногим более пятидесяти лет, и мывсе еще многому учимся. Когда программная архитектура постареет до возраста самой архитектуры, быть может тогда у нас появятся жесткие правила, которым необходимо следовать. А сейчас пусть эти принципы служат критерием оценки качества JavaScript-кода, создаваемого вами и вашей командой.
И еще. Знание этих принципов не сразу сделает вас лучше как разработчиков программного обеспечения, а их использование в течение многих лет не гарантирует, что вы перестанете совершать ошибки. Каждый фрагмент кода как первый набросок, как мокрая глина принимает свою форму постепенно. Наконец, все мы пронизаны несовершенством, когда наш код просматривают коллеги. Не истязайте себя за первые, нуждающиеся в улучшении, наброски. Вместо этого истязайте свой код!
Плохо:
constyyyymmdstr=moment().format('YYYY/MM/DD');
Хорошо:
constcurrentDate=moment().format('YYYY/MM/DD');
Плохо:
getUserInfo();getClientData();getCustomerRecord();
Хорошо:
getUser();
Мы прочитаем больше кода, чем когда-либо напишем. Это важно, чтобы код, который мы создаем, был читабельным и доступным для поиска. Невыразительно названные переменные ухудшают понимание наших программ и оскорбляют наших читателей. Делайте ваши обозначения доступными для поиска. Инструменты вродеbuddy.js иESLint помогут найти неназванные константы.
Плохо:
// Что такое 86400000?setTimeout(blastOff,86400000);
Хорошо:
// Объявляйте их как глобальный `const` со всеми буквами в заглавном регистре.constMILLISECONDS_IN_A_DAY=86400000;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(breweryName='Hipster Brew Co.'){// ...}
Ограничение количества параметров функции невероятно важно, потому что это упрощает тестирование. Более трех входных данных приводят к комбинаторному взрыву, где вы должны протестировать множество различных вариантов с каждым отдельным аргументом.
Один аргумент или два - идеальный случай, трех аргументов нужно избегать. Большее количество аргументов должно быть объединено. Как правило, если у вас более двух аргументов, ваша функция пытается сделать слишком много. Для большинства случаев, где это простительно, в качестве аргумента будет достаточно объекта верхнего уровня.
Поскольку JavaScript позволяет создавать объекты на лету, без классов в качестве основы, вы можете использовать их, если нуждаетесь во множестве аргументов.
Для того, чтобы сделать очевидным, какие свойства функция ожидает на входе, вы можете использовать синтаксис деструкции из ES2015/ES6. Он имеет несколько преимуществ:
- Когда вы смотрите на сигнатуру функции, то сразу понятно, какие свойства используются.
- Деструкция клонирует примитивные значения аргумента-объекта, переданного в функцию. Это предотвращает побочные эффекты. Примечание: объекты и массивы, которые деструктурированы из аргумента-объекта, НЕ клонируются.
- Линтер может предупредить вас о неиспользуемых свойствах, что было бы невозможно без деструктурирования.
Плохо:
functioncreateMenu(title,body,buttonText,cancellable){// ...}
Хорошо:
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);}});}
Хорошо:
functionemailClients(clients){clients.filter(isClientActive).forEach(email);}functionisClientActive(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)=>{// разбираем...});}
Хорошо:
functiontokenize(code){constREGEXES=[// ...];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((REGEX)=>{statements.forEach((statement)=>{tokens.push(/* ... */);});});returntokens;}functionlexer(tokens){constast=[];tokens.forEach((token)=>{ast.push(/* ... */);});returnast;}functionparseBetterJSAlternative(code){consttokens=tokenize(code);constast=lexer(tokens);ast.forEach((node)=>{// разбираем...});}
Делайте все, чтобы избежать дублирования кода. Повторяющийся код плох тем, что если придется править вашу логику, ее придется править в нескольких местах.
Представьте себе, вы открыли ресторан и ведете учет продуктов (всех ваших помидоров, лука, чеснока, специй и т.д.). Если у вас несколько списков с продуктами, то, когда у вас закажут томатный суп, вам придется обновить их все. Если список у вас только один, то обновлять придется только его!
Часто вы дублируете код, потому что у вас есть несколько логических участков, которые во многом схожи, но их различия заставляют вас иметь несколько функций, делающих много одинаковых операций. Удаление повторяющегося кода означает создание абстракции, обрабатывающей эту разную логику с помощью всего одной функции/модуля/класса.
Получение абстракции имеет решающее значение, поэтому вы должны следовать принципам проектирования классов (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();letportfolio=employee.getGithubLink();if(employee.type==='manager'){portfolio=employee.getMBAProjects();}constdata={ expectedSalary, experience, portfolio};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
, редактировала его и возвращала отредактированный клон. Это бы гарантировало, что никакие другие функции, использующие ссылку на массив корзины покупок, не будут затронуты какими-либо изменениями.
Два предостережения:
- Могут быть случаи, когда вы на самом деле хотите изменить входящий объект, но когда вы привыкнете к такому подходу программирования, то обнаружите, что эти случаи довольно редки. Большую часть логики можно переделать так, чтобы побочных эффектов не было совсем!
- Клонирование больших объектов может быть очень дорогими с точки зрения производительности. К счастью,это не является большой проблемой на практике, потому что существуют [отличные библиотеки] (https://facebook.github.io/immutable-js/), которые позволяют такому подходу программирования быть быстрым и не настолько затратным по памяти, как это было бы если бы вы вручную клонировали объекты и массивы.
Плохо:
constaddItemToCart=(cart,item)=>{cart.push({ item,date:Date.now()});};
Хорошо:
constaddItemToCart=(cart,item)=>{return[...cart,{ item,date :Date.now()}];};
Загрязнение глобального объекта - плохая практика в JavaScript, потому что вы можете начать конфликтовать с другой библиотекой, а пользователь вашего API останется в замешательстве даже после получения исключения в режиме production.Давайте поразмышляем о примере: что делать, если вы хотите расширить глобальный объект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}];constINITIAL_VALUE=0;consttotalOutput=programmerOutput.map((programmer)=>programmer.linesOfCode).reduce((acc,linesOfCode)=>acc+linesOfCode,INITIAL_VALUE);
Плохо:
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;}
Под капотом современные браузеры осуществляют большой объем оптимизации во время выполнения кода. В большинстве случаев, если вы занимались оптимизацией, вы попусту потратили свое время. [Есть хорошие ресурсы] (https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) для обнаружения нехватки оптимизации. Используйте их до того момента, пока ситуация не изменится.
Плохо:
// В старых браузерах каждая итерация с незакешированным `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
Чистый код декларирует: "Не должно быть более чем одной причины для изменения класса". Заманчиво представить себе класс, переполненный большим количеством функционала, словно в поездку вам позволили взять всего один чемодан. Проблема в том, что ваш класс не будет концептуально единым и это даст ему множество причин для изменения. Имеет огромное значение свести к минимуму количество таких причин. Если сосредоточить слишком много функциональности в одном классе, а затем попытаться изменить его часть, то спрогнозировать, как это может сказаться на других модулях системы, станет крайне сложно.
Плохо:
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==='httpNodeAdapter'){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, то объекты типа Т могут быть заменены на объекты типа S (то есть, объекты типа S могут заменить объекты типа Т) без влияния на важные свойства программы (корректность, пригодность для выполнения задач и т.д.). И да, в итоге определение получилось еще страшней.
Лучшее объяснение заключается в том, что если у вас есть родительский и дочерний классы, то они могут использоваться как взаимозаменяемые, не приводя при этом к некорректным результатам. Это по-прежнему может сбивать с толку, так что давайте взглянем на классический пример квадрата-прямоугольника. Математически квадрат представляет собой прямоугольник, но если вы смоделируете их отношения через наследование ("является разновидностью"), вы быстро наткнетесь на неприятности.
Плохо:
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 интерфейсы - это неявные контракты.
Хорошим примером станут классы, предусматривающие множество настроек для объектов. Полезно не требовать от клиентов проставления их всех, потому что большую часть времени все они не требуются. Создание опциональных настроек помогает предотвратить разбухание интерфейса.
Плохо:
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(){}// В большинстве случаев анимация нам не пригодится.// ...});
Хорошо:
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(){}}});
Этот принцип закрепляет две важные вещи:
- Модули верхнего уровня не должны зависеть от модулей низкого уровня. И те, и другие должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
На первый взгляд это кажется трудным, но если вы работали с Angular.js, вы видели реализацию этого принципа в виде внедрения зависимостей (Dependency Injection - DI). Несмотря на то, что DIP и DI - понятия не идентичные, DIP оберегает модули верхнего уровня от деталей модулей низкого уровня, а сделать это он может через DI. Огромное преимущество DIP в уменьшении взаимосвязей между модулями. Переплетение модулей - это антипаттерн, потому что оно делает рефакторинг вашего кода гораздо более трудоемким.
Как было сказано выше, JavaScript не имеет интерфейсов, так что абстракции зависят от неявных контрактов. То есть, от методов и свойств, которые объект/класс предоставляет другому объекту/классу. В приведенном ниже примере, неявный контракт в том, что любой модуль запроса дляInventoryTracker
будет иметь методrequestItems
.
Плохо:
classInventoryRequester{constructor(){this.REQ_METHODS=['HTTP'];}requestItem(item){// ...}}classInventoryTracker{constructor(items){this.items=items;// ПЛОХО: Мы создали зависимость от конкретной реализации запроса.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();
При классическом подходе к классам в 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(){this.make='Honda';this.model='Accord';this.color='white';}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();car.setColor('pink');car.setMake('Ford');car.setModel('F-150');car.save();
Хорошо:
classCar{constructor(){this.make='Honda';this.model='Accord';this.color='white';}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().setColor('pink').setMake('Ford').setModel('F-150').save();
Как верно замечено вDesign Patterns под авторством Банды Четырех (Gang of Four), когда это возможно, вы должны отдавать предпочтение композиции перед наследованием. Есть много хороших причин для использования наследования и много хороших причин для применения композиции. Главное, что если ваш ум инстинктивно идет путем наследования, подумайте, может быть композиция способна решить вашу проблему лучше. В ряде случаев это именно так.
Возможно вы задаетесь вопросом: "Когда я должен использовать наследование?". Это зависит от вашей проблемы, но вот конкретный список, когда наследование имеет больше смысла, чем композиция:
- Ваше наследование представляет собой отношения "является разновидностью", а не "включает в себя" (Человек->Животное против Пользователь->Детали_Пользователя).
- Вы можете повторно использовать код из базовых классов (люди могут двигаться как и все животные).
- Вы хотите сделать глобальные изменения в производных классах путем изменения базового класса (изменение расхода калорий всех животных во время движения).
Плохо:
classEmployee{constructor(name,email){this.name=name;this.email=email;}// ...}// Плохо, потому что Employee (Сотрудник) "имеет" налоговые данные.// EmployeeTaxData (Налоговые_данные_сотрудника) не являются типом Employee (Сотрудника).classEmployeeTaxDataextendsEmployee{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);}// ...}
Тестирование важнее деплоя. Если у вас нет тестов или их мало, то каждый раз при выкладке кода на боевые сервера у вас нет уверенности, что ничего не сломалось. Решение о достаточном количестве тестов остается на совести вашей команды, но 100% покрытие тестами всех выражений и ветвлений обеспечивает высокое доверие к вашему коду и спокойствие всех разработчиков. Из этого следует, что в дополнение к отличному фреймворку для тестирования, необходимо также использоватьхороший инструмент покрытия.
Нет оправдания не писать тесты. В JavaScript cуществуетмножество хороших тестовых фреймворков, так что найдите подходящий для вас. А когда найдете, то стремитесь писать тесты для каждой новой фичи или нового модуля. Замечательно, если вы предпочитаете метод Разработки через тестирование (TDD), но главное убедиться, что перед запуском любой новой фичи или рефакторинга существующей вы достигаете достаточного уровня покрытия тестами.
Плохо:
constassert=require('assert');describe('MakeMomentJSGreatAgain',()=>{it('handles date boundaries',()=>{letdate;date=newMakeMomentJSGreatAgain('1/1/2015');date.addDays(30);assert.equal('1/31/2015',date);date=newMakeMomentJSGreatAgain('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);date=newMakeMomentJSGreatAgain('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});
Хорошо:
constassert=require('assert');describe('MakeMomentJSGreatAgain',()=>{it('handles 30-day months',()=>{constdate=newMakeMomentJSGreatAgain('1/1/2015');date.addDays(30);assert.equal('1/31/2015',date);});it('handles leap year',()=>{constdate=newMakeMomentJSGreatAgain('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);});it('handles non-leap year',()=>{constdate=newMakeMomentJSGreatAgain('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});
Callback-функции ухудшают читаемость и приводят к чрезмерному количеству вложенности. В ES2015/ES6 Промисы - встроенный глобальный тип. Используйте их!
Плохо:
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin',(requestErr,response)=>{if(requestErr){console.error(requestErr);}else{require('fs').writeFile('article.html',response.body,(writeErr)=>{if(writeErr){console.error(writeErr);}else{console.log('File written');}});}});
Хорошо:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin').then((response)=>{returnrequire('fs-promise').writeFile('article.html',response);}).then(()=>{console.log('File written');}).catch((err)=>{console.error(err);});
Промисы - очень чистая альтернатива callback-функциям, но ES2017/ES8 привносит async и await с еще более чистым решением. Все, что вам нужно, это функция с ключевым словомasync
, после чего вы можете писать логику императивно - без цепочекthen
. Если уже сегодня вы можете внедрить фичи ES2017/ES8, используйтеasync/await
!
Плохо:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin').then((response)=>{returnrequire('fs-promise').writeFile('article.html',response);}).then(()=>{console.log('File written');}).catch((err)=>{console.error(err);});
Хорошо:
asyncfunctiongetCleanCodeArticle(){try{constresponse=awaitrequire('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');awaitrequire('fs-promise').writeFile('article.html',response);console.log('File written');}catch(err){console.error(err);}}
Выброшенные ошибки - отличная штука! Они означают, что когда во время выполнения вашей программы что-то пошло не так, это было успешно зафиксировано и донесено до вас путем остановки выполнения функции, убийства процесса и уведомления в консоль с трассировкой стека.
Игнорирование пойманной ошибки не дает вам возможности исправить или каким-либо образом отреагировать на ее появление. Логирование ошибок в консоль (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(user);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;}
Они, как правило, просто добавляют шум. Пусть функции и имена переменных вместе с правильными отступами и форматированием задают визуальную структуру кода.
Плохо:
////////////////////////////////////////////////////////////////////////////////// Создание объекта////////////////////////////////////////////////////////////////////////////////$scope.model={menu:'foo',nav:'bar'};////////////////////////////////////////////////////////////////////////////////// Установка экшена////////////////////////////////////////////////////////////////////////////////constactions=function(){// ...};
Хорошо:
$scope.model={menu:'foo',nav:'bar'};constactions=function(){// ...};
Данное руководство также доступно на других языках:
Бразильский португальский:fesnt/clean-code-javascript
Китайский:alivebao/clean-code-js
Немецкий:marcbruederlin/clean-code-javascript
Корейский:qkraudghgh/clean-code-javascript-ko
About
🛁 Адаптированные для JavaScript концепции Чистого кода
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.