Концепции чистого кода адаптированные для TypeScript, вдохновленные clean-code-javascript.
Оригинал на английском clean-code-typescript
У переводчика не идеальные знания английского, указывайте пожалуйста на ошибки!
- Введение
- Переменные
- Функции
- Объекты и структуры данных
- Классы
- SOLID
- Тестирование
- Асинхронность
- Обработка ошибок
- Форматирование
- Комментарии
- Переводы
Инженерные принципы ПО, из книги 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){// ...}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-х аргументов приводит к комбинаторному взрыву, где вы должны протестировать множество вариантов с каждым отдельным аргументом
Один или два аргумента это идеальный случай, а три и более следует избегать, если это возможно. Большое количество аргументов лучше объеденять. Обычно если вы используете более двух аргументов, то ваша функция пытается делать слишком много. В случаях когда это не так, то лучше использовать объект верхнего уровня.
Подумайте о том чтобы использовать объектные литералы, если вам необходимо много аргументов.
Для того чтобы вы знали какие параметры ожидает функция, вы можете использовать синтаксис деструктуризации.
Он имеет несколько преимуществ:
Когда кто-то смотрит на синатуру функции, то сразу становится понятка какие свойства она использует.
Деструктуризация также клонирует примитивные значения аргумента-объекта переданного в функцию. Это помогает избежать сайд эффекта. Заметка: объекты и массивы которые деструктурированы из аргумента-объекта не клонируются.
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});Это одно из самых важных правил в разработке ПО. Когда функции решают больше одной задачи, их труднее объеденять, тестировать. Если вы сможете изолировать функцию так чтобы она выполняла только одну задачу, в дальнейшем она может быть легко переработана, а ваш код будет чище. Если вы запомните только это правило из этого руководства, то вы уже будете лучше многих разработчиков.
Плохо:
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();}Плохо:
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);Если у вас больше одного уровня абстракции, то обычно эта функция делает слишком многое. Разделение функций дает возможность переиспользования и простого тестирования.
Плохо:
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;}Делайте все возможное, чтобы избежать дублирования кода. Дублирование кода плохо, тем что если вам придется править логику, её придется править в нескольких местах.
Представьте если вы открыли ресторан и ведете учет ваших продуктов: всех ваших томатов, лука, чеснока, специй и д.р.. Если у вас закажут блюда из томатов то вам придется вносить изменения во все ваши списки. Если список будет только один то и править нужно будет только его.
Часто вы дублируете код из-за того что когда вам требуется реализовать два и более незначительно различающихся действий, которые очень похожи, но их различия заставляют вас завести несколько функций, делающий практически одно и тоже. Удаление дублирующихся частей кода, означает создание абстракции, обрабатывающий разную логику с помощью всего одной функции/модуля/класса.
Получение абстракции имеет важное значение, поэтому вы должны следовать принципам 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);});}Вы должны критически относиться к дублированию кода. Иногда существует компромисс между дублированием кода и увеличением сложности, вводя новую абстракцию. Когда две реализации из двух разных модулей выглядят одинаково, но существуют в разных доменах, дублирование может быть приемлемым и предпочтительным вариантом, нежели объединений в общий код. Перенос логики в общий код, вводит косвенную зависимость между двумя модулями.
Плохо:
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.
Флаги говорят пользователю, что функция совершает более одного действия. Функция должна решать одну задачу. Разделяйте функции, если они исполняют различные варианты кода на основе логического значения.
Плохо:
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);}Функция производит побочный эффект, если она совершает какое-либо действие помимо получения значения и возврата другого значения или значений. Побочный эффект может быть записью в файл, изменением каких-то глобальных переменных или случайным переводом всех ваших денег неизвестным лицам.
Впрочем, побочные эффекты в программе необходимы. Пусть, как и в предыдущем примере, вам требуется запись в файл. Опишите то, что вы хотите сделать, строго в одном месте. Не создавайте несколько функций и классов, которые пишут что-то в конкретный файл. Создайте один сервис, который всем этим занимается. Один и только один.
Суть в том, чтобы избегать распространенных ошибок, таких как, например, передача состояния между объектами без какой-либо структуры, с помощью изменяемых данных, которые может перезаписывать кто угодно, в обход централизованного места применения побочных эффектов. Если научитесь так делать, вы станете счастливее, чем подавляющее большинство других программистов.
Плохо:
// 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);В JavaScript примитивы передаются по значению, а объекты и массивы передаются по ссылке. В случае объектов или массивов, если ваша функция вносит изменения в корзину покупок(массив), например при добавлении элемента в массив, то любая другая функция использующая корзину массив будет зависеть от этого добавления. Это может быть как хорошо, так и плохо. Давайте представим плохую ситуации:
Пользователь нажимает кнопку "Купить" вызывающую функцию purchase которая делает сетевой запрос и отправляет корзину массив на сервер. Если происходит плохое подключение к сети функция должна отправить повторный запрос. Теперь, если пользователь случайно нажимает на кнопку "Добавить в корзину", но пока не хочет покупать товар? Если это произойдет и в этот момент начнется запрос на сервер, то функция purchase отправит случайно добавленный элемент, так как он имеет ссылку на корзину покупок, котора была изменена функцией addItemToCart. Путем добавления нежелательного элемента.
Хорошим бы решением было бы что бы функция addItemToCart всегда клонировала бы массив cart редактировала его и возвращала клон. Это бы гарантировало, что никакие другие функции, использующие ссылку на массив корзины покупок, не будут затронуты какими-либо изменениями.
Два предостережения по-поводу такого подхода:
- Возможны случаи, когда вы на самом деле хотите изменить объект по ссылке, но такие случаи крайне редки. Большинство функций могут быть объявлены без сайд эффектов! (Смотрите 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()}];};Загрязнение глобальных переменных — плохая практика в 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));};}Отдавайте предпочтение этому стилю программирования, когда можете.
Плохо:
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);Плохо:
if(subscription.isTrial||account.balance>0){// ...}Хорошо:
functioncanActivateService(subscription: Subscription,account: Account){returnsubscription.isTrial||account.balance>0}if(canActivateService(subscription,account)){// ...}Плохо:
functionisEmailNotUsed(email: string): boolean{// ...}if(isEmailNotUsed(email)){// ...}Хорошо:
functionisEmailUsed(email): boolean{// ...}if(!isEmailUsed(node)){// ...}Эта задача кажется невозможной. Большинство людей, впервые услышав это, говорят, "как я должен делать что-либо без выражения 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();}}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'));}Современные браузеры производят множество оптимизаций под капотом во время исполнения кода. Оптимизируя код вручную вы зачастую, просто тратите свое время. Есть хорошие ресурсы для того чтобы увидеть где оптимизация отсутствует. Поглядывайте на них в свободное время, пока эти проблемы не будут исправлены, если вообще будут, конечно.
Плохо:
// 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++){// ...}Мертвый код - так же плохо, как повторяющийся код. Нет никаких оснований продолжать хранить его в кодовой базе. Если он не используется, избавьтесь от него! В случае надобности, его всегда можно найти в истории версий.
Плохо:
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');Используйте генераторы и итераторы при работе с коллекциями данных, которые используются как поток. Есть несколько причин для этого:
- отделяет вызываемый объект от реализации генератора в том смысле, что вызываемый объект решает сколько элементов иметь для доступа
- ленивое выполнение, элементы передаются по требованию
- встроенная поддержка итерации элементов с использованием синтаксиса
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));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;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;}}Система типов в 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;// ошибкаИспользуйте типы, когда вам может понадобиться объединение или пересечение. Используйте интерфейс, когда хотите использовать extends или implements. Однако строгого правила не существует, используйте то, что работает у вас.
Для более подробного объяснения посмотрите это ответы о различиях между type and interface в TypeScript.
Плохо:
interfaceEmailConfig{// ...}interfaceDbConfig{// ...}interfaceConfig{// ...}//...typeShape={// ...}Хорошо:
typeEmailConfig={// ...}typeDbConfig={// ...}typeConfig=EmailConfig|DbConfig;// ...interfaceShape{// ...}classCircleimplementsShape{// ...}classSquareimplementsShape{// ...}Размер класса измеряется его ответственностью. Следуя Принципу единственной ответственности класс должен быть маленьким.
Плохо:
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{/* ... */}}// разделить обязанности, переместив оставшиеся методы в другие классы// ...Сплоченность определяет степень, в которой члены класса связаны друг с другом. В идеале все поля в классе должны использоваться каждым методом. Мы говорим, что класс максимально связный. На практике это, однако, не всегда возможно и даже нежелательно. Однако вы должны добиваться, того чтобы сплоченность была высокой.
Связанность относится и к тому, как связаны или зависимы два класса друг от друга. Классы считаются слабосвязанными если изменения в одном из них не влияют на другой.
Плохо:
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>{// ...}}Как сказано в Design Patterns от банды четырех вы должны Предпочитать композицию наследованию где можете. Есть много веских причин использовать наследование и много хороших причин использовать композицию. Суть этого принципа в том, что если ваш ум инстинктивно идет на наследование, попробуйте подумать, может ли композиция лучше смоделировать вашу проблему. В некоторых случаях может.
Тогда вы можете спросить: "Когда я должен использовать наследование?" Это зависит от вашей проблемы, но это достойный список, когда наследование имеет больше смысла, чем композиция:
Ваше наследование представляет собой "is-a" отношения а не "has-a" отношения (Human->Animal vs. User->UserDetails).
Вы можете повторно использовать код из базовых классов (Люди могут двигаться как все животные).
Вы хотите внести глобальные изменения в производные классы, изменив базовый класс. (Изменение расхода калорий у всех животных при их перемещении).
Плохо:
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){}// ...}Этот паттеррн очень полезен и обычно используется во многих библиотеках. Это позволяет вашему коду быть выразительным и менее многословным. По этой причине используйте цепочку методов и посмотрите, насколько чистым будет ваш код.
Плохо:
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();Как написано в Чистом Коде, "Должна быть лишь одна причина для изменения класса". Заманчиво представить себе класс, переполненный большим количеством функционала, словно в полет вам позволили взять всего один чемодан. Проблема в том, что ваш класс не будет концептуально связан, и вы будете часто его изменять. Очень важно минимизировать изменения в классе. Когда вы вносите изменения в класс с огромным функционалом, тяжело отследить последствия ваших изменений.
Плохо:
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()){// ...}}}Как заявил Бертран Мейер, "программные сущности (классы, модули, функции и т.д.) должны оставаться открытыми для расширения, но закрытыми для модификации." Что это означает на практике? Принцип закрепляет, что вы должны позволить пользователям добавлять новые функциональные возможности, но без изменения существующего кода.
Плохо:
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);// трансформируем ответ и возвращаем}}Это страшный термин для очень простой концепции. Формальным языком он звучит как "Если 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);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(){// ...}}Этот принцип закрепляет две важные вещи:
Модули высшего уровня не должны зависеть от модулей низшего уровня. Оба должны зависеть от абстракций.
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Сначала трудно понять этот принцип. Но если вы работали с 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');Тестирование важнее деплоя. Если у вас нет тестов или их мало, то каждый раз при выкладке кода на боевые сервера у вас не будет уверенности, что ничего не сломается. Решение о достаточном количестве тестов остается на совести вашей команды, но 100% покрытие тестами всех выражений и ветвлений обеспечивает высокое доверие к вашему коду и спокойствие всех разработчиков. Из этого следует, что в дополнение к отличному фреймворку для тестирования, необходимо также использовать хороший инструмент покрытия.
Нет никакого оправдания, чтобы не писать тесты. Есть много хороших фреймворков для тестирования на JS с поддержкой типов для TypeScript, так что вы найдите тот который понравится вашей команде. Когда вы найдете тот, который работает для вашей команды, тогда стремитесь всегда писать тесты для каждой новой фичи/модуля, которую вы пишете. Если вы предпочитаете метод тест-ориентированной разработки (TDD), это замечательно, но главное - просто убедиться, что вы достигли своих целей покрытия, прежде чем запускать какую-либо функцию или реорганизовать существующую.
Новый рабочий код пишется только после того, как будет написан модульный тест, который не проходит.
Вы пишете ровно такой объем кода модульного теста, какой необходим для того, чтобы этот тест не проходил (если код теста не компилируется, считается, что он не проходит).
Вы пишете ровно такой объем рабочего кода, какой необходим для прохождения модульного теста, который в данный момент не проходит.
Чистые тесты должны следовать правилам:
Быстрота(Fast) Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.
Независимость(Independent) Тесты не должны зависеть друг от друга. Они должны обеспечивать одинаковые выходные данные независимо от того, выполняются ли они независимо или все вместе в любом порядке.
Повторяемость(Repeatable) Тесты должны выполняться в любой среде, и не должно быть никаких оправданий тому, почему они провалились.
Очевидность(Self-Validating) Тест должен отвечать либо Passed, либо Failed. Вам не нужно сравнивать файлы логов, дл чтобы ответить, что тест пройден.
Своевременность(Timely) Юнит тесты должны быть написаны перед производственным кодом. Если вы пишете тесты после производственного кода, то вам может показаться, что писать тесты слишком сложно.
Тесты также должны соответствовать Принципу единой ответственности(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));});});Когда тест не пройден, его имя является первым признаком того, что могло пойти не так.
Плохо:
describe('Calendar',()=>{it('2/29/2020',()=>{// ...});it('throws',()=>{// ...});});Хорошо:
describe('Calendar',()=>{it('should handle leap year',()=>{// ...});it('should throw when format is invalid',()=>{// ...});});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 облегчает реализацию таких вещей, как тайм-ауты для промисов.
С помощью синтаксиса 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);}Бросать ошибки — хорошее решение! Это означает, что во время выполнения вы будете знать, если что-то пошло не так, вы сможете остановить выполнение вашего приложения убив процесс (в Node) в нужный момент и увидеть место ошибки с помощью стек трейса в консоли.
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};}Для подробного объяснения этой идеи обратитесь к оригинальному посту.
Игнорирование пойманной ошибки не дает вам возможности исправить или каким-либо образом отреагировать на ее появление. Логирование ошибок в консоль (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);}Вы не должны игнорировать ошибки в промисах по той же причине, что и в 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);}Форматирование носит субъективный характер. Как и во многом собранном здесь, в вопросе форматирования нет жестких правил, которым вы обязаны следовать. Главное - НЕ СПОРИТЬ по поводу форматирования. Есть множество инструментов для автоматизации этого. Используйте один! Это трата времени и денег когда инженеры спорят о форматировании. Общее правило, которому стоит следовать соблюдайте правила форматирования принятые в команде
Для TypeScript есть мощный инструмент под названием TSLint. Это статический анализ инструмент, который может помочь вам значительно улучшить читаемость и поддерживаемость вашего кода. Но лучше используйте ESLint, так как TSLint больше не поддерживается. Есть готовые к использованию конфигурации TSLint и ESLint, на которые вы можете ссылаться в своих проектах:
TSLint Config Standard - стандартный набор правил
TSLint Config Airbnb - правила от Airbnb
TSLint Clean Code - Правила TSLint которые вдохновлены Clean Code: A Handbook of Agile Software Craftsmanship
TSLint react - правила, связанные с React & JSX
TSLint + Prettier - правила линта для Prettier средство форматирования кода
ESLint rules for TSLint - ESLint правила для TypeScript
Immutable - правила отключения мутации в TypeScript
Обратитесь также к этому великому 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 для переменных, функций и членов класса.
Если функция вызывает другую, сохраните эти функции вертикально близко в исходном файле. В идеале, функция, которая использует другую функцию, должна быть прямо над ней. Мы склонны читать код сверху-вниз, как газету. Из-за этого удобно размещать код таким образом.
Плохо:
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();С помощью простых и понятных операторов импорта вы можете быстро увидеть зависимости текущего кода. Убедитесь, что вы используете следующие хорошие практики для операторов 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';Создайте более симпатичный импорт, определив пути и свойства baseUrl в разделе compilerOptions в tsconfig.json Это позволит избежать длинных относительных путей при импорте.
Плохо:
import{UserService}from'../../../services/UserService';Хорошо:
import{UserService}from'@services/UserService';// tsconfig.json ... "compilerOptions": { ... "baseUrl": "src","paths": {"@services": ["services/*"]}...}...Использование комментариев свидетельствует о невозможности высказаться без них. Код должен быть единственным источником правды.
Не комментируйте плохой код - переписывайте его. — Brian W. Kernighan and P. J. Plaugher
Комментарии - это извинения, а не требование. Хороший код в основном сам документирует себя.
Плохо:
// Check if subscription is active.if(subscription.endDate>Date.now){}Хорошо:
constisSubscriptionActive=subscription.endDate>Date.now;if(isSubscriptionActive){/* ... */}Системы контроля версий существуют не зря. Оставьте старый код в истории.
Плохо:
typeUser={name: string;email: string;// age: number;// jobPosition: string;}Хорошо:
typeUser={name: string;email: string;}Не забывайте использовать системы контроля версий! Нет необходимости в мертвом коде, закомментированом коде и особенно в журнальных комментариях. Используйте 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;}Они, как правило, просто добавляют шум. Пусть функции и имена переменных вместе с правильными отступами и форматированием задают визуальную структуру кода. Большинство 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{// ...}};Когда вы обнаружите, что вам нужно оставить заметки в коде для некоторых последующих улучшений, сделайте это с помощью комментариев // 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()}});}Это также доступно на других языках:
Brazilian Portuguese: vitorfreitas/clean-code-typescript
Chinese:
Japanese: MSakamaki/clean-code-typescript
Turkish: ozanhonamlioglu/clean-code-typescript
Ведется работа по переводу этого документа на другие языки:
Ссылки будут добавлены после завершения перевода. Проверьте это обсуждение для получения более подробной информации и прогресса. Вы можете внести незаменимый вклад в сообщество Чистый код, переведя его на свой язык.

