Чистый код для TypeScript — Часть 2

от автора

Первая часть, судя по комментариям вызвала неоднозначное мнение, особенно что касалось части enum. Где-то я так же могу не соглашаться, как с автором оригинала, так и с некоторыми комментариями. Но как и указывалось в начальном описании к первой части, чистый код это не догма которую нужно соблюдать, это всего лишь рекомендации, соблюдение которых каждый сам выбирает под свои потребности и взгляды.

Объекты и структуры данных

Используйте иммутабельность

Система типов в TypeScript позволяет помечать отдельные свойства интерфейса/класса как readonly поля (только для чтения). Это позволяет вам работать функционально (неожиданная мутация это плохо). Для более сложных сценариев есть встроенный тип Readonly, который принимает типT и помечает все его свойства только для чтения с использованием mapped types (смотрите mapped types).

Плохо:

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

Хорошо:

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

В случае массива вы можете создать массив только для чтения, используя ReadonlyArray<T>. который не позволяет делать изменения с использованием push() и fill(), но можно использовать concat() и slice() они не меняют значения.

Плохо:

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

Хорошо:

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

Объявление аргументов только для чтения TypeScript 3.4 is a bit easier.

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

Предпочтение const assertions для литеральных значений.

Плохо:

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; // значение изменено

Хорошо:

// объект только для чтения 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; // ошибка

Типы vs. интерфейсы

Используйте типы, когда вам может понадобиться объединение или пересечение. Используйте интерфейс, когда хотите использовать extends или implements. Однако строгого правила не существует, используйте то, что работает у вас. Для более подробного объяснения посмотрите это ответы о различиях между type and interface в TypeScript.

Плохо:

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

Хорошо:

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

Классы

Классы должны быть маленькими

Размер класса измеряется его ответственностью. Следуя Принципу единственной ответственности класс должен быть маленьким.

Плохо:

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

Хорошо:

class Dashboard {   disable(): void { /* ... */ }   enable(): void { /* ... */ }   getVersion(): string { /* ... */ } }  // разделить обязанности, переместив оставшиеся методы в другие классы // ...

Высокая сплоченность низкая связь

Сплоченность определяет степень, в которой члены класса связаны друг с другом. В идеале все поля в классе должны использоваться каждым методом. Мы говорим, что класс максимально связный. На практике это, однако, не всегда возможно и даже нежелательно. Однако вы должны добиваться, того чтобы сплоченность была высокой.

Связанность относится и к тому, как связаны или зависимы два класса друг от друга. Классы считаются слабосвязанными если изменения в одном из них не влияют на другой.

Плохо:

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

Хорошо:

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

Предпочитайте композицию наследованию

Как сказано в Design Patterns от банды черытех вы должны
Предпочитать композицию наследованию где можете. Есть много веских причин использовать наследование и много хороших причин использовать композицию. Суть этого принципа в том, что если ваш ум инстинктивно идет на наследование, попробуйте подумать, может ли композиция лучше смоделировать вашу проблему. В некоторых случаях может.

Тогда вы можете спросить: "Когда я должен использовать наследование?" Это зависит от вашей проблемы, но это достойный список, когда наследование имеет больше смысла, чем композиция:

  1. Ваше наследование представляет собой "is-a" отношения а не "has-a" отношения (Human->Animal vs. User->UserDetails).
  2. Вы можете повторно использовать код из базовых классов (Люди могут двигаться как все животные).
  3. Вы хотите внести глобальные изменения в производные классы, изменив базовый класс. (Изменение расхода калорий у всех животных при их перемещении).

Плохо:

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

Хорошо:

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

Используйте цепочки вызовов

Этот паттеррн очень полезен и обычно используется во многих библиотеках. Это позволяет вашему коду быть выразительным и менее многословным. По этой причине используйте цепочку методов и посмотрите, насколько чистым будет ваш код.

Плохо:

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

Хорошо:

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

Продолжение следует…

ссылка на оригинал статьи https://habr.com/ru/post/485068/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *