Concepts de "Clean Code (ou code propre)" adaptés à TypeScript. Inspirés par clean-code-javascript. Ce document est une traduction de ce repository rédigé par labs42io.
- Introduction
- Variables
- Fonctions
- Objets et Structures de Données
- Classes
- SOLID
- Tests
- Opérations Concurrentes
- Gestion des erreurs
- Formatage du Code
- Commentaires
- Traductions
Principes de l'ingénierie de logiciels, extraits du livre de Robert C. Martin Clean Code, adaptés à TypeScript. Ce n'est pas un guide de style. C'est un guide pour créer des logiciels lisibles, réutilisables, et faciles à réorganiser en utilisant TypeScript.
Il n'est pas nécessaire de suivre strictement tous les principes mentionés ici, et sans oublier que peu d'entre eux seront universellement acceptés. Ce ne sont que des lignes de recommendations et rien de plus, mais ce sont celles codifiées au cours de nombreuses années d'expérience collective par les auteurs de Clean Code.
Notre métier en tant qu'ingénieur de logiciels a un peu plus de 50 ans et on a encore beaucoup à apprendre. Lorsque l'architecture de logiciels est aussi ancienne que l'architecture elle-même, on aura peut-être alors des règles plus difficiles à suivre. Pour l'instant, laissez ce guide vous servir de pierre de touche pour évaluer la qualité du code TypeScript que vous et votre équipe produisez.
Une dernière chose: les connaître ne fera pas immédiatement de vous un meilleur développeur de logiciels, et travailler avec eux pendant de nombreuses années ne signifie pas que vous ne commettrez pas d’erreurs. Chaque portion de code commence comme un premier échantillon, comme de l'argile humide qui prend sa forme à la fin. Enfin, on chasse ces imperfections quand on les revise ensemble avec notre équipe. Ne vous tracassez pas à tout faire durant vos premiers éssais. Cherchez plutôt à améliorer le code mis en place!
Distinguer les noms de façon que le lecteur sache ce que les différences offrent.
Mal:
functionbetween<T>(a1: T,a2: T,a3: T): boolean{returna2<=a1&&a1<=a3;}Bien:
functionbetween<T>(value: T,left: T,right: T): boolean{returnleft<=value&&value<=right;}Si vous ne pouvez pas le prononcer, vous ne pouvez pas en discuter sans ressembler à un idiot.
Mal:
typeDtaRcrd102={genymdhms: Date;modymdhms: Date;pszqint: number;}Bien:
typeCustomer={generationTimestamp: Date;modificationTimestamp: Date;recordId: number;}Mal:
functiongetUserInfo(): User;functiongetUserDetails(): User;functiongetUserData(): User;Bien:
functiongetUser(): User;Nous lirons plus de code que nous n'en aurons jamais écrit. Il est important que le code que nous écrivons soit lisible et consultable. En ne nommant pas les variables qui finissent par être significatives pour comprendre notre programme, nous blessons nos lecteurs. Rendez vos noms consultables. Des outils comme TSLint peuvent aider à identifier les constantes sans nom.
Mal:
// What the heck is 86400000 for?setTimeout(restart,86400000);Bien:
// Declare them as capitalized named constants.constMILLISECONDS_IN_A_DAY=24*60*60*1000;setTimeout(restart,MILLISECONDS_IN_A_DAY);Mal:
declareconstusers: Map<string,User>;for(constkeyValueofusers){// iterate through users map}Bien:
declareconstusers: Map<string,User>;for(const[id,user]ofusers){// iterate through users map}Mieux vaut être explicite qu'implicite. La clarté est essentielle.
Mal:
constu=getUser();consts=getSubscription();constt=charge(u,s);Bien:
constuser=getUser();constsubscription=getSubscription();consttransaction=charge(user,subscription);Si votre nom de classe/type/objet vous dit quelque chose, ne répétez pas cela dans le nom de votre variable.
Mal:
typeCar={carMake: string;carModel: string;carColor: string;}functionprint(car: Car): void{console.log(`${car.carMake}${car.carModel} (${car.carColor})`);}Bien:
typeCar={make: string;model: string;color: string;}functionprint(car: Car): void{console.log(`${car.make}${car.model} (${car.color})`);}Les arguments par défaut sont souvent plus propres que les raccourcis.
Mal:
functionloadPages(count?: number){constloadCount=count!==undefined ? count : 10;// ...}Bien:
functionloadPages(count: number=10){// ...}Les énumérations peuvent vous aider à documenter l'intention du code. Par exemple, lorsque on veut que les valeurs soient différentes plutôt que la valeur exacte de celles-ci.
Mal:
constGENRE={ROMANTIC: 'romantic',DRAMA: 'drama',COMEDY: 'comedy',DOCUMENTARY: 'documentary',}projector.configureFilm(GENRE.COMEDY);classProjector{// declaration of ProjectorconfigureFilm(genre){switch(genre){caseGENRE.ROMANTIC:
// some logic to be executed}}}Bien:
enumGENRE{ROMANTIC,DRAMA,COMEDY,DOCUMENTARY,}projector.configureFilm(GENRE.COMEDY);classProjector{// declaration of ProjectorconfigureFilm(genre){switch(genre){caseGENRE.ROMANTIC:
// some logic to be executed}}}Limiter le nombre de paramètres d'une fonction est extrêmement important car cela facilite le test de votre fonction. En avoir plus de trois conduit à une explosion combinatoire où chaque argument doit être testé séparément dans des tonnes de cas différents.
Avoir un ou deux paramètres par fonction est idéal, et trois devraient être évités si possible. Rien de plus que cela devrait être consolidé. Assez souvent, si vous avez plus de deux arguments, votre fonction essaie d'en faire trop à la fois. Dans les cas où ce n'est pas le cas, la plupart du temps, un objet de niveau supérieur suffit comme argument.
Pensez à utiliser des objets littéraux si vous avez besoin de beaucoup plus de paramètres.
Pour rendre évidentes les attributs que la fonction attend, vous pouvez utiliser la syntaxe de déstructuration. Cela présente quelques avantages :
Quand quelqu'un regarde la signature d’une fonction, il est immédiatement clair lesquels des attributs sont en train d’être utilisées.
Il peut être utilisé pour simuler des paramètres avec des noms.
La déstructuration clone également les valeurs primitives spécifiées de l'objet passé comme paramètre dans la fonction. Cela peut aider à prévenir les effets secondaires. Remarque: les objets et les tableaux qui sont déstructurés à partir de l'objet argument ne sont PAS clonés.
TypeScript vous avertit des attributs non-utilisés, qui seraient impossibles sans déstructuration.
Mal:
functioncreateMenu(title: string,body: string,buttonText: string,cancellable: boolean){// ...}createMenu('Foo','Bar','Baz',true);Bien:
functioncreateMenu(options: {title: string,body: string,buttonText: string,cancellable: boolean}){// ...}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true});Vous pouvez encore améliorer la lisibilité en utilisant type aliases (ou type d'alias):
typeMenuOptions={title: string,body: string,buttonText: string,cancellable: boolean};functioncreateMenu(options: MenuOptions){// ...}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true});C'est de loin la règle la plus importante en ingénierie de logiciels. Lorsque les fonctions font plus d'une chose, elles sont plus difficiles à composer, à tester et à raisonner. Lorsque vous arrivez à isoler une fonction afin d’exécuter une seule tâche, elle peut être facilement refactorisée et votre code sera beaucoup plus net. Si vous ne prenez en compte ce qui est dit dans ce guide, vous serez en avance sur de nombreux développeurs.
Mal:
functionemailClients(clients: Client[]){clients.forEach((client)=>{constclientRecord=database.lookup(client);if(clientRecord.isActive()){email(client);}});}Bien:
functionemailClients(clients: Client[]){clients.filter(isActiveClient).forEach(email);}functionisActiveClient(client: Client){constclientRecord=database.lookup(client);returnclientRecord.isActive();}Mal:
functionaddToDate(date: Date,month: number): Date{// ...}constdate=newDate();// It's hard to tell from the function name what is addedaddToDate(date,1);Bien:
functionaddMonthToDate(date: Date,month: number): Date{// ...}constdate=newDate();addMonthToDate(date,1);Quand vous avez plus d'un niveau d'abstraction, votre fonction en fait généralement trop. La division des fonctions conduit à une réutilisabilité et à des tests plus faciles.
Mal:
functionparseCode(code: string){constREGEXES=[/* ... */];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((regex)=>{statements.forEach((statement)=>{// ...});});constast=[];tokens.forEach((token)=>{// lex...});ast.forEach((node)=>{// parse...});}Bien:
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;}Faites de votre mieux pour éviter la duplication de certaines parties du votre code. La duplication du code est mauvaise car cela signifie qu'il y a plus d'un endroit pour modifier quelque chose si vous devez changer une logique.
Imaginez que vous dirigiez un restaurant et que vous gardiez une trace de votre inventaire: toutes vos tomates, oignons, ail, épices, etc. Si vous avez plusieurs listes sur lesquelles vous gardez cela, alors toutes doivent être mises à jour lorsque vous servez un plat avec des tomates. Si vous n'avez qu'une seule liste, il n'y a qu'un seul endroit à mettre à jour!
Souvent, vous avez du code en double parce que vous avez deux ou plusieurs choses légèrement différentes, qui partagent beaucoup en commun, mais leurs différences vous obligent à avoir deux ou plusieurs fonctions distinctes qui font à peu près les mêmes choses. Supprimer le code en double signifie créer une abstraction qui peut gérer cet ensemble de choses différentes avec une seule fonction/module/classe.
Avoir la correcte abstraction est essentiel, c'est pourquoi vous devez suivre les principes SOLID. Les mauvaises abstractions peuvent être pires que le code en double, alors faites attention! Cela dit, si vous pouvez faire une bonne abstraction, faites-le! Ne vous répétez pas, sinon vous vous retrouverez à actualiser plusieurs endroits chaque fois que vous voulez changer une chose.
Mal:
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);});}Bien:
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);});}Vous devez être critique sur la duplication de code. Parfois, il y a un compromis entre le code dupliqué et une complexité accrue en introduisant une abstraction inutile. Lorsque deux implémentations de deux modules différents se ressemblent mais vivent dans des domaines différents, la duplication peut être acceptable et préférable à l'extraction du code commun. Le code commun extrait dans ce cas introduit une dépendance indirecte entre les deux modules.
Mal:
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'});Bien:
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'});Également, vous pouvez utiliser la déstructuration avec des valeurs par défaut:
typeMenuConfig={title?: string,body?: string,buttonText?: string,cancellable?: boolean};functioncreateMenu({ title ='Foo', body ='Bar', buttonText ='Baz', cancellable =true}: MenuConfig){// ...}createMenu({body: 'Bar'});Pour éviter tout effet secondaire et tout comportement inattendu en transmettant
explicitement la valeur undefined ou null, vous pouvez dire au compilateur de
TypeScript de ne pas l'autoriser.
Consultez --strictNullChecks l'option dans TypeScript.
Les indicateurs indiquent à votre utilisateur que cette fonction fait plus d'une chose. Les fonctions devraient faire une chose. Divisez vos fonctions si elles suivent des chemins de code différents basés sur un booléen.
Mal:
functioncreateFile(name: string,temp: boolean){if(temp){fs.create(`./temp/${name}`);}else{fs.create(name);}}Bien:
functioncreateTempFile(name: string){createFile(`./temp/${name}`);}functioncreateFile(name: string){fs.create(name);}Une fonction produit un effet secondaire si elle fait autre chose que de prendre une valeur et de renvoyer une ou plusieurs autres valeurs. Un effet secondaire pourrait être d'écrire dans un fichier, de modifier une variable globale ou de transférer accidentellement tout votre argent à un étranger.
Maintenant, vous devez avoir des effets secondaires dans un programme à l'occasion. Comme dans l'exemple précédent, vous devrez peut-être écrire dans un fichier. Ce que vous voulez faire, c'est de centraliser où vous faites cela. Ne pas avoir plusieurs fonctions et classes qui écrivent dans un fichier particulier. Avoir un service qui le fait. Seul et l'unique.
Le point principal est d'éviter les pièges courants comme le partage d'état entre des objets sans aucune structure, l'utilisation de types de données mutables qui peuvent être écrits par n'importe quoi, et ne pas centraliser où se produisent vos effets secondaires. Si vous pouvez le faire, vous serez plus heureux que la grande majorité des autres programmeurs.
Mal:
// 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=='Bien:
constname='Robert C. Martin';functiontoBase64(text: string): string{returnbtoa(text);}constencodedName=toBase64(name);console.log(name);En JavaScript, les primitives sont passées par valeur et les objets et tableaux
sont passés par référence. Dans le cas d'objets et de tableaux, si votre fonction
modifie un tableau de panier d'achat, par exemple, en ajoutant un article à acheter,
alors toute autre fonction qui utilise ce tableau cart sera affectée par cet
ajout. C'est peut-être bien, mais ça peut aussi être mauvais. Imaginons une mauvaise situation:
L'utilisateur clique sur le bouton “Achat”, qui appelle une fonction purchase
qui génère une demande réseau et envoie le tableau cart au serveur. En raison
d'une mauvaise connexion réseau, la fonction d'achat doit continuer à réessayer
la demande. Maintenant, que se passe-t-il si, dans l'intervalle, l'utilisateur
clique accidentellement sur le bouton addItemToCart sur un article qu'il ne
souhaite pas avant le début de la demande réseau? Si cela se produit et que la
demande de réseau commence, alors cette fonction d'achat enverra l'article ajouté
accidentellement car il a une référence à un tableau de panier d'achat que la
fonction addItemToCart a modifié en ajoutant un article indésirable.
Une excellente solution serait que addItemToCart clone toujours lecart, le
modifie et renvoie le clone. Cela garantit qu'aucune autre fonction conservant
une référence du panier ne sera affectée par des modifications.
Deux avertissements à mentionner à cette approche:
Il peut y avoir des cas où vous souhaitez réellement modifier l'objet passée comme paramètre, mais lorsque vous adoptez cette pratique de programmation, vous constaterez que ces cas sont assez rares. La plupart des choses peuvent être refactorisées pour n'avoir aucun effet secondaire! (voir fonction pure)
Le clonage de gros objets peut être très coûteux en termes de performances. Heureusement, ce n'est pas un gros problème dans la pratique, car il existe d'excellentes bibliothèques qui permettent à ce type d'approche de programmation d'être rapide et moins gourmande en mémoire que pour le clonage manuel d'objets et de tableaux.
Mal:
functionaddItemToCart(cart: CartItem[],item: Item): void{cart.push({ item,date: Date.now()});};Bien:
functionaddItemToCart(cart: CartItem[],item: Item): CartItem[]{return[...cart,{ item,date: Date.now()}];};Polluer les fonctions globales est une mauvaise pratique en JavaScript car vous
pourriez entrer en conflit avec une autre bibliothèque et l'utilisateur de votre
API ne serait pas plus sage jusqu'à ce qu'il obtienne une exception en production.
Réfléchissons à un exemple: et si vous vouliez étendre la méthode native Array
de JavaScript pour avoir une méthode diff qui pourrait montrer la différence
entre deux tableaux? Vous pouvez écrire votre nouvelle fonction dans le
Array.prototype, mais elle pourrait entrer en conflit avec une autre bibliothèque
qui a essayé de faire la même chose. Et si cette autre bibliothèque utilisait
simplement diff pour trouver la différence entre le premier et le dernier élément
d'un tableau? C'est pourquoi il serait beaucoup mieux d'utiliser simplement des
classes et d'étendre simplement le global Array.
Mal:
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));};}Bien:
classMyArray<T>extendsArray<T>{diff(other: T[]): T[]{consthash=newSet(other);returnthis.filter(elem=>!hash.has(elem));};}Privilégiez ce style de programmation quand vous le pouvez.
Mal:
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;}Bien:
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);Mal:
if(subscription.isTrial||account.balance>0){// ...}Bien:
functioncanActivateService(subscription: Subscription,account: Account){returnsubscription.isTrial||account.balance>0;}if(canActivateService(subscription,account)){// ...}Mal:
functionisEmailNotUsed(email: string): boolean{// ...}if(isEmailNotUsed(email)){// ...}Bien:
functionisEmailUsed(email: string): boolean{// ...}if(!isEmailUsed(node)){// ...}Cela semble être une tâche impossible. En entendant cela pour la première fois,
la plupart des gens disent: “Comment suis-je censé faire quoi que ce soit sans
une déclaration if?" La réponse est que vous pouvez utiliser le polymorphisme
pour réaliser la même tâche dans de nombreux cas. La deuxième question est
généralement, "c'est bien, mais pourquoi voudrais-je faire ça?" La réponse est
un concept de code propre précédent que nous avons appris: une fonction ne devrait
faire qu'une seule chose. Lorsque vous avez des classes et des fonctions qui ont
des instructions if, vous dites à votre utilisateur que votre fonction fait
plus d'une chose. N'oubliez pas, faites juste une chose.
Mal:
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{// ...}}Bien:
abstractclassAirplane{protectedgetMaxAltitude(): number{// shared logic with subclasses ...}// ...}classBoeing777extendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getPassengerCount();}}classAirForceOneextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude();}}classCessnaextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getFuelExpenditure();}}TypeScript est un superset syntaxique strict de JavaScript et ajoute une vérification de type statique facultative au langage. Préférez toujours spécifier des types de variables, des paramètres et des valeurs de retour pour exploiter toute la puissance des fonctionnalités de TypeScript. Cela facilite la refactorisation.
Mal:
functiontravelToTexas(vehicle: Bicycle|Car){if(vehicleinstanceofBicycle){vehicle.pedal(currentLocation,newLocation('texas'));}elseif(vehicleinstanceofCar){vehicle.drive(currentLocation,newLocation('texas'));}}Bien:
typeVehicle=Bicycle|Car;functiontravelToTexas(vehicle: Vehicle){vehicle.move(currentLocation,newLocation('texas'));}Les navigateurs modernes font beaucoup d'optimisation sous le capot lors de l'exécution. Souvent, si vous optimisez, vous perdez simplement votre temps. Il existe de bonnes ressources pour voir où l'optimisation fait défaut. Ciblez ceux en attendant, jusqu'à ce qu'ils soient corrigés s'ils le peuvent.
Mal:
// 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++){// ...}Bien:
for(leti=0;i<list.length;i++){// ...}Le code qui ne s’utilise pas est tout aussi mauvais que le code en double. Il n'y a aucune raison de le conserver dans votre base de code. S'il n'est pas appelé, débarrassez-vous-en! Il sera toujours sauvegardé en sécurité dans votre historique de version si vous en avez toujours besoin.
Mal:
functionoldRequestModule(url: string){// ...}functionrequestModule(url: string){// ...}constreq=requestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');Bien:
functionrequestModule(url: string){// ...}constreq=requestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');Utilisez des générateurs et des itérables lorsque vous travaillez avec des collections de données utilisées comme un flux. Il y a quelques bonnes raisons:
- dissocie l'appelé de la mise en œuvre du générateur dans le sens où l'appelé décide du nombre éléments à accéder
- exécution paresseuse, les éléments sont diffusés à la demande
- prise en charge intégrée pour l'itération d'éléments à l'aide de la syntaxe
for-of - les itérables permettent d'implémenter des modèles d'itérateurs optimisés
Mal:
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);Bien:
// 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);Il existe des bibliothèques qui permettent de travailler avec les itérables de
la même manière qu'avec les tableaux natifs, en des méthodes de chaînage comme
map, slice, forEach etc. Voir itiriri
pour un exemple de manipulation avancée avec les itérables
(ou itiriri-async pour la manipulation
des itérables asynchrones).
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 supporte la syntaxe getter/setter.
L'utilisation de getters et de setters pour accéder aux données d'objets qui englobent le comportement pourrait être meilleure que la simple recherche d'un attribut sur un objet. "Pourquoi?" vous vous demander. Eh bien, voici une liste de raisons:
- Lorsque vous voulez faire plus que d'obtenir l’attribut d'un objet, vous n'avez pas besoin de rechercher et de modifier chaque accesseur dans votre base de code.
- Valide de façon plus simple en utilisant le mot-clé
set. - Encapsule la représentation interne.
- Facile à ajouter un log des activités et la gestion des erreurs.
- Vous pouvez paresseusement charger les attributs de votre objet, disons l'obtenir à partir d'un serveur.
Mal:
typeBankAccount={balance: number;// ...}constvalue=100;constaccount: BankAccount={balance: 0,// ...};if(value<0){thrownewError('Cannot set negative balance.');}account.balance=value;Bien:
classBankAccount{privateaccountBalance: number=0;getbalance(): number{returnthis.accountBalance;}setbalance(value: number){if(value<0){thrownewError('Cannot set negative balance.');}this.accountBalance=value;}// ...}// Now `BankAccount` encapsulates the validation logic.// If one day the specifications change, and we need extra validation rule,// we would have to alter only the `setter` implementation,// leaving all dependent code unchanged.constaccount=newBankAccount();account.balance=100;TypeScript prend en charge les accesseurs public(par défaut), protected
et private sur les membres de la classe.
Mal:
classCircle{radius: number;constructor(radius: number){this.radius=radius;}perimeter(){return2*Math.PI*this.radius;}surface(){returnMath.PI*this.radius*this.radius;}}Bien:
classCircle{constructor(privatereadonlyradius: number){}perimeter(){return2*Math.PI*this.radius;}surface(){returnMath.PI*this.radius*this.radius;}}Le système de types de TypeScript vous permet de marquer des attributs individuels
sur une interface/classe comme readonly. Cela vous permet de travailler de manière
fonctionnelle (une mutation inattendue est mauvaise). Pour les scénarios plus avancés,
il existe un type intégré Readonly qui prend un typeT et marque toutes ses
attributs comme étant “readonly” à l'aide de types mappés
(voir mapped types ou "types mappés").
Mal:
interfaceConfig{host: string;port: string;db: string;}Bien:
interfaceConfig{readonlyhost: string;readonlyport: string;readonlydb: string;}Dans le cas des tableaux, vous pouvez créer un tableau de readonly en utilisant
ReadonlyArray<T>.
N'autorisez pas les modifications telles que push() et fill(), mais plutôt
utilisez des fonctionnalités telles que concat() et slice() vu que celles-ci
ne modifient pas la valeur.
Mal:
constarray: number[]=[1,3,5];array=[];// errorarray.push(100);// array will updatedBien:
constarray: ReadonlyArray<number>=[1,3,5];array=[];// errorarray.push(100);// errorDéclarer des paramètres de type "read-only" dans TypeScript 3.4 est un peu plus facile.
functionhoge(args: readonlystring[]){args.push(1);// error}Préférer les assertions "const" pour des valeurs littérales.
Mal:
constconfig={hello: 'world'};config.hello='world';// value is changedconstarray=[1,3,5];array[0]=10;// value is changed// writable objects is returnedfunctionreadonlyData(value: number){return{ value };}constresult=readonlyData(100);result.value=200;// value is changedBien:
// read-only objectconstconfig={hello: 'world'}asconst;config.hello='world';// error// read-only arrayconstarray=[1,3,5]asconst;array[0]=10;// error// You can return read-only objectsfunctionreadonlyData(value: number){return{ value }asconst;}constresult=readonlyData(100);result.value=200;// errorUtilisez type lorsque vous pourriez avoir besoin d'une union ou d'une intersection.
Utilisez l'interface lorsque vous voulez “étendre” ou “implémenter”. Il n'y a
cependant pas de règle stricte, utilisez celle qui vous convient. Pour une
explication plus détaillée, reportez-vous à cette
réponse
sur les différences entre type et interface dans TypeScript.
Mal:
interfaceEmailConfig{// ...}interfaceDbConfig{// ...}interfaceConfig{// ...}//...typeShape={// ...}Bien:
typeEmailConfig={// ...}typeDbConfig={// ...}typeConfig=EmailConfig|DbConfig;// ...interfaceShape{// ...}classCircleimplementsShape{// ...}classSquareimplementsShape{// ...}La taille de la classe est mesurée par sa responsabilité. Suivant le Principe de Responsabilité Unique, une classe doit être petite.
Mal:
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{/* ... */}// ...}Bien:
classDashboard{disable(): void{/* ... */}enable(): void{/* ... */}getVersion(): string{/* ... */}}// split the responsibilities by moving the remaining methods to other classes// ...La cohésion définit le degré de relation entre les membres de la classe. Idéalement, tous les champs d'une classe doivent être utilisés par chaque méthode. On dit alors que la classe est au maximum cohérente. En pratique, cela n'est cependant pas toujours possible, ni même conseillé. Vous devez cependant préférer une cohésion élevée.
Le couplage fait référence à la façon dont les parents sont liés ou dépendants entre eux. Les classes sont dites à faible couplage si les changements dans l'un d'entre eux n'affectent pas l'autre.
Une bonne conception logicielle a une haute cohésion et un faible couplage.
Mal:
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>{// ...}}Bien:
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>{// ...}}Comme indiqué dans Design Patterns du “Gang of Four”, vous devriez préférer la composition à l'héritage où vous le pouvez. Il y a beaucoup de bonnes raisons d'utiliser l'héritage et beaucoup de bonnes raisons d'utiliser la composition. Le point principal de cette maxime est que si votre esprit va instinctivement à l'héritage, essayez de penser si la composition pourrait mieux modéliser votre problème. Dans certains cas, c'est possible.
Vous vous demandez peut-être alors "quand dois-je utiliser l'héritage?" Cela dépend de votre problème, mais c'est une liste décente où l'héritage a plus de sens que la composition:
Votre héritage représente une relation "est-une" et non une relation "a-une" (Humain-> Animal vs Utilisateur-> Détails de l'utilisateur).
Vous pouvez réutiliser le code des classes de base (les humains peuvent se déplacer comme tous les animaux).
Vous souhaitez apporter des modifications globales aux classes dérivées en modifiant une classe de base. (Modifiez la dépense calorique de tous les animaux lorsqu'ils se déplacent).
Mal:
classEmployee{constructor(privatereadonlyname: string,privatereadonlyemail: string){}// ...}// Bad because Employees "have" tax data. EmployeeTaxData is not a type of EmployeeclassEmployeeTaxDataextendsEmployee{constructor(name: string,email: string,privatereadonlyssn: string,privatereadonlysalary: number){super(name,email);}// ...}Bien:
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){}// ...}Ce modèle est très utile et couramment utilisé dans de nombreuses bibliothèques. Il permet à votre code d'être expressif et moins verbeux. Pour cette raison, utilisez le chaînage de méthodes et regardez à quel point votre code sera propre.
Mal:
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();Bien:
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();Comme indiqué dans Clean Code, "Il ne devrait jamais y avoir plus d'une raison pour qu'une classe change". Il est tentant d'emballer une classe avec beaucoup de fonctionnalités, comme lorsque vous ne pouvez emporter qu'une seule valise pendant votre vol. Le problème avec cela est que votre classe ne sera pas conceptuellement cohérente et cela lui donnera de nombreuses raisons de changer. Il est important de minimiser le nombre de fois que vous devez changer de classe. C'est important car si trop de fonctionnalités sont dans une classe et que vous en modifiez une partie, il peut être difficile de comprendre comment cela affectera les autres modules dépendants de votre base de code.
Mal:
classUserSettings{constructor(privatereadonlyuser: User){}changeSettings(settings: UserSettings){if(this.verifyCredentials()){// ...}}verifyCredentials(){// ...}}Bien:
classUserAuth{constructor(privatereadonlyuser: User){}verifyCredentials(){// ...}}classUserSettings{privatereadonlyauth: UserAuth;constructor(privatereadonlyuser: User){this.auth=newUserAuth(user);}changeSettings(settings: UserSettings){if(this.auth.verifyCredentials()){// ...}}}Comme l'a déclaré Bertrand Meyer, "les entités logicielles (classes, modules, fonctions, etc.) devraient être ouvertes pour extension, mais fermées pour modification." Mais qu'est-ce que cela signifie? Ce principe stipule essentiellement que vous devez autoriser les utilisateurs à ajouter de nouvelles fonctionnalités sans modifier le code existant.
Mal:
classAjaxAdapterextendsAdapter{constructor(){super();}// ...}classNodeAdapterextendsAdapter{constructor(){super();}// ...}classHttpRequester{constructor(privatereadonlyadapter: Adapter){}asyncfetch<T>(url: string): Promise<T>{if(this.adapterinstanceofAjaxAdapter){constresponse=awaitmakeAjaxCall<T>(url);// transform response and return}elseif(this.adapterinstanceofNodeAdapter){constresponse=awaitmakeHttpCall<T>(url);// transform response and return}}}functionmakeAjaxCall<T>(url: string): Promise<T>{// request and return promise}functionmakeHttpCall<T>(url: string): Promise<T>{// request and return promise}Bien:
abstractclassAdapter{abstractasyncrequest<T>(url: string): Promise<T>;// code shared to subclasses ...}classAjaxAdapterextendsAdapter{constructor(){super();}asyncrequest<T>(url: string): Promise<T>{// request and return promise}// ...}classNodeAdapterextendsAdapter{constructor(){super();}asyncrequest<T>(url: string): Promise<T>{// request and return promise}// ...}classHttpRequester{constructor(privatereadonlyadapter: Adapter){}asyncfetch<T>(url: string): Promise<T>{constresponse=awaitthis.adapter.request<T>(url);// transform response and return}}C'est un terme effrayant pour un concept très simple. Il est formellement défini comme "Si S est un sous-type de T, alors les objets de type T peuvent être remplacés par des objets de type S (c'est-à-dire que les objets de type S peuvent remplacer des objets de type T) sans altérer aucune des attributs souhaitables de ce programme (correction, tâche effectuée, etc.)." C'est une définition encore plus effrayante.
La meilleure explication est que si vous avez une classe parent et une classe enfant, la classe parent et la classe enfant peuvent être utilisées de manière interchangeable sans obtenir de résultats incorrects. Cela peut encore prêter à confusion, alors jetons un coup d'œil à l'exemple classique de Square-Rectangle. Mathématiquement, un carré est un rectangle, mais si vous le modélisez en utilisant la relation "is-a" via l'héritage, vous rencontrez rapidement des problèmes.
Mal:
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);Bien:
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);L'ISP déclare que "les clients ne devraient pas être obligés de dépendre d'interfaces qu'ils n'utilisent pas.". Ce principe est très lié au principe de responsabilité unique. Ce que cela signifie vraiment, c'est que vous devez toujours concevoir vos abstractions de manière à ce que les clients qui utilisent les méthodes exposées n'obtiennent pas tout le gâteau à la place. Cela implique également d'imposer aux clients la charge de mettre en œuvre des méthodes dont ils n'ont pas réellement besoin.
Mal:
interfaceSmartPrinter{print();fax();scan();}classAllInOnePrinterimplementsSmartPrinter{print(){// ...}fax(){// ...}scan(){// ...}}classEconomicPrinterimplementsSmartPrinter{print(){// ...}fax(){thrownewError('Fax not supported.');}scan(){thrownewError('Scan not supported.');}}Bien:
interfacePrinter{print();}interfaceFax{fax();}interfaceScanner{scan();}classAllInOnePrinterimplementsPrinter,Fax,Scanner{print(){// ...}fax(){// ...}scan(){// ...}}classEconomicPrinterimplementsPrinter{print(){// ...}}Ce principe énonce deux choses essentielles:
- Les modules de haut niveau ne doivent pas dépendre de modules de bas niveau. Les deux devraient dépendre d'abstractions.
- Les abstractions ne devraient pas dépendre des détails. Les détails doivent dépendre des abstractions.
Cela peut être difficile à comprendre au début, mais si vous avez travaillé avec Angular, vous avez vu une implémentation de ce principe sous la forme d'une injection de dépendance (DI). Bien qu'il ne s'agisse pas de concepts identiques, DIP empêche les modules de haut niveau de connaître les détails de ses modules de bas niveau et de les configurer. Il peut y parvenir grâce à DI. Un énorme avantage de ceci est qu'il réduit le couplage entre les modules. Le couplage est un très mauvais schéma de développement car il rend votre code difficile à refactoriser.
Le DIP est généralement obtenu en utilisant un conteneur d'inversion de contrôle (IoC). Un exemple de conteneur IoC puissant pour TypeScript est InversifyJs.
Mal:
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');Bien:
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');Les tests sont plus importants que l'expédition. Si vous n'avez aucun test ou une quantité insuffisante, chaque fois que vous expédiez du code, vous ne serez pas sûr de ne rien casser. Décider de ce qui constitue un montant adéquat appartient à votre équipe, mais avoir une couverture à 100% (tous les relevés et succursales) est la façon dont vous obtenez une confiance très élevée et une tranquillité d'esprit de développeur Cela signifie qu'en plus d'avoir un excellent cadre de test, vous devez également utiliser un bon outil de couverture.
Il n'y a aucune excuse pour ne pas écrire de tests. Il existe beaucoup de bons frameworks de test JS avec prise en charge des typages pour TypeScript, alors trouvez celui que votre équipe préfère. Lorsque vous en trouvez un qui fonctionne pour votre équipe, essayez de toujours écrire des tests pour chaque nouvelle fonctionnalité/module que vous introduisez. Si votre méthode préférée est le développement piloté par les tests (TDD), c'est très bien, mais le principal est de vous assurer que vous atteignez vos objectifs de couverture avant de lancer une fonctionnalité ou de refactoriser une fonctionnalité existante.
Vous n'êtes pas autorisé à écrire un code de production, sauf pour effectuer un test unitaire ayant échoué.
Vous n'êtes pas autorisé à écrire plus d'un test unitaire que ce qui est suffisant pour échouer; et les échecs de compilation sont des échecs.
Vous n'êtes pas autorisé à écrire plus de code de production qu'il n'en faut pour réussir le test unitaire ayant échoué.
Les tests propres doivent suivre les règles:
Fast: les tests rapides doivent être rapides car nous voulons les exécuter fréquemment.
Independent: les tests indépendants ne devraient pas dépendre les uns des autres. Ils doivent fournir la même sortie, qu'ils soient exécutés indépendamment ou tous ensemble dans n'importe quel ordre.
Repeatable: les tests doivent être reproductibles dans tous les environnements et ne soyez pas une excuse pour pourquoi ils échouent.
Self-Validating: un test doit répondre avec Réussi ou Échoué. Vous n'avez pas besoin de comparer les fichiers journaux pour répondre si un test a réussi.
Timely: des tests unitaires opportuns doivent être écrits avant le code de production. Si vous écrivez des tests après le code de production, vous pourriez trouver les tests d'écriture trop difficiles.
Les tests doivent également suivre le principe de responsabilité unique. Faites une seule assertion par test unitaire.
Mal:
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));});});Bien:
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));});});Lorsqu'un test échoue, son nom est la première indication de ce qui peut avoir mal tourné.
Mal:
describe('Calendar',()=>{it('2/29/2020',()=>{// ...});it('throws',()=>{// ...});});Bien:
describe('Calendar',()=>{it('should handle leap year',()=>{// ...});it('should throw when format is invalid',()=>{// ...});});Les callbacks ne sont pas propres et provoquent des quantités excessives
d'imbrication (l'enfer des callbacks). Il existe des utilitaires qui transforment
les fonctions existantes en utilisant le style du callback en une version qui renvoie
des promesses (pour Node.js,
voir util.promisify,
pour un usage général, voir pify, es6-promisify)
Mal:
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);}});Bien:
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));Les promesses prennent en charge quelques méthodes d'assistance qui aident à rendre le code plus concis:
| Modèle | Description |
|---|---|
Promise.resolve(value) | Convertit une valeur en promesse résolue. |
Promise.reject(error) | Convertit une erreur en une promesse rejetée. |
Promise.all(promises) | Renvoie une nouvelle promesse qui est remplie avec un tableau de valeurs de réalisation pour les promesses ou les refus passés avec la raison de la première promesse qui rejette. |
Promise.race(promises) | Renvoie une nouvelle promesse qui est remplie/rejetée avec le résultat/l'erreur de la première promesse réglée à partir du tableau des promesses passées. |
Promise.all est particulièrement utile lorsqu'il est nécessaire d'exécuter
des tâches en parallèle. Promise.race facilite l'implémentation de choses comme
les délais d'attente pour les promesses.
Avec la syntaxe async/await, vous pouvez écrire du code beaucoup plus propre et
plus compréhensible que les promesses enchaînées. Dans une fonction préfixée par
le mot clé async, vous avez un moyen de dire au runtime de JavaScript de suspendre
l'exécution du code sur le mot cléawait (lorsqu'il est utilisé sur une promesse).
Mal:
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));Bien:
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 functiontry{constcontent=awaitdownloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin','article.html');console.log(content);}catch(error){console.error(error);}Les erreurs lancées sont une bonne chose! Elles signifient que le runtime a réussi à identifier quand quelque chose dans votre programme a mal tourné et qu'il vous informe en arrêtant la fonction exécution sur la pile actuelle, tuant le processus (dans Node), et vous notifiant dans la console avec une trace de pile.
JavaScript, ainsi que TypeScript, vous permet de throw ou "lancer" n'importe quel
objet. Une promesse peut également être rejetée avec n'importe quel objet de motif.
Il est conseillé d'utiliser la syntaxe throw avec un typeError. C'est parce que
votre erreur peut être interceptée dans un code de niveau supérieur avec une syntaxe catch.
Il serait très déroutant d’y attraper un message de chaîne et
débogage plus douloureux.
Pour la même raison, vous devez rejeter les promesses avec des types Error.
Mal:
functioncalculateTotal(items: Item[]): number{throw'Not implemented.';}functionget(): Promise<Item[]>{returnPromise.reject('Not implemented.');}Bien:
functioncalculateTotal(items: Item[]): number{thrownewError('Not implemented.');}functionget(): Promise<Item[]>{returnPromise.reject(newError('Not implemented.'));}// or equivalent to:asyncfunctionget(): Promise<Item[]>{thrownewError('Not implemented.');}L'avantage de l'utilisation des types Error est qu'il est supporté par la
syntaxetry/catch/finally et implicitement toutes les erreurs ont l'attribut
stack qui est très puissante pour le débogage. Il existe également d'autres
alternatives, pour ne pas utiliser la syntaxe throw et renvoyer à la place
toujours des objets d'erreur personnalisés. TypeScript rend cela encore plus
facile. Prenons l'exemple suivant:
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};}Pour l'explication détaillée de cette idée, reportez-vous à la publication d'origine.
Ne rien faire avec une erreur détectée ne vous donne pas la possibilité de corriger
ou de réagir à cette erreur. L'enregistrement de l'erreur sur la console (console.log)
n'est pas beaucoup mieux car il arrive souvent qu'elle se perde dans un océan de
choses imprimées sur la console. Si vous enveloppez un morceau de code dans un
try/catch cela signifie que vous pensez qu'une erreur peut s'y produire et que
vous devez donc avoir un plan, ou créer un chemin de code, pour quand il se produit.
Mal:
try{functionThatMightThrow();}catch(error){console.log(error);}// or even worsetry{functionThatMightThrow();}catch(error){// ignore error}Bien:
import{logger}from'./logging'try{functionThatMightThrow();}catch(error){logger.log(error);}Pour la même raison, vous ne devez pas ignorer les erreurs interceptées de try/catch.
Mal:
getUser().then((user: User)=>{returnsendEmail(user.email,'Welcome!');}).catch((error)=>{console.log(error);});Bien:
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);}Le formatage est subjectif. Comme beaucoup de règles ici, il n'y a pas de règle stricte que vous devez suivre. Le but principal est NE PAS DISCUTER sur le formatage. Il existe des tonnes d'outils pour automatiser cela. Utilisez-en un! C'est une perte de temps et d'argent pour les ingénieurs de discuter du formatage. La règle générale à suivre est de conserver des règles de formatage cohérentes.
Pour TypeScript, il existe un outil puissant appelé TSLint. Il s'agit d'un outil d'analyse statique qui peut vous aider à améliorer considérablement la lisibilité et la maintenabilité de votre code. Il existe des configurations TSLint prêtes à l'emploi que vous pouvez référencer dans vos projets:
TSLint Config Standard - règles de style standard
TSLint Config Airbnb - guide de style Airbnb
TSLint Clean Code - Les règles TSLint inspirées du Clean Code: A Handbook of Agile Software Craftsmanship
TSLint react - règles sur les peluches liées à React et JSX
TSLint + Prettier - règles de "lint" pour Prettier formateur de code
ESLint rules for TSLint - règles ESLint pour TypeScript
Immutable - règles pour désactiver la mutation dans TypeScript
Reportez-vous également à cette excellente source TypeScript StyleGuide and Coding Conventions.
La capitalisation vous en dit long sur vos variables, fonctions, etc. Ces règles sont subjectives, donc votre équipe peut choisir ce qu'elle veut. Le fait est, peu importe ce que vous choisissez tous, juste soyez cohérent.
Mal:
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={/* ... */}Bien:
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={/* ... */}Utiliser PascalCase de préférence pour les noms de classe, d'interface, de type et d'espace de noms.
Utiliser camelCase de préférence pour les variables, les fonctions et les membres de la classe.
Si une fonction en appelle une autre, gardez ces fonctions verticalement fermées dans le fichier source. Idéalement, gardez le "caller" juste au-dessus du "callee". Nous avons tendance à lire le code de haut en bas, comme un journal. Pour cette raison, faites lire votre code de cette façon.
Mal:
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();Bien:
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();Avec des instructions d'importation propres et faciles à lire, vous pouvez rapidement
voir les dépendances du code actuel. Assurez-vous d'appliquer les bonnes pratiques
suivantes pour les instructions import:
- Les déclarations d'importation doivent être classées par ordre alphabétique et regroupées.
- Les importations non utilisées doivent être supprimées.
- Les importations nommées doivent être classées par ordre alphabétique (c'est-à-dire
import {A, B, C} de 'foo';) - Les sources d'importation doivent être classées par ordre alphabétique dans les groupes, c'est-à-dire:
import * as foo from 'a'; importer *comme barre de 'b'; - Les groupes d'importations sont délimités par des lignes blanches.
- Les groupes doivent respecter l'ordre suivant:
- Polyfills (c'est-à-dire
import 'reflect-metadata';) - modules intégrés de Node (c'est-à-dire
import fs from 'fs';) - modules externes (c'est-à-dire
import {query} from 'itiriri';) - modules internes (c'est-à-dire
import {UserService} from 'src/services/userService';) - modules d'un répertoire parent (c'est-à-dire
import foo from '../foo'; import qux from '../../foo/qux';) - modules du même répertoire ou d'un répertoire frère (c'est-à-dire
import bar from './bar'; import baz from './bar/baz';)
- Polyfills (c'est-à-dire
Mal:
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';Bien:
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';Créez des importations plus jolies en définissant les chemins d'accès et les attributs
baseUrl dans la section compilerOptions dans le fichier tsconfig.json.
Cela évitera de longs chemins relatifs lors des importations.
Mal:
import{UserService}from'../../../services/UserService';Bien:
import{UserService}from'@services/UserService';// tsconfig.json
...
"compilerOptions": {
...
"baseUrl": "src","paths": {"@services": ["services/*"]}...}...L'utilisation de commentaires est une indication de non-expression sans eux. Le code devrait être la seule source de vérité.
Ne commentez pas le mauvais code - réécrivez-le. — Brian W. Kernighan and P. J. Plaugher
Les commentaires sont des excuses, pas une exigence. Un bon code surtout se documente.
Mal:
// Check if subscription is active.if(subscription.endDate>Date.now){}Bien:
constisSubscriptionActive=subscription.endDate>Date.now;if(isSubscriptionActive){/* ... */}Le contrôle de version existe pour une raison. Laissez l'ancien code dans votre historique.
Mal:
typeUser={name: string;email: string;// age: number;// jobPosition: string;}Bien:
typeUser={name: string;email: string;}Rappelez-vous, utilisez le contrôle de version! Ce n'est pas nécessaire garder des
codes non utilisés ou commentés, et surtout de commentaires dans l'archive de base.
Utilisez le git log pour obtenir l'historique!
Mal:
/** * 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;}Bien:
functioncombine(a: number,b: number): number{returna+b;}Ils ajoutent généralement du "noise" au code. Laissez les fonctions et les noms de variables ainsi que l'indentation et le formatage appropriés donner la structure visuelle à votre code. La plupart des IDE prennent en charge la fonction de pliage de code qui vous permet de réduire/développer des blocs de code (voir Visual Studio Code régions de pliage).
Mal:
////////////////////////////////////////////////////////////////////////////////// Client class////////////////////////////////////////////////////////////////////////////////classClient{id: number;name: string;address: Address;contact: Contact;////////////////////////////////////////////////////////////////////////////////// public methods////////////////////////////////////////////////////////////////////////////////publicdescribe(): string{// ...}////////////////////////////////////////////////////////////////////////////////// private methods////////////////////////////////////////////////////////////////////////////////privatedescribeAddress(): string{// ...}privatedescribeContact(): string{// ...}};Bien:
classClient{id: number;name: string;address: Address;contact: Contact;publicdescribe(): string{// ...}privatedescribeAddress(): string{// ...}privatedescribeContact(): string{// ...}};Lorsque vous vous rendez compte que vous devez laisser des notes dans le code pour
des améliorations ultérieures, faites-le en utilisant les commentaires // TODO.
La plupart des IDE ont un support spécial pour ce genre de commentaires afin que
vous puissiez parcourir rapidement la liste complète des "todos".
Gardez cependant à l'esprit qu'un commentaire TODO n'est pas une excuse pour un mauvais code.
Mal:
functiongetActiveSubscriptions(): Promise<Subscription[]>{// ensure `dueDate` is indexed.returndb.subscriptions.find({dueDate: {$lte: newDate()}});}Bien:
functiongetActiveSubscriptions(): Promise<Subscription[]>{// TODO: ensure `dueDate` is indexed.returndb.subscriptions.find({dueDate: {$lte: newDate()}});}Ceci est également disponible dans d'autres langues:
Anglais: labs42io/clean-code-typescript
Portugais Brésilien: vitorfreitas/clean-code-typescript
Japonais: MSakamaki/clean-code-typescript
Espagnol: JoseDeFreitas/clean-code-typescript
Coréen: 738/clean-code-typescript
Les références seront ajoutées une fois les traductions terminées. Consultez cette discussion pour plus de détails et de progrès. Vous pouvez apporter une contribution indispensable à la communauté Clean Code en le traduisant dans votre langue.



