DevGang
Авторизоваться

Освоение Angular: Основные принципы организации кода

Эта статья поможет вам избежать ошибок, которые трудно (или просто утомительно) исправить позже. Если вы собираетесь создать новый проект и хотите сделать его потрясающим — продолжайте читать!

Чистые функции

Это самое главное: держите свои функции в чистоте, как можно больше.

Вот как Википедия определяет чистые функции:

  1. возвращаемые значения функции идентичны для идентичных аргументов (никаких изменений с локальными статическими переменными, нелокальными переменными, изменяемыми ссылочными аргументами или входными потоками) и
  2. функция не имеет побочных эффектов (без изменения локальных статических переменных, нелокальных переменных, изменяемых ссылочных аргументов или потоков ввода/вывода).

Функции, использующие ключевое слово this, не являются чистыми — они используют информацию за пределами своей области, поэтому могут возвращать разные результаты для одних и тех же аргументов.

Тем не менее, некоторые из наших функций, очевидно, должны использовать this — все же постарайтесь переместить как можно больше вашего кода из нечистых функций в чистые функции.

Функции, которые изменяют свои аргументы, являются самым большим источником зла — избегайте их любой ценой.

Неизменность

Случайные мутации данных часто приводят к опасным ошибкам, вот почему сообщество JS создало инструменты для обеспечения неизменности структур данных. Вы можете найти их и прочитать их документацию, если хотите (по крайней мере, прочитать о них - хорошая идея, в любом случае.

Мы покажем вам один трюк, который будет “достаточно хорош” во многих случаях — он не спасет вас от каждой ошибки (как это сделали бы инструменты неизменяемости), но он охватит абсолютное большинство случаев, и вам это ничего не будет стоить:

export type UserModel = {
  readonly name: string;
  readonly email?: string;
  readonly age?: string;
}

Добавляя ключевое слово readonly в поля ваших структур данных, вы будете получать ошибку TS при каждой попытке изменить данные, полученные вами в качестве аргумента. И все это будет стоить только во время компиляции — оно будет удалено после компиляции и не будет проверяться во время выполнения (0 затрат на производительность).

Достаточно просто создать неглубокую копию, чтобы избавиться от этой ошибки и создать новый объект с измененным полем:

updatedUser = {
  ...user,
  age: 25
}

Кроме того, это не так ограничительно, как инструменты для изменения — если вы знаете, что делаете, и хотите игнорировать это ограничение для конкретного случая, вы можете применить модификатор readonly или тип Writeable, из type-fest или ts-essentials.

В этих библиотеках вы также можете найти типы, позволяющие сделать вашу структуру данных доступной только для чтения рекурсивно (deep), но на практике это работает не так безупречно и предсказуемо, как ожидалось — попробуйте их, если хотите, но мы обнаружили, что лучше либо вручную объявлять поля доступными только для чтения (и делать это рекурсивно для вложенных структур), или просто используйте библиотеки неизменяемости.

Неизменность - это большая тема, слишком большая для одной части статьи, и вы можете найти гораздо больше информации и мнений.

Если вы еще не используете его, рекомендуем вам попробовать: вначале это будет немного ухабистый путь, но со временем вы начнете применять правила и шаблоны программирования с учетом неизменности, и это станет таким же простым, как обычное программирование. Кроме того, это особенно хорошо работает с чистыми функциями

Модификаторы видимости и изменчивости

Есть известное правило: использовать const вместо let для объявления переменных.

Советуем использовать и этот: добавляйте модификатор readonly к каждому полю, которое вы не собираетесь модифицировать (в классах, типах, интерфейсах). Это принесет 0 стоимости — если вы не собираетесь его модифицировать, то компилятор отловит любые случайные попытки это сделать. Если позже вы решите сделать это поле доступным для записи — вы можете просто убрать модификатор readonly.

export class HealthyComponent {
   
   // do not modify this!
   private readonly stream$: Observable<UserModel>;
   
   // it's ok to modify this
   protected isInitialized: boolean = false;

   constructor() {
     // you can initialize readonly fields in the constructor
     this.stream$ = of({name: example});
   }
}

Для полей и методов ваших классов (включая компоненты, каналы, службы и директивы) используйте модификаторы видимости: private, protected, public.

Позже вы поблагодарите себя за это: когда какой-то метод field является закрытым, и в какой-то момент вы видите, что ваш класс может избавиться от него (или вам нужно его изменить), вы можете быть уверены, что никакой другой код его не использует, поэтому его можно рефакторировать.

Защищенные поля и методы видны не только для унаследованных классов, но и в шаблонах Angular, поэтому это очень веская причина использовать модификатор protected для полей и методов, которые должны быть доступны шаблону, и private для полей и методов, которые шаблону не нужны.

Мы не добавляем public модификаторы — поля и методы по умолчанию публичны без модификаторов — но это ваш личный выбор. Поскольку входы и выходы должны быть общедоступными, не добавляем к ним модификатор, чтобы не перегружать синтаксис.

Как и в случае с модификатором private, protected сообщит вам, что вы можете безопасно удалить или изменить некоторые поля или методы, не беспокоясь о шаблоне.

Любой код имеет тенденцию усложняться со временем, люди не могут запомнить все: поэтому эти небольшие модификаторы могут иметь большое значение при рефакторинге.

Тип Aliases

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

export type Model = {
  readonly field: string;
  readonly isExample?: boolean;
}

В другом файле:

import type { Model } from '@example/models';

Делая это, вы можете избежать загрузки всей библиотеки, когда вам нужно всего несколько моделей, потому что подобный импорт будет полностью удален во время компиляции TypeScript: ссылка на документацию.

Кроме того, вы избежите слияния неявных объявлений интерфейсов и будете использовать явные типы пересечений (если хотите). Других существенных различий нет, поэтому лучшим выбором является псевдоним типа.

Фирменные типы

Еще один трюк, который лучше начать использовать в начале проекта.

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

export type UUID = string & { __type: 'UUID' };

В приведенном выше примере UUID по-прежнему будет работать как строка, но у него есть часть дополнительной информации («бренд»), которая поможет отличить его от обычной строки.

Брендированные типы уберегут вас от ситуаций, когда вы передаете пароль пользователя вместо электронной почты какой-либо функции. Он поймает ошибку, когда вы отправите неправильный идентификатор — и это самый сложный случай, чтобы поймать без фирменных типов, потому что идентификаторы часто хранятся в переменных и полях с похожими именами и типами.

Мы можем сделать это с уникальным символом или без него:

// Method with additional fields:

export type UUID = string & { 
  readonly __type: 'UUID' 
};

export type DomainUUID = UUID & {
  readonly __model: 'Domain'
}

export type Domain = {
  readonly uuId: DomainUUID;
  readonly isActive: boolean;
  readonly name: string;
}

export type UserUUID = UUID & {
  readonly __model: 'User'
}

export type User = {
  readonly uuId: UserUUID;
  readonly name: string; 
}

// Method with a unique symbol:
declare const brand: unique symbol;

export type Brand<T, TBrand extends string> = T & {
  readonly [brand]: TBrand;
}

export type UUID = Brand<string, 'UUID'>;

// you can extend not only primitive types
export type DomainUUID = Brand<UUID, 'Domain'>;

export type Domain = {
  readonly uuId: DomainUUID;
  readonly isActive: boolean;
  readonly name: string;
}

export type UserUUID = Brand<UUID, 'User'>;

export type User = {
  readonly uuId: UserUUID;
  readonly name: string; 
}

Из кода видно, что здесь уникальный символ элегантно заменяет наше искусственное поле __model.

Типизированные функции

Пожалуйста, введите свои функции: их аргументы и возвращаемый результат.

Когда вы их создаете, вам очевидно, что они должны получить и что они должны вернуть. Но есть две причины для добавления типов:

  1. Для остального кода не очевидно, что они должны отправить в эту функцию и что она гарантированно вернет;
  2. Для вас через несколько месяцев это тоже будет не очевидно.

Есть разные мнения, стоит ли нам объявлять возвращаемые типы: мы рекомендуем вам объявлять их, исключая (если хотите) те, которые ничего не возвращают.

Основное преимущество не очевидно изначально, но оно будет очень полезно при рефакторинге:

  • если вы измените код своей функции и случайно измените тип возвращаемого значения, это будет перехвачено компилятором;
  • если вы намеренно меняете возвращаемый тип вашей функции, а какой-то код, использующий эту функцию, не готов к этому — он будет пойман компилятором.

В случае выводимых типов есть большие шансы, что какой-то код примет возвращаемый результат, но изменит поведение:

// before refactoring
function isAuthenticated(user: User) { 
  //...
  return true;
  // inferred type: boolean
}

if (isAuthenticated(user)) {
  markItemAsPaid();
} else {
  redirectToLogin();
}

// after refactoring
function isAuthenticated(user: User) {  
  //...
  return someApiRequest(user);
  // inferred type: Observable<boolean>
}

В этом примере компилятор не выдаст никаких ошибок: “Observable” — это объект, и if (IsAuthenticated(user)) будет работать - но он всегда будет возвращать true.

Это простой пример, но с более сложным кодом шансы на то, что это произойдет, еще выше.

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

Наследование

Используйте принцип композиции вместо наследования.

Помимо обычных проблем, связанных с наследованием, входные и выходные данные, объявленные в родительском классе, будут унаследованы, поэтому вам придется поддерживать их в дочерних классах, даже если они вам не нужны в конкретном дочернем классе.

Кроме того, в случае с компонентами каждый компонент Angular должен иметь шаблон. Здесь у вас есть 2 пути:

  • Ссылка на родительский шаблон: вы ничего не можете переопределить, поэтому у родительского шаблона будет много ответвлений для удовлетворения потребностей и особых случаев каждого ребенка;
  • Создайте собственный шаблон для ребенка: вам придется дублировать весь шаблон, и это снижает возможность повторного использования кода — первоначальную причину наследования.

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

#Angular #TypeScript #Начинающим
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу