Code propre pour TypeScript - Partie 2

La première partie , à en juger par les commentaires, a suscité une opinion mitigée, en particulier en ce qui concerne la partie enum. Quelque part, je peux également être en désaccord, à la fois avec l'auteur de l'original et avec certains commentaires. Mais comme indiqué dans la description initiale de la première partie, le code propre n'est pas un dogme à suivre, ce sont juste des recommandations dont chacun choisit de s'adapter à ses besoins et à ses opinions.



Objets et structures de données


Utilisez l'immunité


Le système de types de TypeScript vous permet de marquer les propriétés individuelles d'une interface / classe en tant que champs en lecture seule (lecture seule) . Cela vous permet de travailler de manière fonctionnelle (une mutation inattendue est mauvaise). Pour les scénarios plus complexes, il existe un type Readonly qui prend un type T et marque toutes ses propriétés en lecture seule à l'aide de types mappés (voir types mappés ).


Mauvais:


 interface Config { host: string; port: string; db: string; } 

Bon:


 interface Config { readonly host: string; readonly port: string; readonly db: string; } 

Dans le cas d'un tableau, vous pouvez créer un tableau en lecture seule à l'aide de ReadonlyArray<T> . qui n'autorise pas les modifications à l'aide de push() et fill() , mais vous pouvez utiliser concat() et slice() ils ne modifient pas les valeurs.


Mauvais:


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

Bon:


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

Déclarer des arguments en lecture seule TypeScript 3.4 est un peu plus facile .


 function hoge(args: readonly string[]) { args.push(1); // error } 

Assertions const de préférence pour les valeurs littérales.


Mauvais:


 const config = { hello: 'world' }; config.hello = 'world'; //   const array = [ 1, 3, 5 ]; array[0] = 10; //   //    function readonlyData(value: number) { return { value }; } const result = readonlyData(100); result.value = 200; //   

Bon:


 //     const config = { hello: 'world' } as const; config.hello = 'world'; //  //     const array = [ 1, 3, 5 ] as const; array[0] = 10; //  //        function readonlyData(value: number) { return { value } as const; } const result = readonlyData(100); result.value = 200; //  

Types vs interfaces


Utilisez des types lorsque vous pouvez avoir besoin d'une union ou d'une intersection. Utilisez l'interface lorsque vous souhaitez utiliser des extends ou des implements . Cependant, une règle stricte n'existe pas, utilisez ce qui fonctionne pour vous. Pour une explication plus détaillée, consultez ces réponses sur les différences entre le type et l' interface dans TypeScript.


Mauvais:


 interface EmailConfig { // ... } interface DbConfig { // ... } interface Config { // ... } //... type Shape = { // ... } 

Bon:


 type EmailConfig = { // ... } type DbConfig = { // ... } type Config = EmailConfig | DbConfig; // ... interface Shape { // ... } class Circle implements Shape { // ... } class Square implements Shape { // ... } 

Cours


Les classes doivent être petites


La taille d'une classe est mesurée par sa responsabilité. Suivant le principe de la responsabilité exclusive, la classe doit être petite.


Mauvais:


 class Dashboard { 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 { /* ... */ } // ... } 

Bon:


 class Dashboard { disable(): void { /* ... */ } enable(): void { /* ... */ } getVersion(): string { /* ... */ } } //  ,       // ... 

Haute cohésion, faible adhérence


La cohésion détermine le degré de relation entre les membres d'une classe. Idéalement, tous les champs d'une classe doivent être utilisés par chaque méthode. Nous disons que la classe est aussi connectée que possible . Dans la pratique, cependant, cela n'est pas toujours possible et même indésirable. Cependant, vous devez vous assurer que la cohésion est élevée.


La connectivité fait également référence à la façon dont deux classes sont liées ou dépendantes l'une de l'autre. Les classes sont considérées comme faiblement couplées si les modifications de l'une d'entre elles n'affectent pas l'autre.


Mauvais:


 class UserManager { // :         . //   ,    ,    //      ,     , //       `emailSender`. constructor( private readonly db: Database, private readonly emailSender: EmailSender) { } async getUser(id: number): Promise<User> { return await db.users.findOne({ id }); } async getTransactions(userId: number): Promise<Transaction[]> { return await db.transactions.find({ userId }); } async sendGreeting(): Promise<void> { await emailSender.send('Welcome!'); } async sendNotification(text: string): Promise<void> { await emailSender.send(text); } async sendNewsletter(): Promise<void> { // ... } } 

Bon:


 class UserService { constructor(private readonly db: Database) { } async getUser(id: number): Promise<User> { return await this.db.users.findOne({ id }); } async getTransactions(userId: number): Promise<Transaction[]> { return await this.db.transactions.find({ userId }); } } class UserNotifier { constructor(private readonly emailSender: EmailSender) { } async sendGreeting(): Promise<void> { await this.emailSender.send('Welcome!'); } async sendNotification(text: string): Promise<void> { await this.emailSender.send(text); } async sendNewsletter(): Promise<void> { // ... } } 

Préfère la composition à l'héritage


Comme indiqué dans Design Patterns from the Fourth Gang, vous devez
Préférez la composition à l'héritage partout où vous le pouvez. Il existe de nombreuses bonnes raisons d'utiliser l'héritage et de nombreuses bonnes raisons d'utiliser la composition. L'essence de ce principe est que si votre esprit hérite instinctivement, essayez de penser si la composition peut mieux modéliser votre problème. Dans certains cas, c'est possible.


Ensuite, vous pouvez demander: "Quand dois-je utiliser l'héritage?" Cela dépend de votre problème, mais c'est une liste décente lorsque l'héritage a plus de sens que la composition:


  1. Votre héritage est une relation is-a et non une relation has-a (Human-> Animal vs. User-> UserDetails).
  2. Vous pouvez réutiliser le code des classes de base (les gens peuvent se déplacer comme tous les animaux).
  3. Vous souhaitez apporter des modifications globales aux classes dérivées en modifiant la classe de base. (Variation de la dépense calorique de tous les animaux lors de leur déplacement).

Mauvais:


 class Employee { constructor( private readonly name: string, private readonly email: string) { } // ... } // ,   Employees ""  . EmployeeTaxData    Employee class EmployeeTaxData extends Employee { constructor( name: string, email: string, private readonly ssn: string, private readonly salary: number) { super(name, email); } // ... } 

Bon:


 class Employee { private taxData: EmployeeTaxData; constructor( private readonly name: string, private readonly email: string) { } setTaxData(ssn: string, salary: number): Employee { this.taxData = new EmployeeTaxData(ssn, salary); return this; } // ... } class EmployeeTaxData { constructor( public readonly ssn: string, public readonly salary: number) { } // ... } 

Utiliser des chaînes d'appel


Ce modèle est très utile et est couramment utilisé dans de nombreuses bibliothèques. Cela permet à votre code d'être expressif et moins verbeux. Pour cette raison, utilisez une chaîne de méthodes et voyez à quel point votre code sera propre.


Mauvais:


 class QueryBuilder { private collection: string; private pageNumber: number = 1; private itemsPerPage: number = 100; private orderByFields: 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 { // ... } } // ... const queryBuilder = new QueryBuilder(); queryBuilder.from('users'); queryBuilder.page(1, 100); queryBuilder.orderBy('firstName', 'lastName'); const query = queryBuilder.build(); 

Bon:


 class QueryBuilder { private collection: string; private pageNumber: number = 1; private itemsPerPage: number = 100; private orderByFields: string[] = []; from(collection: string): this { this.collection = collection; return this; } page(number: number, itemsPerPage: number = 100): this { this.pageNumber = number; this.itemsPerPage = itemsPerPage; return this; } orderBy(...fields: string[]): this { this.orderByFields = fields; return this; } build(): Query { // ... } } // ... const query = new QueryBuilder() .from('users') .page(1, 100) .orderBy('firstName', 'lastName') .build(); 

À suivre ...

Source: https://habr.com/ru/post/fr485068/


All Articles