Skip to content

Real001/clean-code-typescript

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

82 Commits

Repository files navigation

clean-code-typescript Tweet

Концепции чистого кода адаптированные для TypeScript, вдохновленные clean-code-javascript.

Оригинал на английском clean-code-typescript

У переводчика не идеальные знания английского, указывайте пожалуйста на ошибки!

Содержание

  1. Введение
  2. Переменные
  3. Функции
  4. Объекты и структуры данных
  5. Классы
  6. SOLID
  7. Тестирование
  8. Асинхронность
  9. Обработка ошибок
  10. Форматирование
  11. Комментарии
  12. Переводы

Введение

Humorous image of software quality estimation as a count of how many expletives you shout when reading code

Инженерные принципы ПО, из книги Robert C. Martin' Clean Code, адаптированные для TypeScript. Это не руководство по стилю. Это руководство по написанию читаемого, переиспользуемого и пригодного для рефакторинга кода на TypeScript.

Не каждый принцип описанный здесь должен строго соблюдаться и еще меньше получать всеобщего признаний. Это принципы и ни чего более, оно они накапливались в течение многих лет с опытом коллектива авторов Чистого Кода

Ремеслу по написанию программного обеспечения чуть более 50 лет, но мы все еще многому учимся. Когда программная архитектура постареет до возрастра самой архитектуры, быть может тогда у нас появятся жесткие правила которым необходимо следовать. А сейчас пусть это служит критериями, с помощью которого вы будете оценивать качество вашего TypeScript кода и вашей команды.

И еще одна вещь: знание этих принципов не делает вас сразу лучшим разработчиком ПО, а их использование в течение многих лет не гарантирует, что вы не будете совершать ошибки. Каждый кусок кода начинается как черновик, как мокрый кусок глины который только постепенно приобретает свою форму. Не упрекайте себя при первых набросках кода, которые нуждаются в улучшении. Улучшайте код вместо этого!

⬆ Вернуться в начало

Переменные

Используйте выразительные имена переменных

Различайте имена таким образом, чтобы читатель знал что они означают.

Плохо:

functionbetween<T>(a1: T,a2: T,a3: T): boolean{returna2<=a1&&a1<=a3;}

Хорошо:

functionbetween<T>(value: T,left: T,right: T): boolean{returnleft<=value&&value<=right;}

⬆ Вернуться в начало

Используйте произносительные имена переменных

Если вы не можете произносить их, вы не можете обсуждать их не выглядя как идиот.

Плохо:

typeDtaRcrd102={genymdhms: Date;modymdhms: Date;pszqint: number;}

Хорошо:

typeCustomer={generationTimestamp: Date;modificationTimestamp: Date;recordId: number;}

⬆ Вернуться в начало

Используйте один и тот же словарь для одних и тех же типов переменных

Плохо:

functiongetUserInfo(): User;functiongetUserDetails(): User;functiongetUserData(): User;

Хорошо:

functiongetUser(): User;

⬆ Вернуться в начало

Используйте имена, доступные для поиска

Мы читаем больше кода, чем пишем. Это важно чтобы код, который мы пишем, был читаемым и доступным для поиска. Не называйте переменные, которые в конечном итоге имеют смысл только для наших программ мы вредим нашим читателям. Делайте ваши имена доступными для поиска. Такие инструменты, как TSLint и ESLint могут помочь идентифицировать не названные константы.

Плохо:

// What the heck is 86400000 for?setTimeout(restart,86400000);

Хорошо:

// Declare them as capitalized named constants.constMILLISECONDS_IN_A_DAY=24*60*60*1000;setTimeout(restart,MILLISECONDS_IN_A_DAY);

⬆ Вернуться в начало

Используйте объясняющие переменные

Плохо:

declareconstusers: Map<string,User>;for(constkeyValueofusers){// iterate through users map}

Хорошо:

declareconstusers: Map<string,User>;for(const[id,user]ofusers){// iterate through users map}

⬆ Вернуться в начало

Избегайте мысленного связывания

Явное лучше, чем неявное. Ясность - это король.

Плохо:

constu=getUser();consts=getSubscription();constt=charge(u,s);

Хорошо:

constuser=getUser();constsubscription=getSubscription();consttransaction=charge(user,subscription);

⬆ Вернуться в начало

Не добавляйте не нужный контекст

Если имя вашего класса/типа/объекта говорит само за себя, не повторяйте его в вашем имени переменной.

Плохо:

typeCar={carMake: string;carModel: string;carColor: string;}functionprint(car: Car): void{console.log(`${car.carMake}${car.carModel} (${car.carColor})`);}

Хорошо:

typeCar={make: string;model: string;color: string;}functionprint(car: Car): void{console.log(`${car.make}${car.model} (${car.color})`);}

⬆ Вернуться в начало

Используйте аргументы по умолчанию вместо замыканий или вычислений

Аргументы по умолчанию часто чище, чем короткое вычисление.

Плохо:

functionloadPages(count?: number){constloadCount=count!==undefined ? count : 10;// ...}

Хорошо:

functionloadPages(count: number=10){// ...}

⬆ Вернуться в начало

Используйте enum для документирования

Enam'ы могут помочь документированию вашего кода. Например когда мы обеспокоены тем, что наши переменные отличаются от значений.

Плохо:

constGENRE={ROMANTIC: 'romantic',DRAMA: 'drama',COMEDY: 'comedy',DOCUMENTARY: 'documentary',}projector.configureFilm(GENRE.COMEDY);classProjector{// delactation of ProjectorconfigureFilm(genre){switch(genre){caseGENRE.ROMANTIC: // some logic to be executed }}}

Хорошо:

enumGENRE{ROMANTIC,DRAMA,COMEDY,DOCUMENTARY,}projector.configureFilm(GENRE.COMEDY);classProjector{// delactation of ProjectorconfigureFilm(genre){switch(genre){caseGENRE.ROMANTIC: // some logic to be executed }}}

⬆ Вернуться в начало

Функции

Аргументы функции (идеально два или меньше)

Ограничение количества параметров функции невероятно важно, потому что это делает тестирование ваших функций проще. Наличие более 3-х аргументов приводит к комбинаторному взрыву, где вы должны протестировать множество вариантов с каждым отдельным аргументом

Один или два аргумента это идеальный случай, а три и более следует избегать, если это возможно. Большое количество аргументов лучше объеденять. Обычно если вы используете более двух аргументов, то ваша функция пытается делать слишком много. В случаях когда это не так, то лучше использовать объект верхнего уровня.

Подумайте о том чтобы использовать объектные литералы, если вам необходимо много аргументов.

Для того чтобы вы знали какие параметры ожидает функция, вы можете использовать синтаксис деструктуризации.

Он имеет несколько преимуществ:

  1. Когда кто-то смотрит на синатуру функции, то сразу становится понятка какие свойства она использует.

  2. Деструктуризация также клонирует примитивные значения аргумента-объекта переданного в функцию. Это помогает избежать сайд эффекта. Заметка: объекты и массивы которые деструктурированы из аргумента-объекта не клонируются.

  3. TypeScript предупреждает о неиспользуемых свойствах, это было бы не возможно без деструктуризации.

Плохо:

functioncreateMenu(title: string,body: string,buttonText: string,cancellable: boolean){// ...}createMenu('Foo','Bar','Baz',true);

Хорошо:

functioncreateMenu(options: {title: string,body: string,buttonText: string,cancellable: boolean}){// ...}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true});

Вы можете еще больше повысить читаемость, если будете использовать type aliases:

typeMenuOptions={title: string,body: string,buttonText: string,cancellable: boolean};functioncreateMenu(options: MenuOptions){// ...}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true});

⬆ back to top

Функции должны выполнять одну задачу

Это одно из самых важных правил в разработке ПО. Когда функции решают больше одной задачи, их труднее объеденять, тестировать. Если вы сможете изолировать функцию так чтобы она выполняла только одну задачу, в дальнейшем она может быть легко переработана, а ваш код будет чище. Если вы запомните только это правило из этого руководства, то вы уже будете лучше многих разработчиков.

Плохо:

functionemailClients(clients: Client[]){clients.forEach((client)=>{constclientRecord=database.lookup(client);if(clientRecord.isActive()){email(client);}});}

Хорошо:

functionemailClients(clients: Client[]){clients.filter(isActiveClient).forEach(email);}functionisActiveClient(client: Client){constclientRecord=database.lookup(client);returnclientRecord.isActive();}

⬆ back to top

Название функций должны описывать что они делают

Плохо:

functionaddToDate(date: Date,month: number): Date{// ...}constdate=newDate();// It's hard to tell from the function name what is addedaddToDate(date,1);

Хорошо:

functionaddMonthToDate(date: Date,month: number): Date{// ...}constdate=newDate();addMonthToDate(date,1);

⬆ back to top

Функции должны иметь один уровень абстракции

Если у вас больше одного уровня абстракции, то обычно эта функция делает слишком многое. Разделение функций дает возможность переиспользования и простого тестирования.

Плохо:

functionparseCode(code: string){constREGEXES=[/* ... */];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((regex)=>{statements.forEach((statement)=>{// ...});});constast=[];tokens.forEach((token)=>{// lex...});ast.forEach((node)=>{// parse...});}

Хорошо:

constREGEXES=[/* ... */];functionparseCode(code: string){consttokens=tokenize(code);constsyntaxTree=parse(tokens);syntaxTree.forEach((node)=>{// parse...});}functiontokenize(code: string): Token[]{conststatements=code.split(' ');consttokens: Token[]=[];REGEXES.forEach((regex)=>{statements.forEach((statement)=>{tokens.push(/* ... */);});});returntokens;}functionparse(tokens: Token[]): SyntaxTree{constsyntaxTree: SyntaxTree[]=[];tokens.forEach((token)=>{syntaxTree.push(/* ... */);});returnsyntaxTree;}

⬆ back to top

Удаляйте дублированный код

Делайте все возможное, чтобы избежать дублирования кода. Дублирование кода плохо, тем что если вам придется править логику, её придется править в нескольких местах.

Представьте если вы открыли ресторан и ведете учет ваших продуктов: всех ваших томатов, лука, чеснока, специй и д.р.. Если у вас закажут блюда из томатов то вам придется вносить изменения во все ваши списки. Если список будет только один то и править нужно будет только его.

Часто вы дублируете код из-за того что когда вам требуется реализовать два и более незначительно различающихся действий, которые очень похожи, но их различия заставляют вас завести несколько функций, делающий практически одно и тоже. Удаление дублирующихся частей кода, означает создание абстракции, обрабатывающий разную логику с помощью всего одной функции/модуля/класса.

Получение абстракции имеет важное значение, поэтому вы должны следовать принципам SOLID. Плохие абстракции могут оказаться хуже дублирующего кода, будьте осторожны! Если вы можете сделать хорошую абстракцию делайте. Не повторяйте себя в противном случае вы можете обнаружить себя вносящим изменения в разные места, для одной единственной логики.

Плохо:

functionshowDeveloperList(developers: Developer[]){developers.forEach((developer)=>{constexpectedSalary=developer.calculateExpectedSalary();constexperience=developer.getExperience();constgithubLink=developer.getGithubLink();constdata={ expectedSalary, experience, githubLink };render(data);});}functionshowManagerList(managers: Manager[]){managers.forEach((manager)=>{constexpectedSalary=manager.calculateExpectedSalary();constexperience=manager.getExperience();constportfolio=manager.getMBAProjects();constdata={ expectedSalary, experience, portfolio };render(data);});}

Хорошо:

classDeveloper{// ...getExtraDetails(){return{githubLink: this.githubLink,}}}classManager{// ...getExtraDetails(){return{portfolio: this.portfolio,}}}functionshowEmployeeList(employee: Developer|Manager){employee.forEach((employee)=>{constexpectedSalary=employee.calculateExpectedSalary();constexperience=employee.getExperience();constextra=employee.getExtraDetails();constdata={ expectedSalary, experience, extra,};render(data);});}

Вы должны критически относиться к дублированию кода. Иногда существует компромисс между дублированием кода и увеличением сложности, вводя новую абстракцию. Когда две реализации из двух разных модулей выглядят одинаково, но существуют в разных доменах, дублирование может быть приемлемым и предпочтительным вариантом, нежели объединений в общий код. Перенос логики в общий код, вводит косвенную зависимость между двумя модулями.

⬆ back to top

Устанавливайте объекты по умолчанию с помощью Object.assign или деструктуризации

Плохо:

typeMenuConfig={title?: string,body?: string,buttonText?: string,cancellable?: boolean};functioncreateMenu(config: MenuConfig){config.title=config.title||'Foo';config.body=config.body||'Bar';config.buttonText=config.buttonText||'Baz';config.cancellable=config.cancellable!==undefined ? config.cancellable : true;// ...}createMenu({body: 'Bar'});

Хорошо:

typeMenuConfig={title?: string,body?: string,buttonText?: string,cancellable?: boolean};functioncreateMenu(config: MenuConfig){constmenuConfig=Object.assign({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true},config);// ...}createMenu({body: 'Bar'});

Кроме того можно использовать деструктуризацию со значениями по умолчанию:

typeMenuConfig={title?: string,body?: string,buttonText?: string,cancellable?: boolean};functioncreateMenu({ title ='Foo', body ='Bar', buttonText ='Baz', cancellable =true}: MenuConfig){// ...}createMenu({body: 'Bar'});

Чтобы избежать каких-либо побочных эффектов и неожиданного поведения передавая явно undefined или null вы можете сказать компилятору TypeScript чтобы он не разрешал этого. Смотрите --strictNullChecks опция для TypeScript.

⬆ back to top

Не используйте флаги в качестве параметров функции

Флаги говорят пользователю, что функция совершает более одного действия. Функция должна решать одну задачу. Разделяйте функции, если они исполняют различные варианты кода на основе логического значения.

Плохо:

functioncreateFile(name: string,temp: boolean){if(temp){fs.create(`./temp/${name}`);}else{fs.create(name);}}

Хорошо:

functioncreateTempFile(name: string){createFile(`./temp/${name}`);}functioncreateFile(name: string){fs.create(name);}

⬆ back to top

Избегайте побочных эффектов (часть 1)

Функция производит побочный эффект, если она совершает какое-либо действие помимо получения значения и возврата другого значения или значений. Побочный эффект может быть записью в файл, изменением каких-то глобальных переменных или случайным переводом всех ваших денег неизвестным лицам.

Впрочем, побочные эффекты в программе необходимы. Пусть, как и в предыдущем примере, вам требуется запись в файл. Опишите то, что вы хотите сделать, строго в одном месте. Не создавайте несколько функций и классов, которые пишут что-то в конкретный файл. Создайте один сервис, который всем этим занимается. Один и только один.

Суть в том, чтобы избегать распространенных ошибок, таких как, например, передача состояния между объектами без какой-либо структуры, с помощью изменяемых данных, которые может перезаписывать кто угодно, в обход централизованного места применения побочных эффектов. Если научитесь так делать, вы станете счастливее, чем подавляющее большинство других программистов.

Плохо:

// Global variable referenced by following function.letname='Robert C. Martin';functiontoBase64(){name=btoa(name);}toBase64();// If we had another function that used this name, now it'd be a Base64 valueconsole.log(name);// expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='

Хорошо:

constname='Robert C. Martin';functiontoBase64(text: string): string{returnbtoa(text);}constencodedName=toBase64(name);console.log(name);

⬆ back to top

Избегайте побочных эффектов (Часть 2)

В JavaScript примитивы передаются по значению, а объекты и массивы передаются по ссылке. В случае объектов или массивов, если ваша функция вносит изменения в корзину покупок(массив), например при добавлении элемента в массив, то любая другая функция использующая корзину массив будет зависеть от этого добавления. Это может быть как хорошо, так и плохо. Давайте представим плохую ситуации:

Пользователь нажимает кнопку "Купить" вызывающую функцию purchase которая делает сетевой запрос и отправляет корзину массив на сервер. Если происходит плохое подключение к сети функция должна отправить повторный запрос. Теперь, если пользователь случайно нажимает на кнопку "Добавить в корзину", но пока не хочет покупать товар? Если это произойдет и в этот момент начнется запрос на сервер, то функция purchase отправит случайно добавленный элемент, так как он имеет ссылку на корзину покупок, котора была изменена функцией addItemToCart. Путем добавления нежелательного элемента.

Хорошим бы решением было бы что бы функция addItemToCart всегда клонировала бы массив cart редактировала его и возвращала клон. Это бы гарантировало, что никакие другие функции, использующие ссылку на массив корзины покупок, не будут затронуты какими-либо изменениями.

Два предостережения по-поводу такого подхода:

  1. Возможны случаи, когда вы на самом деле хотите изменить объект по ссылке, но такие случаи крайне редки. Большинство функций могут быть объявлены без сайд эффектов! (Смотрите pure function)

2.Клонирование больших объектов может быть очень нагрузочным и влиять на производительность. К счастью, это не является большой проблемой на практике, потому что есть отличные библиотеки, которые позволяют клонировать объекты с меньшей нагрузкой на память в отличии от клонирования вручную.

Плохо:

functionaddItemToCart(cart: CartItem[],item: Item): void{cart.push({ item,date: Date.now()});};

Хорошо:

functionaddItemToCart(cart: CartItem[],item: Item): CartItem[]{return[...cart,{ item,date: Date.now()}];};

⬆ back to top

Не пишите глобальные функции

Загрязнение глобальных переменных — плохая практика в JavaScript, так как может породить конфликты с другой библиотекой, и пользователь вашего API не увидит ошибок, пока не получит исключение в продакшене. Давайте рассмотрим пример: что если вы хотите расширить стандартный метод Array JavaScript, имея метод diff который бы вычислял различие между двумя массивами? Вы должны были бы записать новую функцию в Array.prototype, но тогда она может войти в конфликт с другой библиотекой, которая пыталась сделать то же самое. Что если другая библиотека использовала метод diff, чтобы найти разницу между первым и последним элементами массива? В этом случае лучше использовать классы и просто сделать наследование от глобального Array.

Плохо:

declare global {interfaceArray<T>{diff(other: T[]): Array<T>;}}if(!Array.prototype.diff){Array.prototype.diff=function<T>(other: T[]): T[]{consthash=newSet(other);returnthis.filter(elem=>!hash.has(elem));};}

Хорошо:

classMyArray<T>extendsArray<T>{diff(other: T[]): T[]{consthash=newSet(other);returnthis.filter(elem=>!hash.has(elem));};}

⬆ back to top

Предпочтение функциональное программирование над императивным

Отдавайте предпочтение этому стилю программирования, когда можете.

Плохо:

constcontributions=[{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<contributions.length;i++){totalOutput+=contributions[i].linesOfCode;}

Хорошо:

constcontributions=[{name: 'Uncle Bobby',linesOfCode: 500},{name: 'Suzie Q',linesOfCode: 1500},{name: 'Jimmy Gosling',linesOfCode: 150},{name: 'Gracie Hopper',linesOfCode: 1000}];consttotalOutput=contributions.reduce((totalLines,output)=>totalLines+output.linesOfCode,0);

⬆ back to top

Инкапсулируйте условия

Плохо:

if(subscription.isTrial||account.balance>0){// ...}

Хорошо:

functioncanActivateService(subscription: Subscription,account: Account){returnsubscription.isTrial||account.balance>0}if(canActivateService(subscription,account)){// ...}

⬆ back to top

Избегайте негативных условий

Плохо:

functionisEmailNotUsed(email: string): boolean{// ...}if(isEmailNotUsed(email)){// ...}

Хорошо:

functionisEmailUsed(email): boolean{// ...}if(!isEmailUsed(node)){// ...}

⬆ back to top

Избегайте условных операторов

Эта задача кажется невозможной. Большинство людей, впервые услышав это, говорят, "как я должен делать что-либо без выражения if?". Ответ заключается в том, что во многих случаях для достижения тех же целей можно использовать полиморфизм. Второй вопрос обычно, "хорошо, замечательно, но почему я должен их избегать?" Ответ, предыдущая концепция чистого кода, которую вы узнали: функция должна выполнять только одну задачу. Когда у вас есть классы и функции, содержащие конструкцию if, вы говорите своему пользователю, что ваша функция выполняет больше одной задачи. Запомните, делать только одну задачу.

Плохо:

classAirplane{privatetype: string;// ...getCruisingAltitude(){switch(this.type){case'777': returnthis.getMaxAltitude()-this.getPassengerCount();case'Air Force One': returnthis.getMaxAltitude();case'Cessna': returnthis.getMaxAltitude()-this.getFuelExpenditure();default: thrownewError('Unknown airplane type.');}}privategetMaxAltitude(): number{// ...}}

Хорошо:

abstractclassAirplane{protectedgetMaxAltitude(): number{// shared logic with subclasses ...}// ...}classBoeing777extendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getPassengerCount();}}classAirForceOneextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude();}}classCessnaextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getFuelExpenditure();}}

⬆ back to top

Избегайте проверки типов

TypeScript является надмножеством синтаксиса JavaScript и добавляют дополнительные статические проверки типов для языка. Всегда предпочитайте указывать типы переменных, параметров и возвращаемых значений, чтобы использовать всю мощь TypeScript. Это делает будущий рефакторинг более легким.

Плохо:

functiontravelToTexas(vehicle: Bicycle|Car){if(vehicleinstanceofBicycle){vehicle.pedal(currentLocation,newLocation('texas'));}elseif(vehicleinstanceofCar){vehicle.drive(currentLocation,newLocation('texas'));}}

Хорошо:

typeVehicle=Bicycle|Car;functiontravelToTexas(vehicle: Vehicle){vehicle.move(currentLocation,newLocation('texas'));}

⬆ back to top

Не делайте слишком много оптимизаций

Современные браузеры производят множество оптимизаций под капотом во время исполнения кода. Оптимизируя код вручную вы зачастую, просто тратите свое время. Есть хорошие ресурсы для того чтобы увидеть где оптимизация отсутствует. Поглядывайте на них в свободное время, пока эти проблемы не будут исправлены, если вообще будут, конечно.

Плохо:

// On old browsers, each iteration with uncached `list.length` would be costly// because of `list.length` recomputation. In modern browsers, this is optimized.for(leti=0,len=list.length;i<len;i++){// ...}

Хорошо:

for(leti=0;i<list.length;i++){// ...}

⬆ back to top

Удаляйте мертвый код

Мертвый код - так же плохо, как повторяющийся код. Нет никаких оснований продолжать хранить его в кодовой базе. Если он не используется, избавьтесь от него! В случае надобности, его всегда можно найти в истории версий.

Плохо:

functionoldRequestModule(url: string){// ...}functionrequestModule(url: string){// ...}constreq=requestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');

Хорошо:

functionrequestModule(url: string){// ...}constreq=requestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');

⬆ back to top

Используйте итераторы и генераторы

Используйте генераторы и итераторы при работе с коллекциями данных, которые используются как поток. Есть несколько причин для этого:

  • отделяет вызываемый объект от реализации генератора в том смысле, что вызываемый объект решает сколько элементов иметь для доступа
  • ленивое выполнение, элементы передаются по требованию
  • встроенная поддержка итерации элементов с использованием синтаксиса for-of
  • итераторы позволяют реализовать оптимизированные паттерны итераторов

Плохо:

functionfibonacci(n: number): number[]{if(n===1)return[0];if(n===2)return[0,1];constitems: number[]=[0,1];while(items.length<n){items.push(items[items.length-2]+items[items.length-1]);}returnitems;}functionprint(n: number){fibonacci(n).forEach(fib=>console.log(fib));}// Print first 10 Fibonacci numbers.print(10);

Хорошо:

// Generates an infinite stream of Fibonacci numbers.// The generator doesn't keep the array of all numbers.function*fibonacci(): IterableIterator<number>{let[a,b]=[0,1];while(true){yielda;[a,b]=[b,a+b];}}functionprint(n: number){leti=0;for(constfiboffibonacci()){if(i++===n)break;console.log(fib);}}// Print first 10 Fibonacci numbers.print(10);

Существуют библиотеки, которые позволяют работать с итераторами так же, как и с собственными массивами, путем цепочка методов, таких как map, slice, forEach и др. Смотрите itiriri пример продвинутой манипуляции с итераторами (или itiriri-async для манипуляции с асинхронными итераторами).

importitiririfrom'itiriri';function*fibonacci(): IterableIterator<number>{let[a,b]=[0,1];while(true){yielda;[a,b]=[b,a+b];}}itiriri(fibonacci()).take(10).forEach(fib=>console.log(fib));

⬆ back to top

Объекты и структуры данных

Используйте геттеры и сеттеры

TypeScript поддерживает синтаксис геттеров и сеттеров. Использовать геттеры и сеттеры для доступа к данным объекта гораздо лучше, чем напрямую обращаться к его свойствам. "Почему?" спросите вы. Вот список причин:

  • Если вы хотите реализовать больше, чем просто доступ к свойству, вам нужно поменять реализацию в одном месте, а не по всему коду.
  • Валидацию легко реализовать на уровне реализации set.
  • Инкапсуляция внутреннего состояния.
  • Легко добавить логирование и обработку ошибок на уровне геттеров и сеттеров.
  • Вы можете лениво подгружать свойства вашего объекта, например, с сервера.

Плохо:

typeBankAccount={balance: number;// ...}constvalue=100;constaccount: BankAccount={balance: 0,// ...};if(value<0){thrownewError('Cannot set negative balance.');}account.balance=value;

Хорошо:

classBankAccount{privateaccountBalance: number=0;getbalance(): number{returnthis.accountBalance;}setbalance(value: number){if(value<0){thrownewError('Cannot set negative balance.');}this.accountBalance=value;}// ...}// Теперь `BankAccount` инкапсулирует логику проверки.// Если однажды спецификации изменятся, и нам понадобится дополнительное правило проверки,// нам придется изменить только реализацию `сеттера`,// оставив весь зависимый код без изменений.constaccount=newBankAccount();account.balance=100;

⬆ back to top

Создавайте объекты с приватными/защищенными полями

TypeScript поддерживает public(по умолчанию), protected и private средства доступа к свойствам класса.

Плохо:

classCircle{radius: number;constructor(radius: number){this.radius=radius;}perimeter(){return2*Math.PI*this.radius;}surface(){returnMath.PI*this.radius*this.radius;}}

Хорошо:

classCircle{constructor(privatereadonlyradius: number){}perimeter(){return2*Math.PI*this.radius;}surface(){returnMath.PI*this.radius*this.radius;}}

⬆ back to top

Используйте иммутабельность

Система типов в TypeScript позволяет помечать отдельные свойства интерфейса/класса как readonly поля (только для чтения). Это позволяет вам работать функционально (неожиданная мутация это плохо).
Для более сложных сценариев есть встроенный тип Readonly, который принимает тип T и помечает все его свойства только для чтения с использованием mapped types (смотрите mapped types).

Плохо:

interfaceConfig{host: string;port: string;db: string;}

Хорошо:

interfaceConfig{readonlyhost: string;readonlyport: string;readonlydb: string;}

В случае массива вы можете создать массив только для чтения, используя ReadonlyArray<T>. который не позволяет делать изменения с использованием push() и fill(), но можно использовать concat() и slice() они не меняют значения.

Плохо:

constarray: number[]=[1,3,5];array=[];// errorarray.push(100);// array will updated

Хорошо:

constarray: ReadonlyArray<number>=[1,3,5];array=[];// errorarray.push(100);// error

Объявление аргументов только для чтения TypeScript 3.4 is a bit easier.

functionhoge(args: readonlystring[]){args.push(1);// error}

Предпочтение const assertions для литеральных значений.

Плохо:

constconfig={hello: 'world'};config.hello='world';// значение измененоconstarray=[1,3,5];array[0]=10;// значение изменено// записываемые объекты возвращаютсяfunctionreadonlyData(value: number){return{ value };}constresult=readonlyData(100);result.value=200;// значение изменено

Хорошо:

// объект только для чтенияconstconfig={hello: 'world'}asconst;config.hello='world';// ошибка// массив только для чтенияconstarray=[1,3,5]asconst;array[0]=10;// ошибка// Вы можете вернуть объект только для чтенияfunctionreadonlyData(value: number){return{ value }asconst;}constresult=readonlyData(100);result.value=200;// ошибка

⬆ back to top

Типы vs. интерфейсы

Используйте типы, когда вам может понадобиться объединение или пересечение. Используйте интерфейс, когда хотите использовать extends или implements. Однако строгого правила не существует, используйте то, что работает у вас.
Для более подробного объяснения посмотрите это ответы о различиях между type and interface в TypeScript.

Плохо:

interfaceEmailConfig{// ...}interfaceDbConfig{// ...}interfaceConfig{// ...}//...typeShape={// ...}

Хорошо:

typeEmailConfig={// ...}typeDbConfig={// ...}typeConfig=EmailConfig|DbConfig;// ...interfaceShape{// ...}classCircleimplementsShape{// ...}classSquareimplementsShape{// ...}

⬆ back to top

Классы

Классы должны быть маленькими

Размер класса измеряется его ответственностью. Следуя Принципу единственной ответственности класс должен быть маленьким.

Плохо:

classDashboard{getLanguage(): string{/* ... */}setLanguage(language: string): void{/* ... */}showProgress(): void{/* ... */}hideProgress(): void{/* ... */}isDirty(): boolean{/* ... */}disable(): void{/* ... */}enable(): void{/* ... */}addSubscription(subscription: Subscription): void{/* ... */}removeSubscription(subscription: Subscription): void{/* ... */}addUser(user: User): void{/* ... */}removeUser(user: User): void{/* ... */}goToHomePage(): void{/* ... */}updateProfile(details: UserDetails): void{/* ... */}getVersion(): string{/* ... */}// ...}

Хорошо:

classDashboard{disable(): void{/* ... */}enable(): void{/* ... */}getVersion(): string{/* ... */}}// разделить обязанности, переместив оставшиеся методы в другие классы// ...

⬆ back to top

Высокая сплоченность низкая связь

Сплоченность определяет степень, в которой члены класса связаны друг с другом. В идеале все поля в классе должны использоваться каждым методом. Мы говорим, что класс максимально связный. На практике это, однако, не всегда возможно и даже нежелательно. Однако вы должны добиваться, того чтобы сплоченность была высокой.

Связанность относится и к тому, как связаны или зависимы два класса друг от друга. Классы считаются слабосвязанными если изменения в одном из них не влияют на другой.

Плохо:

classUserManager{// Плохо: каждая закрытая переменная используется той или иной группой методов.// Это ясно показывает, что класс несет больше, чем одну ответственность// Если мне нужно только создать сервис, чтобы получить транзакции для пользователя,// Я все еще вынужден передавать экземпляр `emailSender`.constructor(privatereadonlydb: Database,privatereadonlyemailSender: EmailSender){}asyncgetUser(id: number): Promise<User>{returnawaitdb.users.findOne({ id });}asyncgetTransactions(userId: number): Promise<Transaction[]>{returnawaitdb.transactions.find({ userId });}asyncsendGreeting(): Promise<void>{awaitemailSender.send('Welcome!');}asyncsendNotification(text: string): Promise<void>{awaitemailSender.send(text);}asyncsendNewsletter(): Promise<void>{// ...}}

Хорошо:

classUserService{constructor(privatereadonlydb: Database){}asyncgetUser(id: number): Promise<User>{returnawaitthis.db.users.findOne({ id });}asyncgetTransactions(userId: number): Promise<Transaction[]>{returnawaitthis.db.transactions.find({ userId });}}classUserNotifier{constructor(privatereadonlyemailSender: EmailSender){}asyncsendGreeting(): Promise<void>{awaitthis.emailSender.send('Welcome!');}asyncsendNotification(text: string): Promise<void>{awaitthis.emailSender.send(text);}asyncsendNewsletter(): Promise<void>{// ...}}

⬆ back to top

Предпочитайте композицию наследованию

Как сказано в Design Patterns от банды четырех вы должны Предпочитать композицию наследованию где можете. Есть много веских причин использовать наследование и много хороших причин использовать композицию. Суть этого принципа в том, что если ваш ум инстинктивно идет на наследование, попробуйте подумать, может ли композиция лучше смоделировать вашу проблему. В некоторых случаях может.

Тогда вы можете спросить: "Когда я должен использовать наследование?" Это зависит от вашей проблемы, но это достойный список, когда наследование имеет больше смысла, чем композиция:

  1. Ваше наследование представляет собой "is-a" отношения а не "has-a" отношения (Human->Animal vs. User->UserDetails).

  2. Вы можете повторно использовать код из базовых классов (Люди могут двигаться как все животные).

  3. Вы хотите внести глобальные изменения в производные классы, изменив базовый класс. (Изменение расхода калорий у всех животных при их перемещении).

Плохо:

classEmployee{constructor(privatereadonlyname: string,privatereadonlyemail: string){}// ...}// Плохо, потому что Employees "имеют" налоговые данные. EmployeeTaxData не является типом EmployeeclassEmployeeTaxDataextendsEmployee{constructor(name: string,email: string,privatereadonlyssn: string,privatereadonlysalary: number){super(name,email);}// ...}

Хорошо:

classEmployee{privatetaxData: EmployeeTaxData;constructor(privatereadonlyname: string,privatereadonlyemail: string){}setTaxData(ssn: string,salary: number): Employee{this.taxData=newEmployeeTaxData(ssn,salary);returnthis;}// ...}classEmployeeTaxData{constructor(publicreadonlyssn: string,publicreadonlysalary: number){}// ...}

⬆ back to top

Используйте цепочки вызовов

Этот паттеррн очень полезен и обычно используется во многих библиотеках. Это позволяет вашему коду быть выразительным и менее многословным. По этой причине используйте цепочку методов и посмотрите, насколько чистым будет ваш код.

Плохо:

classQueryBuilder{privatecollection: string;privatepageNumber: number=1;privateitemsPerPage: number=100;privateorderByFields: string[]=[];from(collection: string): void{this.collection=collection;}page(number: number,itemsPerPage: number=100): void{this.pageNumber=number;this.itemsPerPage=itemsPerPage;}orderBy(...fields: string[]): void{this.orderByFields=fields;}build(): Query{// ...}}// ...constqueryBuilder=newQueryBuilder();queryBuilder.from('users');queryBuilder.page(1,100);queryBuilder.orderBy('firstName','lastName');constquery=queryBuilder.build();

Хорошо:

classQueryBuilder{privatecollection: string;privatepageNumber: number=1;privateitemsPerPage: number=100;privateorderByFields: string[]=[];from(collection: string): this {this.collection=collection;returnthis;}page(number: number,itemsPerPage: number=100): this {this.pageNumber=number;this.itemsPerPage=itemsPerPage;returnthis;}orderBy(...fields: string[]): this {this.orderByFields=fields;returnthis;}build(): Query{// ...}}// ...constquery=newQueryBuilder().from('users').page(1,100).orderBy('firstName','lastName').build();

⬆ back to top

SOLID

Принцип единой ответственности (SRP)

Как написано в Чистом Коде, "Должна быть лишь одна причина для изменения класса". Заманчиво представить себе класс, переполненный большим количеством функционала, словно в полет вам позволили взять всего один чемодан. Проблема в том, что ваш класс не будет концептуально связан, и вы будете часто его изменять. Очень важно минимизировать изменения в классе. Когда вы вносите изменения в класс с огромным функционалом, тяжело отследить последствия ваших изменений.

Плохо:

classUserSettings{constructor(privatereadonlyuser: User){}changeSettings(settings: UserSettings){if(this.verifyCredentials()){// ...}}verifyCredentials(){// ...}}

Хорошо:

classUserAuth{constructor(privatereadonlyuser: User){}verifyCredentials(){// ...}}classUserSettings{privatereadonlyauth: UserAuth;constructor(privatereadonlyuser: User){this.auth=newUserAuth(user);}changeSettings(settings: UserSettings){if(this.auth.verifyCredentials()){// ...}}}

⬆ back to top

Принцип открытости/закрытости (OCP)

Как заявил Бертран Мейер, "программные сущности (классы, модули, функции и т.д.) должны оставаться открытыми для расширения, но закрытыми для модификации." Что это означает на практике? Принцип закрепляет, что вы должны позволить пользователям добавлять новые функциональные возможности, но без изменения существующего кода.

Плохо:

classAjaxAdapterextendsAdapter{constructor(){super();}// ...}classNodeAdapterextendsAdapter{constructor(){super();}// ...}classHttpRequester{constructor(privatereadonlyadapter: Adapter){}asyncfetch<T>(url: string): Promise<T>{if(this.adapterinstanceofAjaxAdapter){constresponse=awaitmakeAjaxCall<T>(url);// трансформируем ответ и возвращаем}elseif(this.adapterinstanceofNodeAdapter){constresponse=awaitmakeHttpCall<T>(url);// трансформируем ответ и возвращаем}}}functionmakeAjaxCall<T>(url: string): Promise<T>{// запрос и возвращение промиса}functionmakeHttpCall<T>(url: string): Promise<T>{// запрос и возвращение промиса}

Хорошо:

abstractclassAdapter{abstractasyncrequest<T>(url: string): Promise<T>;// общий код для подклассов ...}classAjaxAdapterextendsAdapter{constructor(){super();}asyncrequest<T>(url: string): Promise<T>{// запрос и возвращение промиса}// ...}classNodeAdapterextendsAdapter{constructor(){super();}asyncrequest<T>(url: string): Promise<T>{// запрос и возвращение промиса}// ...}classHttpRequester{constructor(privatereadonlyadapter: Adapter){}asyncfetch<T>(url: string): Promise<T>{constresponse=awaitthis.adapter.request<T>(url);// трансформируем ответ и возвращаем}}

⬆ back to top

Принцип подстановки Лисков (LSP)

Это страшный термин для очень простой концепции. Формальным языком он звучит как "Если S является подтипом T, то объекты типа Т могут быть заменены на объекты типа S (то есть, объекты типа S могут заменить объекты типа Т) без влияния на важные свойства программы (корректность, пригодность для выполнения задач и т.д.)." Это еще более страшное определение.

Лучшее объяснение заключается в том, что если у вас есть родительский и дочерний классы, то они могут использоваться как взаимозаменяемые, не приводя при этом к некорректным результатам. Это по-прежнему может сбивать с толку, так что давайте взглянем на классический пример квадрата-прямоугольника. Математически квадрат представляет собой прямоугольник, но если вы смоделируете их отношения через наследование, вы быстро наткнетесь на неприятности..

Плохо:

classRectangle{constructor(protectedwidth: number=0,protectedheight: number=0){}setColor(color: string): this {// ...}render(area: number){// ...}setWidth(width: number): this {this.width=width;returnthis;}setHeight(height: number): this {this.height=height;returnthis;}getArea(): number{returnthis.width*this.height;}}classSquareextendsRectangle{setWidth(width: number): this {this.width=width;this.height=width;returnthis;}setHeight(height: number): this {this.width=height;this.height=height;returnthis;}}functionrenderLargeRectangles(rectangles: Rectangle[]){rectangles.forEach((rectangle)=>{constarea=rectangle.setWidth(4).setHeight(5).getArea();// BAD: Returns 25 for Square. Should be 20.rectangle.render(area);});}constrectangles=[newRectangle(),newRectangle(),newSquare()];renderLargeRectangles(rectangles);

Хорошо:

abstractclassShape{setColor(color: string): this {// ...}render(area: number){// ...}abstractgetArea(): number;}classRectangleextendsShape{constructor(privatereadonlywidth=0,privatereadonlyheight=0){super();}getArea(): number{returnthis.width*this.height;}}classSquareextendsShape{constructor(privatereadonlylength: number){super();}getArea(): number{returnthis.length*this.length;}}functionrenderLargeShapes(shapes: Shape[]){shapes.forEach((shape)=>{constarea=shape.getArea();shape.render(area);});}constshapes=[newRectangle(4,5),newRectangle(4,5),newSquare(5)];renderLargeShapes(shapes);

⬆ back to top

Принцип разделения интерфейса (ISP)

ISP говорит что "Клиенты не должны зависеть от классов, которые они не используют.". Этот принцип очень связан с Принципом единой ответственности. На самом деле это означает, что вы всегда должны проектировать свои абстракции таким образом, чтобы клиенты, которые используют открытые методы не получали весь пирог. Это также включает в себя возложение на клиентов бремени реализации методов, которые им на самом деле не нужны.

Плохо:

interfaceSmartPrinter{print();fax();scan();}classAllInOnePrinterimplementsSmartPrinter{print(){// ...}fax(){// ...}scan(){// ...}}classEconomicPrinterimplementsSmartPrinter{print(){// ...}fax(){thrownewError('Fax not supported.');}scan(){thrownewError('Scan not supported.');}}

Хорошо:

interfacePrinter{print();}interfaceFax{fax();}interfaceScanner{scan();}classAllInOnePrinterimplementsPrinter,Fax,Scanner{print(){// ...}fax(){// ...}scan(){// ...}}classEconomicPrinterimplementsPrinter{print(){// ...}}

⬆ back to top

Принцип инверсии зависимостей (DIP)

Этот принцип закрепляет две важные вещи:

  1. Модули высшего уровня не должны зависеть от модулей низшего уровня. Оба должны зависеть от абстракций.

  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Сначала трудно понять этот принцип. Но если вы работали с AngularJS, вы видели реализацию этого принципа в виде Dependency Injection (DI). Несмотря на то, что они не являются идентичными понятиями, DIP даёт возможность отграничить модули высокого уровня от деталей модулей низкого уровня и установки их. Он может сделать это через DI. Этот принцип уменьшает связь между модулями. Если ваши модули тесно связаны, их тяжело рефакторить.

DIP обычно достигается использованием контейнера инверсии управления (IoC). Пример мощного контейнера IoC для TypeScript это InversifyJs

Плохо:

import{readFileasreadFileCb}from'fs';import{promisify}from'util';constreadFile=promisify(readFileCb);typeReportData={// ..}classXmlFormatter{parse<T>(content: string): T{// Converts an XML string to an object T}}classReportReader{// BAD: We have created a dependency on a specific request implementation.// We should just have ReportReader depend on a parse method: `parse`privatereadonlyformatter=newXmlFormatter();asyncread(path: string): Promise<ReportData>{consttext=awaitreadFile(path,'UTF8');returnthis.formatter.parse<ReportData>(text);}}// ...constreader=newReportReader();awaitreport=awaitreader.read('report.xml');

Хорошо:

import{readFileasreadFileCb}from'fs';import{promisify}from'util';constreadFile=promisify(readFileCb);typeReportData={// ..}interfaceFormatter{parse<T>(content: string): T;}classXmlFormatterimplementsFormatter{parse<T>(content: string): T{// Converts an XML string to an object T}}classJsonFormatterimplementsFormatter{parse<T>(content: string): T{// Converts a JSON string to an object T}}classReportReader{constructor(privatereadonlyformatter: Formatter){}asyncread(path: string): Promise<ReportData>{consttext=awaitreadFile(path,'UTF8');returnthis.formatter.parse<ReportData>(text);}}// ...constreader=newReportReader(newXmlFormatter());awaitreport=awaitreader.read('report.xml');// or if we had to read a json reportconstreader=newReportReader(newJsonFormatter());awaitreport=awaitreader.read('report.json');

⬆ back to top

Тестирование

Тестирование важнее деплоя. Если у вас нет тестов или их мало, то каждый раз при выкладке кода на боевые сервера у вас не будет уверенности, что ничего не сломается. Решение о достаточном количестве тестов остается на совести вашей команды, но 100% покрытие тестами всех выражений и ветвлений обеспечивает высокое доверие к вашему коду и спокойствие всех разработчиков. Из этого следует, что в дополнение к отличному фреймворку для тестирования, необходимо также использовать хороший инструмент покрытия.

Нет никакого оправдания, чтобы не писать тесты. Есть много хороших фреймворков для тестирования на JS с поддержкой типов для TypeScript, так что вы найдите тот который понравится вашей команде. Когда вы найдете тот, который работает для вашей команды, тогда стремитесь всегда писать тесты для каждой новой фичи/модуля, которую вы пишете. Если вы предпочитаете метод тест-ориентированной разработки (TDD), это замечательно, но главное - просто убедиться, что вы достигли своих целей покрытия, прежде чем запускать какую-либо функцию или реорганизовать существующую.

Три закона TDD

  1. Новый рабочий код пишется только после того, как будет написан модульный тест, который не проходит.

  2. Вы пишете ровно такой объем кода модульного теста, какой необходим для того, чтобы этот тест не проходил (если код теста не компилируется, считается, что он не проходит).

  3. Вы пишете ровно такой объем рабочего кода, какой необходим для прохождения модульного теста, который в данный момент не проходит.

⬆ back to top

Правила F.I.R.S.T.

Чистые тесты должны следовать правилам:

  • Быстрота(Fast) Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.

  • Независимость(Independent) Тесты не должны зависеть друг от друга. Они должны обеспечивать одинаковые выходные данные независимо от того, выполняются ли они независимо или все вместе в любом порядке.

  • Повторяемость(Repeatable) Тесты должны выполняться в любой среде, и не должно быть никаких оправданий тому, почему они провалились.

  • Очевидность(Self-Validating) Тест должен отвечать либо Passed, либо Failed. Вам не нужно сравнивать файлы логов, дл чтобы ответить, что тест пройден.

  • Своевременность(Timely) Юнит тесты должны быть написаны перед производственным кодом. Если вы пишете тесты после производственного кода, то вам может показаться, что писать тесты слишком сложно.

⬆ back to top

Один кейс на тест

Тесты также должны соответствовать Принципу единой ответственности(SPP). Делайте только одно утверждение за единицу теста.(ps. не пренебрегайте этим правилом)

Плохо:

import{assert}from'chai';describe('AwesomeDate',()=>{it('handles date boundaries',()=>{letdate: AwesomeDate;date=newAwesomeDate('1/1/2015');assert.equal('1/31/2015',date.addDays(30));date=newAwesomeDate('2/1/2016');assert.equal('2/29/2016',date.addDays(28));date=newAwesomeDate('2/1/2015');assert.equal('3/1/2015',date.addDays(28));});});

Хорошо:

import{assert}from'chai';describe('AwesomeDate',()=>{it('handles 30-day months',()=>{constdate=newAwesomeDate('1/1/2015');assert.equal('1/31/2015',date.addDays(30));});it('handles leap year',()=>{constdate=newAwesomeDate('2/1/2016');assert.equal('2/29/2016',date.addDays(28));});it('handles non-leap year',()=>{constdate=newAwesomeDate('2/1/2015');assert.equal('3/1/2015',date.addDays(28));});});

⬆ back to top

Название теста должно раскрывать его намерение

Когда тест не пройден, его имя является первым признаком того, что могло пойти не так.

Плохо:

describe('Calendar',()=>{it('2/29/2020',()=>{// ...});it('throws',()=>{// ...});});

Хорошо:

describe('Calendar',()=>{it('should handle leap year',()=>{// ...});it('should throw when format is invalid',()=>{// ...});});

⬆ back to top

Асинхронность

Используйте promises а не callbacks

Callback-функции ухудшают читаемость и приводят к чрезмерному количеству вложенности (ад обратных вызовов(callback hell)). Существуют утилиты, которые преобразуют существующие функции, используя стиль callback-ов, в версию, которая возвращает промисы (для Node.js смотрите util.promisify, для общего назначения смотрите pify, es6-promisify)

Плохо:

import{get}from'request';import{writeFile}from'fs';functiondownloadPage(url: string,saveTo: string,callback: (error: Error,content?: string)=>void){get(url,(error,response)=>{if(error){callback(error);}else{writeFile(saveTo,response.body,(error)=>{if(error){callback(error);}else{callback(null,response.body);}});}});}downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin','article.html',(error,content)=>{if(error){console.error(error);}else{console.log(content);}});

Хорошо:

import{get}from'request';import{writeFile}from'fs';import{promisify}from'util';constwrite=promisify(writeFile);functiondownloadPage(url: string,saveTo: string): Promise<string>{returnget(url).then(response=>write(saveTo,response));}downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin','article.html').then(content=>console.log(content)).catch(error=>console.error(error));

Промисы поддерживают несколько вспомогательных методов, которые помогают сделать код более понятным:

МетодыОписание
Promise.resolve(value)Преобразуйте значение в решенный промис.
Promise.reject(error)Преобразуйте ошибку в отклоненный промис.
Promise.all(promises)Возвращает новый промис, который выполняется с массивом значений выполнения для переданных промисов или отклоняется по причине первого промиса, который выполняется с ошибкой.
Promise.race(promises)Возвращает новый промис, который выполнен/отклонен с результатом/ошибкой первого выполненного промиса из массива переданных промисов.

Promise.all особенно полезен, когда есть необходимость запускать задачи параллельно. Promise.race облегчает реализацию таких вещей, как тайм-ауты для промисов.

⬆ back to top

Async/Await делает код чище, чем промисы

С помощью синтаксиса async`` await вы можете написать код, который будет намного чище и понятнее, чем промисы, связанные цепочкой. Внутри функции с префиксом ключевого слова async у вас есть способ указать среде выполнения JavaScript приостановить выполнение кода по ключевому слову await (при использовании в промисе).

Плохо:

import{get}from'request';import{writeFile}from'fs';import{promisify}from'util';constwrite=util.promisify(writeFile);functiondownloadPage(url: string,saveTo: string): Promise<string>{returnget(url).then(response=>write(saveTo,response));}downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin','article.html').then(content=>console.log(content)).catch(error=>console.error(error));

Хорошо:

import{get}from'request';import{writeFile}from'fs';import{promisify}from'util';constwrite=promisify(writeFile);asyncfunctiondownloadPage(url: string,saveTo: string): Promise<string>{constresponse=awaitget(url);awaitwrite(saveTo,response);returnresponse;}// где-то в асинхронной функцииtry{constcontent=awaitdownloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin','article.html');console.log(content);}catch(error){console.error(error);}

⬆ back to top

Обработка ошибок

Бросать ошибки — хорошее решение! Это означает, что во время выполнения вы будете знать, если что-то пошло не так, вы сможете остановить выполнение вашего приложения убив процесс (в Node) в нужный момент и увидеть место ошибки с помощью стек трейса в консоли.

Всегда используйте ошибки для отклонений(reject)

JavaScript и TypeScript позволяют вам делать throw любым объектом. Промис также может быть отклонен с любым объектом причины. Рекомендуется использовать синтаксис throw с типом Error. Это потому что ваша ошибка может быть поймана в более высоком уровне кода с синтаксисом catch. Было бы очень странно поймать там строковое сообщение и сделать отладку более болезненной. По той же причине вы должны отклонять промисы с типами Error.

Плохо:

functioncalculateTotal(items: Item[]): number{throw'Not implemented.';}functionget(): Promise<Item[]>{returnPromise.reject('Not implemented.');}

Хорошо:

functioncalculateTotal(items: Item[]): number{thrownewError('Not implemented.');}functionget(): Promise<Item[]>{returnPromise.reject(newError('Not implemented.'));}// or equivalent to:asyncfunctionget(): Promise<Item[]>{thrownewError('Not implemented.');}

Преимущество использования типов Error заключается в том, что они поддерживается синтаксисом try/catch/finally и неявно всеми ошибками и имеют свойство stack, которое является очень мощным для отладки. Есть и другие альтернативы: не использовать синтаксис throw и вместо этого всегда возвращать пользовательские объекты ошибок. TypeScript делает это еще проще. Рассмотрим следующий пример:

typeResult<R>={isError: false,value: R};typeFailure<E>={isError: true,error: E};typeFailable<R,E>=Result<R>|Failure<E>;functioncalculateTotal(items: Item[]): Failable<number,'empty'>{if(items.length===0){return{isError: true,error: 'empty'};}// ...return{isError: false,value: 42};}

Для подробного объяснения этой идеи обратитесь к оригинальному посту.

⬆ back to top

Не игнорируйте отловленные ошибки

Игнорирование пойманной ошибки не дает вам возможности исправить или каким-либо образом отреагировать на ее появление. Логирование ошибок в консоль (console.log) не намного лучше, так как зачастую оно может потеряться в море консольных записей. Оборачивание куска кода в try/catch означает, что вы предполагаете возможность появления ошибки и имеете на этот случай четкий план.

Плохо:

try{functionThatMightThrow();}catch(error){console.log(error);}// or even worsetry{functionThatMightThrow();}catch(error){// ignore error}

Хорошо:

import{logger}from'./logging'try{functionThatMightThrow();}catch(error){logger.log(error);}

⬆ back to top

Не игнорируйте ошибки, возникшие в промисах

Вы не должны игнорировать ошибки в промисах по той же причине, что и в try/catch.

Плохо:

getUser().then((user: User)=>{returnsendEmail(user.email,'Welcome!');}).catch((error)=>{console.log(error);});

Хорошо:

import{logger}from'./logging'getUser().then((user: User)=>{returnsendEmail(user.email,'Welcome!');}).catch((error)=>{logger.log(error);});// or using the async/await syntax:try{constuser=awaitgetUser();awaitsendEmail(user.email,'Welcome!');}catch(error){logger.log(error);}

⬆ back to top

Форматирование

Форматирование носит субъективный характер. Как и во многом собранном здесь, в вопросе форматирования нет жестких правил, которым вы обязаны следовать. Главное - НЕ СПОРИТЬ по поводу форматирования. Есть множество инструментов для автоматизации этого. Используйте один! Это трата времени и денег когда инженеры спорят о форматировании. Общее правило, которому стоит следовать соблюдайте правила форматирования принятые в команде

Для TypeScript есть мощный инструмент под названием TSLint. Это статический анализ инструмент, который может помочь вам значительно улучшить читаемость и поддерживаемость вашего кода. Но лучше используйте ESLint, так как TSLint больше не поддерживается. Есть готовые к использованию конфигурации TSLint и ESLint, на которые вы можете ссылаться в своих проектах:

Обратитесь также к этому великому TypeScript StyleGuide and Coding Conventions источнику.

Используйте один вариант именования

Использование заглавных букв говорит вам о ваших переменных, функциях и др.. Эти правила субъективны, поэтому ваша команда может выбирать все, что они хотят. Дело в том, что независимо от того, что вы все выберите, просто будьте последовательны.

Плохо:

constDAYS_IN_WEEK=7;constdaysInMonth=30;constsongs=['Back In Black','Stairway to Heaven','Hey Jude'];constArtists=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestore_database(){}typeanimal={/* ... */}typeContainer={/* ... */}

Хорошо:

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(){}typeAnimal={/* ... */}typeContainer={/* ... */}

Предпочитайте использовать PascalCase для имен классов, интерфейсов, типов и пространств имен. Предпочитаю использовать camelCase для переменных, функций и членов класса.

⬆ back to top

Связанные функции должны находится рядом

Если функция вызывает другую, сохраните эти функции вертикально близко в исходном файле. В идеале, функция, которая использует другую функцию, должна быть прямо над ней. Мы склонны читать код сверху-вниз, как газету. Из-за этого удобно размещать код таким образом.

Плохо:

classPerformanceReview{constructor(privatereadonlyemployee: Employee){}privatelookupPeers(){returndb.lookup(this.employee.id,'peers');}privatelookupManager(){returndb.lookup(this.employee,'manager');}privategetPeerReviews(){constpeers=this.lookupPeers();// ...}review(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();// ...}privategetManagerReview(){constmanager=this.lookupManager();}privategetSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.review();

Хорошо:

classPerformanceReview{constructor(privatereadonlyemployee: Employee){}review(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();// ...}privategetPeerReviews(){constpeers=this.lookupPeers();// ...}privatelookupPeers(){returndb.lookup(this.employee.id,'peers');}privategetManagerReview(){constmanager=this.lookupManager();}privatelookupManager(){returndb.lookup(this.employee,'manager');}privategetSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.review();

⬆ back to top

Организация импортов

С помощью простых и понятных операторов импорта вы можете быстро увидеть зависимости текущего кода. Убедитесь, что вы используете следующие хорошие практики для операторов import:

  • Операторы импорта должны быть в алфавитном порядке и сгруппированы.
  • Неиспользованный импорт должен быть удален.
  • Именованные импорты должны быть в алфавитном порядке (т.е. import{A, B, C} from 'foo';)
  • Источники импорта должны быть в алфавитном порядке в группах, т.е.: import * as foo from 'a'; import * as bar from 'b';
  • Группы импорта обозначены пустыми строками.
  • Группы должны соблюдать следующий порядок:
    • Полифилы (т.е. import 'reflect-metadata';)
    • Модули сборки Node (т.е. import fs from 'fs';)
    • Внешние модули (т.е. import{query } from 'itiriri';)
    • Внутренние модули (т.е. import{UserService } from 'src/services/userService';)
    • Модули из родительского каталога (т.е. import foo from '../foo'; import qux from '../../foo/qux';)
    • Модули из того же или родственного каталога (т.е. import bar from './bar'; import baz from './bar/baz';)

Плохо:

import{TypeDefinition}from'../types/typeDefinition';import{AttributeTypes}from'../model/attribute';import{ApiCredentials,Adapters}from'./common/api/authorization';importfsfrom'fs';import{ConfigPlugin}from'./plugins/config/configPlugin';import{BindingScopeEnum,Container}from'inversify';import'reflect-metadata';

Хорошо:

import'reflect-metadata';importfsfrom'fs';import{BindingScopeEnum,Container}from'inversify';import{AttributeTypes}from'../model/attribute';import{TypeDefinition}from'../types/typeDefinition';import{ApiCredentials,Adapters}from'./common/api/authorization';import{ConfigPlugin}from'./plugins/config/configPlugin';

⬆ back to top

Используйте typescript алиасы

Создайте более симпатичный импорт, определив пути и свойства baseUrl в разделе compilerOptions в tsconfig.json Это позволит избежать длинных относительных путей при импорте.

Плохо:

import{UserService}from'../../../services/UserService';

Хорошо:

import{UserService}from'@services/UserService';
// tsconfig.json ... "compilerOptions": { ... "baseUrl": "src","paths": {"@services": ["services/*"]}...}...

⬆ back to top

Комментарии

Использование комментариев свидетельствует о невозможности высказаться без них. Код должен быть единственным источником правды.

Не комментируйте плохой код - переписывайте его. — Brian W. Kernighan and P. J. Plaugher

Предпочитаю понятный код вместо комментариев

Комментарии - это извинения, а не требование. Хороший код в основном сам документирует себя.

Плохо:

// Check if subscription is active.if(subscription.endDate>Date.now){}

Хорошо:

constisSubscriptionActive=subscription.endDate>Date.now;if(isSubscriptionActive){/* ... */}

⬆ back to top

Не оставляйте закомментированный код в вашей кодовой базе

Системы контроля версий существуют не зря. Оставьте старый код в истории.

Плохо:

typeUser={name: string;email: string;// age: number;// jobPosition: string;}

Хорошо:

typeUser={name: string;email: string;}

⬆ back to top

Не заводите журнальных комментариев

Не забывайте использовать системы контроля версий! Нет необходимости в мертвом коде, закомментированом коде и особенно в журнальных комментариях. Используйте git log, чтобы получить историю!

Плохо:

/** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Added type-checking (LI) * 2015-03-14: Implemented combine (JR) */functioncombine(a: number,b: number): number{returna+b;}

Хорошо:

functioncombine(a: number,b: number): number{returna+b;}

⬆ back to top

Избегайте маркеров позиционирования

Они, как правило, просто добавляют шум. Пусть функции и имена переменных вместе с правильными отступами и форматированием задают визуальную структуру кода. Большинство IDE поддерживают функцию свертывания кода, которая позволяет свернуть/развернуть блоки кода (смотрите Visual Studio Code folding regions).

Плохо:

////////////////////////////////////////////////////////////////////////////////// Client class////////////////////////////////////////////////////////////////////////////////classClient{id: number;name: string;address: Address;contact: Contact;////////////////////////////////////////////////////////////////////////////////// public methods////////////////////////////////////////////////////////////////////////////////publicdescribe(): string{// ...}////////////////////////////////////////////////////////////////////////////////// private methods////////////////////////////////////////////////////////////////////////////////privatedescribeAddress(): string{// ...}privatedescribeContact(): string{// ...}};

Хорошо:

classClient{id: number;name: string;address: Address;contact: Contact;publicdescribe(): string{// ...}privatedescribeAddress(): string{// ...}privatedescribeContact(): string{// ...}};

⬆ back to top

TODO комментарии

Когда вы обнаружите, что вам нужно оставить заметки в коде для некоторых последующих улучшений, сделайте это с помощью комментариев // TODO. Большинство IDE имеют специальную поддержку для так что вы можете быстро просмотреть весь список todo.

Однако имейте в виду, что комментарий TODO не является оправданием для плохого кода.

Плохо:

functiongetActiveSubscriptions(): Promise<Subscription[]>{// ensure `dueDate` is indexed.returndb.subscriptions.find({dueDate: {$lte: newDate()}});}

Хорошо:

functiongetActiveSubscriptions(): Promise<Subscription[]>{// TODO: ensure `dueDate` is indexed.returndb.subscriptions.find({dueDate: {$lte: newDate()}});}

⬆ back to top

Переводы

Это также доступно на других языках:

Ведется работа по переводу этого документа на другие языки:

  • kr Korean

Ссылки будут добавлены после завершения перевода. Проверьте это обсуждение для получения более подробной информации и прогресса. Вы можете внести незаменимый вклад в сообщество Чистый код, переведя его на свой язык.

About

Концепции чистого кода адаптированные для TypeScript

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript100.0%