Código Limpo para TypeScript - Parte 2

A primeira parte , a julgar pelos comentários, causou uma opinião mista, especialmente no que diz respeito à parte enum. Em algum lugar, também posso discordar, tanto do autor do original quanto de alguns comentários. Mas, como indicado na descrição inicial da primeira parte, o código limpo não é um dogma a ser seguido, são apenas recomendações, cuja observância todos escolhem para atender às suas necessidades e pontos de vista.



Objetos e estruturas de dados


Use imunidade


O sistema de tipos no TypeScript permite marcar propriedades individuais da interface / classe como campos somente leitura (somente leitura) . Isso permite que você trabalhe funcionalmente (mutações inesperadas são ruins). Para cenários mais complexos, existe um tipo Readonly que pega um tipo T e marca todas as suas propriedades somente leitura usando tipos mapeados (consulte tipos mapeados ).


Ruim:


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

Bom:


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

No caso de uma matriz, você pode criar uma matriz somente ReadonlyArray<T> usando o ReadonlyArray<T> . que não permite alterações usando push() e fill() , mas você pode usar concat() e slice() não alterar os valores.


Ruim:


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

Bom:


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

Declarando argumentos somente leitura O TypeScript 3.4 é um pouco mais fácil .


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

Constações de preferência para valores literais.


Ruim:


 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; //   

Bom:


 //     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; //  

Tipos vs. interfaces


Use tipos quando precisar de união ou interseção. Use a interface quando desejar usar extends ou implements . No entanto, uma regra estrita não existe, use o que funciona para você. Para uma explicação mais detalhada, consulte estas respostas sobre as diferenças entre type e interface no TypeScript.


Ruim:


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

Bom:


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

Aulas


As aulas devem ser pequenas


O tamanho de uma classe é medido por sua responsabilidade. Seguindo o princípio de responsabilidade exclusiva, a classe deve ser pequena.


Ruim:


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

Bom:


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

Baixa ligação de alta coesão


A coesão determina o grau em que os membros de uma classe estão relacionados. Idealmente, todos os campos de uma classe devem ser usados ​​por cada método. Dizemos que a classe está o mais conectada possível . Na prática, no entanto, isso nem sempre é possível e até indesejável. No entanto, você deve garantir que a coesão seja alta.


Conectividade também se refere a como duas classes são relacionadas ou dependentes uma da outra. As classes são consideradas fracamente acopladas se as alterações em uma delas não afetarem a outra.


Ruim:


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

Bom:


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

Preferir composição sobre herança


Como dito em Padrões de Design da Quarta Turma, você deve
Prefira composição à herança sempre que puder. Há muitas boas razões para usar a herança e muitas boas razões para usar a composição. A essência desse princípio é que, se sua mente herdar instintivamente, tente pensar se a composição pode modelar melhor o seu problema. Em alguns casos, pode.


Então você pode perguntar: "Quando devo usar a herança?" Depende do seu problema, mas é uma lista decente quando a herança faz mais sentido que a composição:


  1. Sua herança é uma relação "é-a" e não uma relação "tem-a" (Humano-> Animal vs. Usuário-> Detalhes do Usuário).
  2. Você pode reutilizar o código das classes base (as pessoas podem se mover como todos os animais).
  3. Você deseja fazer alterações globais nas classes derivadas alterando a classe base. (Mudança no gasto calórico em todos os animais ao movê-los).

Ruim:


 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); } // ... } 

Bom:


 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) { } // ... } 

Usar cadeias de chamadas


Esse padrão é muito útil e é comumente usado em muitas bibliotecas. Isso permite que seu código seja expressivo e menos detalhado. Por esse motivo, use uma cadeia de métodos e veja como seu código ficará limpo.


Ruim:


 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(); 

Bom:


 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(); 

Para continuar ...

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


All Articles