Pull to refresh

Избавляемся от boilerplate в Angular: использование TypeScript-декораторов

Level of difficultyEasy
Reading time9 min
Views521

Каждый Angular-разработчик хотя бы раз задумывался: "Почему я пишу так много однотипного кода?". Инъекция зависимостей, повторяющиеся методы логирования, одинаковая обработка событий — всё это напоминает вечную гонку с шаблонным кодом (boilerplate). Однако в арсенале Angular есть мощное средство для упрощения задач и автоматизации повторяющихся действий — TypeScript-декораторы. Декораторы — это быстрый способ добавить унифицированную функциональность к кодовой базе, сделав её чище, понятнее и поддерживаемой. В этой статье мы разберём, как с помощью декораторов избавляться от однообразных повторов, одновременно привнося гибкость и снижение количества ошибок.

Знакомство с TypeScript-декораторами

Декораторы — это функции, которые применяются к классам, методам, свойствам или параметрам. Они позволяют модифицировать поведение объекта или его элементов без изменения их исходного кода. Декораторы доступны в TypeScript благодаря стандарту ES7. Фактически, Angular уже активно использует декораторы:  @Component,  @Injectable,  @Input и т. д.

Функция декораторов

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

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

2.Автоматизировать повседневные задачи:

  • Логирование.

  • Валидация.

  • Кэширование.

  • Управление зависимостями (Dependency Injection).

3.Добавлять метаданные — например, регистрацию классов или методов.

4.Упрощать работу с API, освобождая разработчиков от ручных вызовов.

Пример задачи: Допустим, вы хотите логировать любые методы, вызываемые в приложении. Вместо того чтобы добавлять console.log() в каждый метод, можно использовать метод-декоратор:

function LogMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Вызван метод: ${propertyKey}, аргументы: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class Example {
  @LogMethod
  doSomething(param: string) {
    console.log("Делаю что-то важное...");
  }
}

const instance = new Example();
instance.doSomething("тест");
// Консоль:
// Вызван метод: doSomething, аргументы: ["тест"]
// Делаю что-то важное...

Как работают декораторы?

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

Типы декораторов:

  1. Class Decorator (Декораторы классов): Обрабатывают или модифицируют сам класс.

  2. Property Decorator (Декораторы свойств): Изменяют или добавляют функциональность для полей/свойств класса.

  3. Method Decorator (Декораторы методов): Позволяют модифицировать поведение метода.

  4. Parameter Decorator (Декораторы параметров): Используются для обработки параметров методов или конструктора.

Логика работы декораторов

Вызов декоратора. Когда TypeScript компилирует класс, он вызывает декораторы сразу после объявления.

Аргументы декоратора зависят от его типа.

Тип декоратора

Аргументы

Пример использования

Class Decorator

Конструктор класса

@MyDecorator

Property Decorator

Объект класса и имя свойства

@MyDecorator propertyName: string

Method Decorator

Объект, имя метода, дескриптор свойства

@MyDecorator methodName()

Parameter Decorator

Объект, имя метода, индекс параметра

@MyDecorator(paramName: string)

Результат выполнения. Декораторы вызываются для обработки целевого элемента и могут вернуть изменённый объект.

Преимущества декораторов

  1. Снижение объемов шаблонного кода.

  2. Централизованное управление функциональностью.

  3. Облегчение добавления повторяющихся задач в приложении.

  4. Более читаемый и поддерживаемый код.

Задача №1: Логирование вызовов методов (Method Decorator)

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

Реализация

Создадим метод-декоратор @LogMethod, который будет логировать имя метода и переданные аргументы:

function LogMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Вызван метод: ${propertyKey} с аргументами: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Метод ${propertyKey} вернул: ${JSON.stringify(result)}`);
    return result;
  };

  return descriptor;
}

Использование:

class Calculator {
  @LogMethod
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(5, 7);

Результат в консоли:

Вызван метод: add с аргументами: [5,7]
Метод add вернул: 12

Это избавляет нас от необходимости везде прописывать вызовы логирования вручную.

Задача №2: Преобразование и Validations (Property Decorator)

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

Реализация

Создадим декораторы @Capitalize и @Validate.

Декоратор Capitalize: автоматически приводит строки к заглавной букве.

function Capitalize(target: Object, propertyKey: string) {
  let value: string;

  const getter = () => value;
  const setter = (newValue: string) => {
    value = newValue.charAt(0).toUpperCase() + newValue.slice(1);
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

Пример использования:

class User {
  @Capitalize
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const user = new User("john");
console.log(user.name); // "John"

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

function ValidatePositive(target: Object, propertyKey: string) {
  let value: number;

  const getter = () => value;
  const setter = (newValue: number) => {
    if (newValue < 0) {
      throw new Error(`Значение ${propertyKey} должно быть положительным`);
    }
    value = newValue;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

Пример использования:

class Product {
  @ValidatePositive
  price: number;

  constructor(price: number) {
    this.price = price;
  }
}

const product = new Product(50);
product.price = -10; // Ошибка: Значение price должно быть положительным

Задача №3: Автоматизация DI в сервисах (Class Decorator)

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

Реализация

Создадим декоратор @Cacheable, который будет автоматически кэшировать результаты работы метода. Код декоратора:

const methodCache = new Map();

function Cacheable(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (methodCache.has(key)) {
      console.log(`Возвращено из кэша: ${propertyKey}(${key})`);
      return methodCache.get(key);
    }

    const result = originalMethod.apply(this, args);
    methodCache.set(key, result);
    return result;
  };

  return descriptor;
}

Использование:

class ApiService {
  @Cacheable
  fetchData(url: string) {
    console.log(`Запрос данных с ${url}`);
    return `Данные с ${url}`;
  }
}

const apiService = new ApiService();
console.log(apiService.fetchData("https://example.com/api")); // "Запрос данных..."
console.log(apiService.fetchData("https://example.com/api")); // "Возвращено из кэша..."

Теперь любые методы, маркированные @Cacheable, получат встроенный кэш.

Задача №4: Улучшение работы с Angular-компонентами (Decorator для компонентного шаблона)

Создадим декоратор @Autounsubscribe, который автоматизирует отписку от подписок (unsubscribe) для всех компонентов, где это нужно. Реализация:

function AutoUnsubscribe(constructor: Function) {
  const originalOnDestroy = constructor.prototype.ngOnDestroy;

  constructor.prototype.ngOnDestroy = function () {
    for (const prop in this) {
      if (this[prop] && typeof this[prop].unsubscribe === "function") {
        this[prop].unsubscribe();
      }
    }
    if (originalOnDestroy) {
      originalOnDestroy.apply(this);
    }
  };
}

Использование:

@AutoUnsubscribe
@Component({ selector: 'app-example', template: '' })
export class ExampleComponent implements OnDestroy {
  subscription = this.someService.data$.subscribe();

  constructor(private someService: SomeService) {}

  ngOnDestroy() {
    console.log("Component destroyed");
  }
}

С этим декоратором можно автоматизировать отписки, защититься от утечки памяти и упростить разработку.

Минусы декораторов и когда их лучше избегать

Несмотря на всю мощь и удобство декораторов, они не лишены недостатков. Более того, бывают сценарии, в которых их использование может привести к проблемам, усложнению кода или снижению производительности.

1. Неустоявшаяся стандартность

Проблема:

Декораторы до сих пор находятся на стадии Stage 2 в спецификации ECMAScript. Это означает, что их поведение может измениться, а сами декораторы могут быть реализованы иначе в будущих версиях JavaScript. Поэтому существует риск, что код, созданный сейчас с использованием декораторов, потребуется переписывать в будущем.

Последствия:

  • Полная зависимость от реализации декораторов в TypeScript.

  • Не все JavaScript-локальные стандарты и инструменты их поддерживают (например, некоторые библиотеки или среды интерпретации).

2. Ухудшение читаемости сложного кода

Проблема:

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

Пример:

@Auth('admin')
@TrackUsage('createUser')
@Retry(3)
class UserService {
  createUser(user: User) {
    // Реализация
  }
}

Разработчику будет неочевидно:

  • Что делает каждый из декораторов.

  • Как они взаимодействуют между собой.

  • На какой стадии выполнения применяется декоратор.

Когда это критично:

  1. Проекты с большим числом участников, где важна простота понимания кода.

  2. Новички, которые будут работать с проектом, могут потратить больше времени на изучение логики.

3. Избыточная магия (overhead)

Проблема:

Декораторы зачастую накладывают определённую "магическую" функциональность, которая может создавать неожиданные эффекты. Например, изменения через Object.defineProperty или перезапись методов затрудняют отладку и понимание кода.

Последствия:

  • Трудности при отладке: поведение может зависеть от последовательности выполнения декораторов.

  • Непредсказуемые баги: миграция на новую версию TypeScript или Angular может нарушить работу декораторов.

4. Сложности в тестировании

Проблема:

Тестирующие инструменты могут испытывать затруднения при интерпретации кода с большим количеством скрытой логики в декораторах. Декораторы могут добавлять прокси-контроль, а "подкапотные" изменения кода будут сложны для симуляции в тестах.

Пример:

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

5. Сложность отладки

Проблема:

Когда декораторы не просто добавляют метаданные, а изменяют методы/свойства, результат становится менее предсказуемым. Отладочные инструменты (например, дебаггер) иногда показывают "неизменённый" код, а не его реальное поведение.

Пример:

Если вы смотрите через дебаггер на модифицированный метод, трудно понять, где и как был применён декоратор.

Когда НЕ стоит использовать декораторы?

1. Маленькие проекты

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

2. Проекты с низким порогом входа

Если проект рассчитан на разработчиков-новичков или тех, кто не знаком с TypeScript-декораторами, их использование может стать камнем преткновения. Для таких команд лучше не усложнять код.

3. Опасные изменения состояния

Если декоратор изменяет существующий метод/свойство, это может привести к нестабильным изменениям поведения, особенно если декораторы накладываются друг на друга.

Советы по использованию декораторов

  1. Используйте их для обработки "скучной" логики. Декораторы подходят для задач вроде логирования, авторизации, кеширования — там, где важно сократить шаблонный код.

  2. Не усложняйте код абстракциями. Если задача решается с помощью двух строк кода без декораторов, лучше обойтись без них.

  3. Документируйте поведение декораторов. Обязательно объясняйте в комментариях или документации, что выполняет декоратор, чтобы избежать недоразумений.

  4. Проверяйте производительность. Если декоратор выполняет интенсивные задачи, убедитесь, что он не вызывает ощутимого влияния на скорость работы приложения.

  5. Не добавляйте бизнес-логику в декораторы. Они должны быть легковесными и отвечать за инфраструктурные задачи (например, валидация, логирование), а не за прямую обработку данных.

Заключение

Декораторы в TypeScript — это мощный инструмент, но как и любой инструмент, они требуют разумного подхода. Их избыточное использование, особенно в простых проектах, только запутает код и усложнит работу. Важно понимать, что декораторы подходят для управления сквозной функциональностью (например, логирование, авторизация, валидация), но не для всего подряд. Помните: чем проще и понятнее код, тем лучше для вас и вашей команды. Декораторы — это инструмент, а не волшебное решение всех проблем! 😊

Tags:
Hubs:
Total votes 5: ↑3 and ↓2+3
Comments0

Articles