- Notifications
You must be signed in to change notification settings - Fork22
Conceitos do livro "Clean Code" adaptados para TypeScript
License
vitorfreitas/clean-code-typescript
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Conceitos deCódigo Limpo adaptados para TypeScript.Inspirado emclean-code-javascript
- Introdução
- Variáveis
- Funções
- Objetos e Estruturas de dados
- Classes
- SOLID
- Testando
- Concorrência
- Tratamento de erros
- Formatação
- Comentários
Principios da engenharia de software, do livro de Robert C. MartinCódigo Limpo, para TypeScript. Isto não é um style guide.É um guia para desenvolver softwarelegível, reutilizavel, e refatorável em TypeScript.
Nem todos principios contidos aqui tem de ser seguidos estritamente,e muito menos irão ser universalmente aprovados.Estes são apenas guias e nada mais, mas que foram codificados durante muitoanos por experiências coletivas dos autores deCódigo Limpo.
Nosso trabalho de engenharia de software tem aproximadamente 50 anos de idade,e ainda estamos aprendendo muito. Quando arquitetura de software foralgo antigo como uma arquitetura em si, talvez então teremos regras maisrígidas para serem seguidas. Por enquanto, deixe esses guias servirem comoreferências pelo qual podemos avaliar a qualidade do código JavaScript quevocê e seu time produz.
Mais uma coisa: Saber de tudo isso não vai te tornar um melhor desenvolvedorimediatamente, e trabalhar com isso por muitos anos não significa que vocênão irá cometer erros. Todo pedaço de código começa como um rascunho, comoargila se moldando para sua forma final. E finalmente, cortamos fora asimperfeições quando revemos isso com nossos parceiros. Não se deixe abalarpelos primeiros rascunhos que precisam de melhorias. Mande ver no seu código aoinvés disso!
Diferencie os nomes de tal forma que o leitor saiba as diferença entre eles
Ruim:
functionbetween<T>(a1:T,a2:T,a3:T){returna2<=a1&&a1<=a3;}
Bom:
functionbetween<T>(value:T,left:T,right:T){returnleft<=value&&value<=right;}
Se você não consegue pronunciar sua variável, você não consegue argumentar semparecer um idiota.
Ruim:
classDtaRcrd102{privategenymdhms:Date;privatemodymdhms:Date;privatepszqint='102';}
Bom:
classCustomer{privategenerationTimestamp:Date;privatemodificationTimestamp:Date;privaterecordId='102';}
Ruim:
functiongetUserInfo():User;functiongetUserDetails():User;functiongetUserData():User;
Bom:
functiongetUser():User;
Nós vamos ler mais código do que escrever. É importante que o código que escrevemos seja legível e fácil de achar. Ao não nomear variáveis que acabam sendo inúteis para entender nosso programa, machucamos nossos leitores. Faça seus nomes pesquisáveis. Ferramentas comoTSLint podem te ajudar a identificar constantes sem nome.
Ruim:
// What the heck is 86400000 for?setTimeout(restart,86400000);
Bom:
// Declare them as capitalized named constants.constMILLISECONDS_IN_A_DAY=24*60*60*1000;setTimeout(restart,MILLISECONDS_IN_A_DAY);
Ruim:
declareconstusers:Map<string,User>;for(constkeyValueofusers){// iterate through users map}
Bom:
declareconstusers:Map<string,User>;for(const[id,user]ofusers){// iterate through users map}
Explicito é melhor que implito.Clareza é um Rei.
Ruim:
constu=getUser();consts=getSubscription();constt=charge(u,s);
Bom:
constuser=getUser();constsubscription=getSubscription();consttransaction=charge(user,subscription);
If your class/object name tells you something, don't repeat that in your variable name.
Se o nome da sua classe/objeto expressa algo, não repita isso no nome da variável.
Ruim:
typeCar={carMake:string;carModel:string;carColor:string;};functionprint(car:Car):void{console.log(`${this.carMake}${this.carModel} (${this.carColor})`);}
Bom:
typeCar={make:string;model:string;color:string;};functionprint(car:Car):void{console.log(`${this.make}${this.model} (${this.color})`);}
Argumentos padrões são normalmente mais limpos que condicionais.
Ruim:
functionloadPages(count?:number){constloadCount=count!==undefined ?count :10;// ...}
Bom:
functionloadPages(count:number=10){// ...}
Limitar a quantidade de parametros de uma função é incrivelmente importantanteporque isso torna sua função fácil de testar.Ter mais de três de leva em uma explosão onde você tem que testar várioscasos diferentes, com argumentos separados.
Um ou dois argumentos é o caso ideal, e três deve ser evitado se possível.Algo além disso deve ser deixado de lado.Usualmente, se você tem mais de dois argumentos, suas funções estão tentando fazercoisas demais.Nos casos que não estão, na maior parte do tempo um objeto irá ser o suficiente como argumento.
Considere usar objetos caso sinta necessidade de enviar muitos argumentos.
Para deixar explicitos quais propriedades suas funções esperam, você pode usardesestruturação.Aqui vão algumas vantagens:
Quando alguém olhar a assinatura da função, imediatamente será claro quais propriedades estão sendo usadas.
Desestruturação também clone os valores primitivos especificados do objeto passado como argumento para a função. Isso ajuda a evitar efeitos colaterais. Nota: Objetos e Arrays que são desestruturados do objetoargument não são clonados.
TypeScript irá te avisar quando haver propriedades não utilizadas, o que seria impossivel sem usar desestruturação.
Ruim:
functioncreateMenu(title:string,body:string,buttonText:string,cancellable:boolean,){// ...}createMenu('Foo','Bar','Baz',true);
Bom:
functioncreateMenu(options:{title:string;body:string;buttonText:string;cancellable:boolean;}){// ...}createMenu({title:'Foo',body:'Bar',buttonText:'Baz',cancellable:true,});
Você ainda pode aumentar a legibilidade ao utilizar ostype aliases do TypeScript.
typeMenuOptions={title:string;body:string;buttonText:string;cancellable:boolean;};functioncreateMenu(options:MenuOptions){// ...}createMenu({title:'Foo',body:'Bar',buttonText:'Baz',cancellable:true,});
Esta é, de longe, a regra mais importante da engenharia de software. Quando funções fazem mais de uma coisa, elas são mais difíceis de compor, testar e pensar sobre. Quando você consegue isolar a função para apenas uma ação, elas podem ser refatoradas fácilmente e teu código será fácilmente lido. Se você ignorar todo o resto deste guia além dessa dica, você já estará a frente de vários outros desenvolvedores.
Ruim:
functionemailClients(clients:Client[]){clients.forEach(client=>{constclientRecord=database.lookup(client);if(clientRecord.isActive()){email(client);}});}
Bom:
functionemailClients(clients:Client[]){clients.filter(isActiveClient).forEach(email);}functionisActiveClient(client:Client){constclientRecord=database.lookup(client);returnclientRecord.isActive();}
Ruim:
functionaddToDate(date:Date,month:number):Date{// ...}constdate=newDate();// It's hard to tell from the function name what is addedaddToDate(date,1);
Bom:
functionaddMonthToDate(date:Date,month:number):Date{// ...}constdate=newDate();addMonthToDate(date,1);
Quando você tem mais de um nível de abstração possívelmente sua função está fazendo coisa demais. Dividir suas funções desencadeia em código reusável e fácil de testar.
Ruim:
functionparseCode(code:string){constREGEXES=[/* ... */];conststatements=code.split(' ');consttokens=[];REGEXES.forEach(regex=>{statements.forEach(statement=>{// ...});});constast=[];tokens.forEach(token=>{// lex...});ast.forEach(node=>{// parse...});}
Bom:
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;}
Faça o seu melhor para evitar código duplicado.Código duplicado é ruim pois significa que há mais de um lugar para ser alterado se houver alguma mudança na lógica.
Imagine que você tem um restaurante que mantém uma lista do seu inventário: todos seus tomates, cebolas, alho, pimentas, etc.Se você tem multiplas listas que contém esses dados, então todas irão ser modificadas quando você servir um prato com tomates, por exemplo.Se você tem apenas uma lista, então este será o único lugar a ser alterado.
Algumas vezes você tem códigos duplicados porque há duas ou mais coisas diferentes, mas que compartilham muito em comum, mas suas diferenças os forçam a ter duas ou mais funções separadas que fazem muito das mesmas coisas. Remover código duplicado significa criar uma abstração que pode lidar com essas diferenças com apenas uma função/módulo/classe.
Ter sua abstração do jeito certo é algo crítico, por isso você deve seguir os principiosSOLID. Más abstrações podem ser pior que código duplicado, então tome cuidado! Com isto dito, se você pode fazer uma boa abstração, faça! Não repita você mesmo, ou então você se encontrará atualizando vários lugares toda vez que desejar alterar uma coisa.
Ruim:
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);});}
Bom:
classDeveloper{// ...getExtraDetails(){return{githubLink:this.githubLink,};}}classManager{// ...getExtraDetails(){return{portfolio:this.portfolio,};}}functionshowEmployeeList(employee:Developer|Manager){employee.forEach(employee=>{constexpectedSalary=developer.calculateExpectedSalary();constexperience=developer.getExperience();constextra=employee.getExtraDetails();constdata={ expectedSalary, experience, extra,};render(data);});}
Você deve ser duro quando o assunto for código duplicado. As vezes há uma troca entre código duplicado e complexidade aumentada quando introduz abstrações desnecessárias. Quando duas implementações de módulos diferentes se parecem bastante mas vivem em diferentes lugares, código duplicado pode ser aceitável e preferível à extrair para um código comum entre os lugares. Nesse caso, o código que seria extraido iria criar uma dependência indireta entre os dois módulos.
Ruim:
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;}constmenuConfig={title:null,body:'Bar',buttonText:null,cancellable:true,};createMenu(menuConfig);
Bom:
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'});
Alternativamente, você pode usar desestruturação com valores predefinidos:
typeMenuConfig={title?:string;body?:string;buttonText?:string;cancellable?:boolean;};functioncreateMenu({ title='Foo', body='Bar', buttonText='Baz', cancellable=true,}:MenuConfig){// ...}createMenu({body:'Bar'});
Para evitar efeitos colaterais e comportamentos indesejados ao passar explicitamenteundefined ounull, você pode dizer ao compilador TypeScript para não permitir isso. Veja mais em--strictNullChecks.
Flags indicam ao seu usuário que a função faz mais de uma coisa.Funções devem fazer apenas uma coisa. Divida sua função se ela segue diferentes caminhos baseados em uma condição.
Ruim:
functioncreateFile(name:string,temp:boolean){if(temp){fs.create(`./temp/${name}`);}else{fs.create(name);}}
Bom:
functioncreateFile(name:string){fs.create(name);}functioncreateTempFile(name:string){fs.create(`./temp/${name}`);}
Uma função produz efeitos colaterais se ela faz algo além de receber um valor e retornar outro valor ou valores.Efeitos colaterais poderia ser escrever em um arquivo, modificar alguma variável global, ou acidentamente transferir todo seu dinheiro para um estranho.
Agora, você precisa ter efeitos colaterais em algumas ocasiões. Como no exemplo anterior, onde você precisa escrever em um arquivo.O que você deseja fazer é centralizar onde você está fazendo isto, ao invés de ter várias funções e classes que escrevem em um só arquivo.Tenha um serviço que faça isso. Um, e apenas um.
O ponto principal é evitar alguns vacilos como compartilhar o estado entre dois objetos sem nenhuma estrutura, usando tipo de dados mutáveis que podem ser escritos por qualquer coisas, e não centralizar onde seus efeitos colaterais vão ocorrer. Se você pode fazer isso, você será mais feliz que a maioria dos outros programadores.
Ruim:
// Global variable referenced by following function.// If we had another function that used this name, now it'd be an array and it could break it.letname='Robert C. Martin';functiontoBase64(){name=btoa(name);}toBase64();// produces side effects to `name` variableconsole.log(name);// expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
Bom:
// Global variable referenced by following function.// If we had another function that used this name, now it'd be an array and it could break it.constname='Robert C. Martin';functiontoBase64(text:string):string{returnbtoa(text);}constencodedName=toBase64(name);console.log(name);
No JavaScript, primitivos são passados por valores e objetos/arrays são passados por referência. No caso dos objetos e arrays, se sua função faz uma mudança em um array de carrinho de loja, por exemplo, adicionando um item à compra, então todas outras funções que usam esse array serão afetadas por esta adição. Isso pode ser bom, mas pode ser ruim também. Vamos imaginar um cenário ruim:
O usuário clica em "Comprar", botão que chama uma função de compra, que envia uma requisição com o array de carrinho de compras ao servidor. Por conta de uma conexão ruim, a função de comprar precisa ficar tentando novamente a requisição. Agora, e se o usuário, neste meio tempo, clicar no botão "Adicionar ao carrinho", em um item que ele não quer, antes da requisição começar? Se isso acontecer e a requisição começar, a função de compra irá enviar o item adicionado acidentalmente, pois este tem a referência do mesmo array anterior e que a funçãoaddItemToCart modificou, ao adicionar um item novo.
Uma ótima solução seria a funçãoaddItemToCart sempre clonar o carrinho, editar, e retornar o clone. Isso assegura que nenhuma outra função que tem a referência do carrinho será afetada pelas mudanças.
Dois avisos ao mencionar essa abordagem:
Podem haver casos onde você quer modificar o objeto inputado, mas quando você adota esse prática você verá que esses casos são bem raros. A maiorias das coisas podem ser refatoradas para não terem efeitos colaterais! (vejafunções puras)
Clonar grandes objetos pode ser bem caro em termos de performance. Com sorte, isso não é um grande problema na prática pois há ótimas bibliotecas que permitem esse tipo de abordagem serem rápidas e não tão intensivas no consumo de memória, como seria em clonar os objetos manualmente.
Ruim:
functionaddItemToCart(cart:CartItem[],item:Item):void{cart.push({item,date:Date.now()});}
Bom:
functionaddItemToCart(cart:CartItem[],item:Item):CartItem[]{return[...cart,{item,date:Date.now()}];}
Poluir escopos globais é uma má prática em JavaScript pois você pode colidir com código de outra biblioteca, e o usuário da sua API não será esclarecido até ele receber uma erro em produção. Vamos pensar no exemplo a seguir: E se você quiser extender o código nativo do Array em JavaScript, para ter uma função diff que pode mostrar a diferença entre dois arrays? Você pode escrever sua nova função emArray.prototype, mas isso iria colidir com código de outra biblioteca que tenta fazer a mesma coisa. E se essa biblioteca usa o métododiff para achar a diferença entre o primeiro e o último elemento de um array? Por isso seria muito melhor usar classes e simplesmente extender oArray global.
Ruim:
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));};}
Bom:
classMyArray<T>extendsArray<T>{diff(other:T[]):T[]{consthash=newSet(other);returnthis.filter(elem=>!hash.has(elem));}}
Use esse tipo de paradigma quando puder.
Ruim:
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;}
Bom:
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,);
Ruim:
if(subscription.isTrial||account.balance>0){// ...}
Bom:
functioncanActivateService(subscription:Subscription,account:Account){returnsubscription.isTrial||account.balance>0;}if(canActivateService(subscription,account)){// ...}
Ruim:
functionisEmailNotUsed(email:string){// ...}if(isEmailNotUsed(email)){// ...}
Bom:
functionisEmailUsed(email){// ...}if(!isEmailUsed(node)){// ...}
Isso parece uma tarefa impossivel. Quando se escuta isso pela primeira vez, a maioria das pessoas dizem, "Como eu posso fazer qualquer coisa sem declarar umif?" A resposta é que você pode usar polimorfismo para alcançar o mesmo objetivo em muitos casos. A segunda pergunta normalmente é,"Bom isso é otimo, mas por que eu iria querer fazer isso?" A resposta é um conceito anterior de codigo limpo que aprendemos antes: Uma função deve fazer apenas uma coisa. Quando você tem classes e funções comif declarados, você está falando para seu usuario que sua função faz mais que uma coisa. Lembre-se apenas faça uma coisa.
Ruim:
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.');}}}
Bom:
classAirplane{// ...}classBoeing777extendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getPassengerCount();}}classAirForceOneextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude();}}classCessnaextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getFuelExpenditure();}}
TypeScript é um superconjunto sintático estrito de JavaScript e adciona verificação de tipo estático para a linguagem.Prefira sempre especificar tipos de variáveis, parâmetros e retornar valores para aproveitar todo o potencial dos recursos do TypeScript.Isso torna refatoração mais fácil.
Ruim:
functiontravelToTexas(vehicle:Bicycle|Car){if(vehicleinstanceofBicycle){vehicle.pedal(this.currentLocation,newLocation('texas'));}elseif(vehicleinstanceofCar){vehicle.drive(this.currentLocation,newLocation('texas'));}}
Bom:
typeVehicle=Bicycle|Car;functiontravelToTexas(vehicle:Vehicle){vehicle.move(this.currentLocation,newLocation('texas'));}
Navegadores modernos fazem muita otimização por baixo dos panos na hora da execução. Muitas vezes, se você está otimizando então, você está apenas perdendo seu tempo. Há bonsrecursos para ver aonde está faltando otimização. Procure esses, até que eles sejam corrigidos caso possam ser.
Ruim:
// 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++){// ...}
Bom:
for(leti=0;i<list.length;i++){// ...}
Código morto é tão ruim quanto código duplicado. Não há razão alguma para mantê-lo na sua base de códigos.Se não está sendo chamado, livre-se dele! Ainda continuará seguro no seu histórico de versões se você ainda precisar.
Ruim:
functionoldRequestModule(url:string){// ...}functionrequestModule(url:string){// ...}constreq=requestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');
Bom:
functionrequestModule(url:string){// ...}constreq=requestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');
Use generators e iterables quando trabalhar com coleções de dados que se comportam como uma corrente/fluxo.Há alguns bons motivos para isso:
- desacopla quem está chamando o generator, deixando a ele a opção de quantos itens deseja acessar
- 'lazy execution', os itens são chamados por demanda
- suporte nativo para iterar itens utilizando a sintaxe
for-of - 'iterables' permitem implementar um padrão mais otimizado de 'iterators'
Ruim:
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));}// Printa os 10 primeiros números de Fibonnaci.print(10);
Bom:
// Gera uma corrente infinita de números de Fibonacci.// O generator não mantém um array com todos os números.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);}}// Printa os 10 primeiros números de Fibonnaci.print(10);
Tem diversas bibliotecas que permitem trabalhar com iterables de uma forma similar aos arrays nativos, ao encadear métodos comomap,slice,forEach etc. Vejaitiriri para um exemplo avançado de manipulação utilizando iterables (ouitiriri-async para manipulação de iterables assíncronos).
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 suporta sintaxe de getter/setter.Usar getters e setters para acessar dados de objetos que encapsulam comportamento pode ser melhor do que simplesmente procurar por uma propriedade em um objeto."Por que?" você pode perguntar. Bom, aqui está uma lista de razões:
- Quando você quer fazer mais além de pegar uma propriedade de um objeto, você não precisa olhar e mudar todos os acessores na sua base de códigos.
- Faz adicionar validação simples quando está setando.
- Encapsula a representação interna;
- Fácil de adicionar logging e tratamento de erros quando usar get e set.
- Você pode carregar preguiçosamente as propriedades do seu objeto, vamos dizer pegar de um servido.
Ruim:
classBankAccount{balance:number=0;// ...}constvalue=100;constaccount=newBankAccount();if(value<0){thrownewError('Cannot set negative balance.');}account.balance=value;
Bom:
classBankAccount{privateaccountBalance:number=0;getbalance():number{returnthis.accountBalance;}setbalance(value:number){if(value<0){thrownewError('Cannot set negative balance.');}this.accountBalance=value;}// ...}constaccount=newBankAccount();account.balance=100;
TypeScript suporta acessoresPublico(Padrão),Protegido ePrivado em membros das classes.
Ruim:
classCircle{radius:number;constructor(radius:number){this.radius=radius;}perimeter(){return2*Math.PI*this.radius;}surface(){returnMath.PI*this.radius*this.radius;}}
Bom:
classCircle{constructor(privatereadonlyradius:number){}perimeter(){return2*Math.PI*this.radius;}surface(){returnMath.PI*this.radius*this.radius;}}
TypeScript's type system allows you to mark individual properties on an interface / class as readonly. This allows you to work in a functional way (unexpected mutation is bad).For more advanced scenarios there is a built-in typeReadonly that takes a typeT and marks all of its properties as readonly using mapped types (seemapped types).
O sistema de tipagem do TypeScript permite que você marque propriedades individuais em uma interface/classe como de apenas leitura. Isso permite que você trabalhe de uma maneira funcional (mutações inesperadas são ruins).Para cenários mais avançados há um tipo integradoReadonly que recebe um tipoT e marca todas suas propriedades como de apenas leitura, usando tipos mapeados (mapped types) (vejamapped types).
Ruim:
interfaceConfig{host:string;port:string;db:string;}
Bom:
interfaceConfig{readonlyhost:string;readonlyport:string;readonlydb:string;}
O tamanho de uma clase é mensurado por sua responsabilidade. Seguindo o princípio de única responsabilidade, uma classe deve ser pequena.
Ruim:
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{/* ... */}// ...}
Bom:
classDashboard{disable():void{/* ... */}enable():void{/* ... */}getVersion():string{/* ... */}}// divida as responsábilidades movendo os métodos restantes para outras classes// ...
Coesão define o grau de parentesco que um membro de uma classe tem com os outros. Idealmente, cada campo de uma classe deve ser usado por cada um dos métodos.Nós dizemos, então, que uma classe é super coesa. Na prática, nem sempre isso é possível, e nem recomendado em alguns casos. Você deve preferir, entretanto, classes com alta coesão.
Acoplamento se refere ao quanto duas classes são relacionadas ou dependentes umas das outras. Classes são pouco acopladas/desacopladas quando mudanças em uma não afeta a outra.
Um bom design de software temcoesão edesacoplamento
Ruim:
classUserManager{// Bad: each private variable is used by one or another group of methods.// It makes clear evidence that the class is holding more than a single responsibility.// If I need only to create the service to get the transactions for a user,// I'm still forced to pass and instance of 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>{// ...}}
Bom:
classUserService{constructor(privatereadonlydb:Database){}asyncgetUser(id:number):Promise<User>{returnawaitdb.users.findOne({id});}asyncgetTransactions(userId:number):Promise<Transaction[]>{returnawaitdb.transactions.find({userId});}}classUserNotifier{constructor(privatereadonlyemailSender:EmailSender){}asyncsendGreeting():Promise<void>{awaitemailSender.send('Welcome!');}asyncsendNotification(text:string):Promise<void>{awaitemailSender.send(text);}asyncsendNewsletter():Promise<void>{// ...}}
Como foi muito bem pontuado emDesign Patterns, pelaGang of Four, você deve preferir composição à herança quando puder. Há ótimos motivos para usar herança, e ótimos motivos para usar composição. O ponto aqui é, se sua mente instintivamente pensa em heranças, tente pensar em como composições poderiam resolver seu problema de forma melhor. Em muitos casos isso é possível.
Você pode estar pensando, "quando devo usar herança?" E isso depende do seu problema, mas aqui vai uma lista de quando usar herança faz mais sentido de que composição:
Sua herança representa uma relação de "é um..." e não "tem um..." (Humano->Animal vs Usuário->Detalhes).
Você pode reutilizar código das classes base (Humanos podem se mover como animais).
Você deseja fazer mudanças globais ao alterar a classe base. (Alterar o gasto de calorias de todos os animais quando se movimentam).
Ruim:
classEmployee{constructor(privatereadonlyname:string,privatereadonlyemail:string){}// ...}// Ruim pois Employees "tem" taxas. EmployeeTaxData não é um tipo de EmployeeclassEmployeeTaxDataextendsEmployee{constructor(name:string,email:string,privatereadonlyssn:string,privatereadonlysalary:number,){super(name,email);}// ...}
Bom:
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){}// ...}
Esse padrão é bem útil e usado normalmente em muitas bibliotecas. Seu uso permite que seu código seja mais expressivo, e menos verboso. Por esse motivo, use cadeia de métodos e olhe depois como seu código irá estar mais limpo.
Ruim:
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{// ...}}// ...constquery=newQueryBuilder();query.from('users');query.page(1,100);query.orderBy('firstName','lastName');constquery=queryBuilder.build();
Bom:
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();
Como dito em Código Limpo, "Não deve haver mais de um motivo para alterar uma classe". É tentador lotar uma classe com várias funcionalidades, como se você só pudesse carregar uma mala em uma viagem. O problema disso é que sua classe não será conceitualmente coesiva e posteriormente te trará vários motivos para mudar. Reduzir a quantidade de vezes que você precisa mudar uma classe é importante. É importante pois se muitas funcionalidades estão contidas em uma classe e você altera um pedaço disso, pode ser difícil entender como isso irá afetar os módulos dependentes da sua classe/do seu código.
Ruim:
classUserSettings{constructor(privatereadonlyuser:User){}changeSettings(settings:UserSettings){if(this.verifyCredentials()){// ...}}verifyCredentials(){// ...}}
Bom:
classUserAuth{constructor(privatereadonlyuser:User){}verifyCredentials(){// ...}}classUserSettings{privatereadonlyauth:UserAuth;constructor(privatereadonlyuser:User){this.auth=newUserAuth(user);}changeSettings(settings:UserSettings){if(this.auth.verifyCredentials()){// ...}}}
Como dito por Bertrand Meyer, "entidades em software (classes, módulos, funções, etc.) devem ser abertas para extenções, mas fechadas para modificações." Mas o que isso significa? Este princípio diz, basicamente, que você deve permitir que seus usuários adicionem novas funcionalidades sem alterar código já existente.
Ruim:
classAjaxAdapterextendsAdapter{constructor(){super();}// ...}classNodeAdapterextendsAdapter{constructor(){super();}// ...}classHttpRequester{constructor(privatereadonlyadapter:Adapter){}asyncfetch<T>(url:string):Promise<T>{if(this.adapterinstanceofAjaxAdapter){constresponse=awaitmakeAjaxCall<T>(url);// transforma a resposta e retorna}elseif(this.adapterinstanceofNodeAdapter){constresponse=awaitmakeHttpCall<T>(url);// transforma a resposta e retorna}}}functionmakeAjaxCall<T>(url:string):Promise<T>{// faz a requisição e retorna uma Promise}functionmakeHttpCall<T>(url:string):Promise<T>{// faz a requisição e retorna uma Promise}
Bom:
abstractclassAdapter{abstractasyncrequest<T>(url:string):Promise<T>;}classAjaxAdapterextendsAdapter{constructor(){super();}asyncrequest<T>(url:string):Promise<T>{// faz requisição e retorna uma Promise}// ...}classNodeAdapterextendsAdapter{constructor(){super();}asyncrequest<T>(url:string):Promise<T>{// faz requisição e retorna uma Promise}// ...}classHttpRequester{constructor(privatereadonlyadapter:Adapter){}asyncfetch<T>(url:string):Promise<T>{constresponse=awaitthis.adapter.request<T>(url);// transforma a resposta e retorna}}
Este é um termo muito assustador para um conceito bem simples. É formalmente definido como "Se S é um subtipo de T, então os objetos do tipo T podem ser substituidos com objetos do tipo S(ou seja, objetos do tipo S podem substituir objetos do tipo T) sem alterar nenhuma propriedade desejáveis daquele programa (correção, tarefa executada, etc.)." E essa é uma definição ainda mais assustadora.
A melhor explicação para isso é se você tem uma classe pi e uma classe filho, então a classe pai e a classe filho podem ser usada sem ocorrer resultados incorretos. Isso pode ainda estar sendo confuso, então vamos dar uma olhada no exemplo clássico Quadrado-Retângulo. Matemáticamente, o quadrado é um retângulo, mas se você modelar o quadrado usando o relacionamento "é-um" via herança, você terá problemas.
Ruim:
classRectangle{constructor(protectedwidth:number=0,protectedheight:number=0){}setColor(color:string){// ...}render(area:number){// ...}setWidth(width:number){this.width=width;}setHeight(height:number){this.height=height;}getArea():number{returnthis.width*this.height;}}classSquareextendsRectangle{setWidth(width:number){this.width=width;this.height=width;}setHeight(height:number){this.width=height;this.height=height;}}functionrenderLargeRectangles(rectangles:Rectangle[]){rectangles.forEach(rectangle=>{rectangle.setWidth(4);rectangle.setHeight(5);constarea=rectangle.getArea();// BAD: Returns 25 for Square. Should be 20.rectangle.render(area);});}constrectangles=[newRectangle(),newRectangle(),newSquare()];renderLargeRectangles(rectangles);
Bom:
abstractclassShape{setColor(color:string){// ...}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);
PSI afima que "Clientes não deveriam ser forçados a serem dependentes de interfaces que eles não usam". Esse princípio é muito relacionado ao Princípio da única responsabilidade.O que isso realmente significa é que você deve sempre projetar suas abstrações de uma maneira que os clientes que estão usando os métodos expostos não obtenham a "a torta inteira". Isto também inclui aos clientes o dever implementar metódos que eles, na realidade, não precisam.
Ruim:
interfaceISmartPrinter{print();fax();scan();}classAllInOnePrinterimplementsISmartPrinter{print(){// ...}fax(){// ...}scan(){// ...}}classEconomicPrinterimplementsISmartPrinter{print(){// ...}fax(){thrownewError('Fax not supported.');}scan(){thrownewError('Scan not supported.');}}
Bom:
interfaceIPrinter{print();}interfaceIFax{fax();}interfaceIScanner{scan();}classAllInOnePrinterimplementsIPrinter,IFax,IScanner{print(){// ...}fax(){// ...}scan(){// ...}}classEconomicPrinterimplementsIPrinter{print(){// ...}}
Esse princípio afirma duas coisas essenciais:
Modulos de alto nível não deveriam ser dependentes de módulos de baixo nível. Ambos devem depender de abstrações.
Abstrações não deveriam ser dependentes de detalhes. Detalhes devem depender de abstrações.
Isso pode ser dificil de entender de primeira, porém se você já trabalhou com Angular, você já viu a implementação desse principio na forma de Injeção de Dependência (Dependency Injection). Entretanto, não são conceitos idênticos, DIP mantém modulos de alto nível conhecendo os detalhes dos módulos de baixo nível e os configura. Isso pode ser feito através da Injeção de Depedencia. Um grande benficio disso é que o acoplamento entre módulos é reduzido. Acoplamento é um padrão muito ruim de desenvolvimento porque faz o seu código ser dificil de refatorar.
DIP é normalmente alcançado através do uso de um container de controle de inversão (IoC). Um exemplo de um container IoC poderoso para o TypeScript é oInversifyJs
Ruim:
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');
Bom:
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 report:constreader=newReportReader(newJsonFormatter());awaitreport=awaitreader.read('report.json');
Testar é mais importante do que lançar o software. Se você não tem nenhum teste, ou poucos testes, todas as vezes que você lançar um software você não vai ter certeza de que não quebrou nada.Decidir o que seria uma boa quantidade de testes é de responsabilidade do seu time, mas ter uma cobertura ampla das funcionalidades do seu código é como você atinge confiança e certa paz de espirito. Isso implica que, além de uma boa ferramente para fazer seus testes, precisa de algo para promover essa total cobertura.
Não há desculpas para não escrever testes. Há uma vasta leva de frameworks para testar JavaScript com suporte a tipos com TypeScript, então ache um que seu time prefira. Quando achar um que funcione pro seu time, escreva testes para toda funcionalidade ou módulo que você introduzir. Se seu método preferido é o TDD (Test Driven Development - Desenvolvimento guiado à testes), isso é ótimo, mas o ponto é você ter certeza do código que está lançando, antes de lança-lo, ou refatorar códigos antigos.
Você não pode escrever nenhum código de produção a não ser que seja para fazer um teste unitário quebrado, passar.
Você não pode escrever mais de um teste que falhe; e erros de compilação são erros.
Você não pode esrever mais de um código de produção do que o suficiente para passar o teste unitário que falhou.
Testes limpos devem seguir essas regras:
Fast - Rápido testes devem ser rápidos pois queremos roda-los frequentemente.
Independent - Independente testes não devem depender um dos outros. Eles devem retornar algo, seja ele rodado sozinho ou com outros testes.
Repeatable - Repetitivos testes devem ser repetitivos em qualquer ambiente e não devem haver motivos para eles falharem.
Self-Validating - Auto-validado um teste devem responder comPassed ouFailed. Você não tem que comparar com arquivos de log para saber se passaram ou não.
Timely - Pontuais testes unitários devem ser escritos antes do código de produção. se você escrever depois, pode parar muito complicado escrever testes.
Testes devem seguir também o principio da única responsabilidade. Faça apenas uma asserção por teste unitário.
Ruim:
import{assert}from'chai';describe('AwesomeDate',()=>{it('handles date boundaries',()=>{letdate:AwesomeDate;date=newAwesomeDate('1/1/2015');date.addDays(30);assert.equal('1/31/2015',date);date=newAwesomeDate('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);date=newAwesomeDate('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});
Bom:
import{assert}from'chai';describe('AwesomeDate',()=>{it('handles 30-day months',()=>{constdate=newAwesomeDate('1/1/2015');date.addDays(30);assert.equal('1/31/2015',date);});it('handles leap year',()=>{constdate=newAwesomeDate('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);});it('handles non-leap year',()=>{constdate=newAwesomeDate('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});
Quando um teste falha, o seu nome é a primeira indicação do que deu errado.
Ruim:
describe('Calendar',()=>{it('2/29/2020',()=>{// ...});it('throws',()=>{// ...});});
Bom:
describe('Calendar',()=>{it('should handle leap year',()=>{// ...});it('should throw when format is invalid',()=>{// ...});});
Callbacks não são claros, e eles causam uma quantidade desnecessária de agrupamento(callback hell).Há utilitários que transformam funções existentes que usam callbacks para uma versão que retorna promises(Em Node.js háutil.promisify, para propositos gerais, vejapify,es6-promisify)
Ruim:
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);}},);
Bom:
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));
Promises suportam alguns padrões que podem ser úteis em alguns casos:
| Padrão | Descrição |
|---|---|
Promise.resolve(value) | Converte um valor para uma promise resolvida. |
Promise.reject(error) | Converte um erro para uma promise rejeitada. |
Promise.all(promises) | Retorna uma nova promise, que é preenchida com um array of fulfillment values for the passed promises or rejects with the reason of the first promise that rejects. |
Promise.race(promises) | Returns a new promise which is fulfilled/rejected with the result/error of the first settled promise from the array of passed promises. |
Promise.all is especially useful when there is a need to run tasks in parallel.Promise.race makes it easier to implement things like timeouts for promises.
Com a sintaxe Async/Await você pode escrever um código muito mais claro e compreensível do que com promises. Dentro de uma função prefixada comasync você tem uma maneira de dizer ao tempo de execução do JavaScript pausar a execução do código quando utilizado o prefixoawait (quando usando em um promise).
Ruim:
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));
Bom:
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;}// somewhere in an async function (Alguma parte de uma função async)try{constcontent=awaitdownloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin','article.html',);console.log(content);}catch(error){console.error(error);}
Erros lançados são uma coisa boa! Eles significam que o tempo de execução identificou com sucesso quando alguma coisa no seu programa deu errado e está deixando você saber parando a execução da função atual, matando o processo (em Node), e notificando você no console.
JavaScript assim como o TypeScript te permite jogar (throw) qualquer objeto. Um promise também ser rejeitado com qualquer objeto de razão.É aconselhavel usar a sintaxe jogar(throw) com um tipo Erro (Error). Isto é porque seu erro pode ser pego em um código de alto nível com uma sintaxe pegar (catch).Seria muito confuso pegar uma mensagem string la e fariadebugar mais doloroso.Pela mesma razão você deveria rejeitar promises com tipo de erro (Error).
Ruim:
functioncalculateTotal(items:Item[]):number{throw'Not implemented.';}functionget():Promise<Item[]>{returnPromise.reject('Not implemented.');}
Bom:
functioncalculateTotal(items:Item[]):number{thrownewError('Not implemented.');}functionget():Promise<Item[]>{returnPromise.reject(newError('Not implemented.'));}// Ou equivalente a:asyncfunctionget():Promise<Item[]>{thrownewError('Not implemented.');}
O benefício de user o tipoError é que este é suportado portry/catch/finally e implicitamente todos os erros tem a propriedadestack, que é uma ferramenta poderosa para debug.Há também outras alternativas, não usarthrow e, ao invés disso, sempre retornar objetos de erro.TypeScript deixa isso ainda mais fácil. Considere o exemplo abaixo:
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};}
Para entender mais disso, leia apublicação original.
Não fazer nada com um erro capturado não te da a habilidade para consertar ou reagir ao erro. Mostrar o erro no console (console.log) não é muito bom, já que fácilmente pode ser perdido no meio de tanta coisa mostrada no console. Se você coloca todo pedaço de código em umtry/catch, significa que você acha que um erro pode acontecer, então você deve ter um plano, ou criar uma saída, pra quando acontecer.
Ruim:
try{functionThatMightThrow();}catch(error){console.log(error);}// ou ainda piortry{functionThatMightThrow();}catch(error){// ignorar o erro}
Bom:
import{logger}from'./logging';try{functionThatMightThrow();}catch(error){logger.log(error);}
Pelo mesmo motivo que você não deve ignorar erros que vem dotry/catch.
Ruim:
getUser().then((user:User)=>{returnsendEmail(user.email,'Welcome!');}).catch(error=>{console.log(error);});
Bom:
import{logger}from'./logging';getUser().then((user:User)=>{returnsendEmail(user.email,'Welcome!');}).catch(error=>{logger.log(error);});// ou usando async/await:try{constuser=awaitgetUser();awaitsendEmail(user.email,'Welcome!');}catch(error){logger.log(error);}
Formatação é subjetivo. Como todas outras regras aqui, não há uma mais rápida ou mais difícil que você deva seguir. O ponto principal éNÃO ARGUMENTE formatação do código. Há várias ferramentas que automatizam isso. Escolha uma! É uma perda de tempo e dinheiro para engenheiros (de software) argumentar em cima de formatação de código. A regra geral éseguir e manter consistente as regras de formatação.
Em TypeScript, tem uma ótima ferramenta chamadaTSLint. É uma ferramenta de análise estática que pode te ajudar a melhorar drásticamente a legibilidade e manutenibilidade do seu código. Há algumas configurações prontas para serem utilizadas com TSLint.
TSLint Config Standard - Configurações padrões
TSLint Config Airbnb - Configurações utilizadas no Airbnb
TSLint Clean Code - Regras inspiradas no livroClean Code: A Handbook of Agile Software Craftsmanship
TSLint react - Regras para React e JSX
TSLint + Prettier - Regras para serem usadas com oPrettier.
ESLint rules for TSLint - Regras do ESLint p/ TypeScript
Immutable - Regras para desabilitar mutações no TypeScript
Vale mencionar também este ótimo artigoTypeScript StyleGuide and Coding Conventions.
Capitalização diz muito sobre suas variáveis, funções, etc. Essas regras são subjetivas, seu time pode escolher o que eles quiserem. Indepentente da forma que escolherem,sejam consistentes.
Ruim:
constDAYS_IN_WEEK=7;constdaysInMonth=30;constsongs=['Back In Black','Stairway to Heaven','Hey Jude'];constArtists=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestore_database(){}classanimal{}classContainer{}
Bom:
constDAYS_IN_WEEK=7;constDAYS_IN_MONTH=30;constSONGS=['Back In Black','Stairway to Heaven','Hey Jude'];constARTISTS=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestoreDatabase(){}classAnimal{}classContainer{}
UsePascalCase para classes, interfaces, tipos e namespaces.UsecamelCase para variáveis, funções e classes.
Se uma função chama outra, mantenha essas funções verticalmente perto no seu arquivo. Idealmente, mantenha a função que chama a outra, logo acima da função chamada.
Tendemos a ler código de cima para baixo, como um jornal. Por conta disso, faça seu código ser fácil de ler dessa maneira.
Ruim:
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();
Bom:
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();
Usetype quando você precisar de uma união ou interseção. Use interface quando precisar usarextends ouimplements. Não há uma regra a ser seguida, entretanto. Use aquela que funciona para você.
Dê uma olhada nestaexplicação sobre as diferenças entretype einterface.
Ruim:
interfaceEmailConfig{// ...}interfaceDbConfig{// ...}interfaceConfig{// ...}//...typeShape{// ...}
Bom:
typeEmailConfig{// ...}typeDbConfig{// ...}typeConfig=EmailConfig|DbConfig;// ...interfaceShape{}classCircleimplementsShape{// ...}classSquareimplementsShape{// ...}
Com declarações de import fáceis e limpas de ler, você consegue rapidamente ver as dependências do seu código. Assegure-se de que está aplicando as boas práticas para fazer imports:
- Imports devem ser em ordem alfabética e agrupados.
- Imports não utilizadas tem de ser removidos.
- Imports com nomes devem estar em ordem alfabética (ex.:
import {A, B, C} from 'foo';). - As fontes do seu imports devem estar em ordem alfabética dividido em grupos, ex.:
import * as foo from 'a'; import * as bar from 'b'; - Grupos de imports são separados por espaços em branco.
- Grupos devem respeitar a seguinte ordem:
- polyfills (ex.:
import 'reflect-metadata';) - módulos do node (ex.:
import fs from 'fs';) - módulos externos (ex.:
import { query } from 'itiriri';) - módulos internos (ex.:
import { UserService } from 'src/services/userService';) - módulos de um diretório pai (ex.:
import foo from '../foo'; import qux from '../../foo/qux';) - módulos de um mesmo diretório (ex.:
import bar from './bar'; import baz from './bar/baz';)
- polyfills (ex.:
Ruim:
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';
Bom:
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';
Faça imports mais agradáveis definindo caminhos e a propriedade baseUrl na seção compilerOptions emtsconfig.json
Isso irá evitar caminhos relativos longos quando fizer imports.
Ruim:
import{UserService}from'../../../services/UserService';
Bom:
import{UserService}from'@services/UserService';
// tsconfig.json..."compilerOptions":{ ..."baseUrl":"src","paths":{"@services":["services/*"]}...}...
O uso de comentários é uma indicação que você falhou ao se expressar sem eles. Seu código deve ser sua única fonte.
Don’t comment bad code—rewrite it. (Não comente código ruim - Reescreva-o.)—Brian W. Kernighan and P. J. Plaugher
Comentários são desculpas, não um requisito. Bom código normalmente se auto-documentam.
Ruim:
// Checa se subscriptions estão ativas.if(subscription.endDate>Date.now){}
Bom:
constisSubscriptionActive=subscription.endDate>Date.now;if(isSubscriptionActive){/* ... */}
Versionamento de código existe por um motivo. Deixe código antigo no seu histórico.
Ruim:
classUser{name:string;email:string;// age: number;// jobPosition: string;}
Bom:
classUser{name:string;email:string;}
Lembre-se, versione seu código! Não há motivo para manter código morto, comentado, e especialmente, datado. Usegit log para ter o histórico.
Ruim:
/** * 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;}
Bom:
functioncombine(a:number,b:number):number{returna+b;}
Normalmente eles só sujam o código. Deixe que os nomes das funções e varáveis, juntamente com uma identação e formatação apropriada, dê uma boa estrutura visual no seu código.Opcionalmente você pode usar o suporte da sua IDE paracode folding (VSCodefolding regions)
Ruim:
////////////////////////////////////////////////////////////////////////////////// Client class////////////////////////////////////////////////////////////////////////////////classClient{id:number;name:string;address:Address;contact:Contact;////////////////////////////////////////////////////////////////////////////////// public methods////////////////////////////////////////////////////////////////////////////////publicdescribe():string{// ...}////////////////////////////////////////////////////////////////////////////////// private methods////////////////////////////////////////////////////////////////////////////////privatedescribeAddress():string{// ...}privatedescribeContact():string{// ...}}
Bom:
classClient{id:number;name:string;address:Address;contact:Contact;publicdescribe():string{// ...}privatedescribeAddress():string{// ...}privatedescribeContact():string{// ...}}
Quando você reparar que precisa deixar notas no código, para alterar coisas futuramente, faça isso utilizando comentários// TODO. A maioria das IDEs tem um suporte especial para esse tipo de comentário para que possa voltar nesses comentários mais fácilmente depois.
Tenha em mente queTODOs não são uma desculpa para código ruim.
Ruim:
functiongetActiveSubscriptions():Promise<Subscription[]>{// garantir que `dueDate` está indexadoreturndb.subscriptions.find({dueDate:{$lte:newDate()}});}
Bom:
functiongetActiveSubscriptions():Promise<Subscription[]>{// TODO: garantir que `dueDate` está indexado.returndb.subscriptions.find({dueDate:{$lte:newDate()}});}
Um grande obrigado ao meu amigoLuís Gustavo que me deu uma força ao traduzir esse conteúdo maravilhoso!
About
Conceitos do livro "Clean Code" adaptados para TypeScript
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Languages
- TypeScript100.0%
