Развернутые декораторы машинописного текста: руководство для начинающих по программированию с наддувом

Предпосылки

  1. Основы TypeScript
  2. Основные понятия объектно-ориентированного программирования (ООП)
  3. Знание JavaScript
  4. ES6 и далее

Введение

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

Декораторы обозначаются символом @, за которым следует имя декоратора, и могут быть размещены непосредственно перед объявлением, которое они украшают. Это не что иное, как функции, которые применяются к цели, и они получают информацию о цели в качестве параметров.

Давайте посмотрим на синтаксис декораторов на примере:

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

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${target.constructor.name}.${propertyKey} with arguments: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };

  return descriptor;
}

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

const calculator = new Calculator();
console.log(calculator.add(2, 3));

// Output:-
// Calling Calculator.add with arguments: [2,3]
// Result: 5
// 5

В приведенном выше примере у нас есть декоратор с именем log, который регистрирует вызовы методов и их аргументы до и после выполнения исходного метода. Декоратор log получил 3 параметра: target (в данном случае прототип класса), property key (имя метода), descriptor. Мы более подробно рассмотрим эти параметры в следующих разделах.

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

Когда вызывается метод add класса Calculator, декоратор log перехватывает вызов, регистрирует аргументы, вызывает исходный метод, регистрирует результат и возвращает его. Это позволяет нам изменять или дополнять поведение декорированного метода.

Параметры декоратора

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

  1. target:- Параметр target относится к классу или прототипу декорируемого объекта. Он представляет функцию-конструктор класса, когда декоратор применяется к объявлению класса. В случае методов экземпляра или статических методов это относится к прототипу класса. Параметр target позволяет использовать для доступа и изменения класса или его свойств во время выполнения.
  2. propertyKey:- Параметр propertyKey представляет имя декорированного метода или свойства. Для методов instance это соответствует имени декорируемого метода. Для статических методов это также включает имя класса. В случае свойств это относится к имени украшаемого свойства. Параметр propertyKey позволяет нам идентифицировать конкретный декорируемый метод или свойство и выполнять действия на его основе.
  3. descriptor:- Параметр descriptor — это объект, который содержит дескриптор свойства декорированного метода или свойства. Он включает настраиваемые свойства, такие как методы value, writable, enumerable и get/set, если применимо. descriptor позволяет нам изменить или заменить исходный метод или свойство новой реализацией. Манипулируя descriptor, мы можем изменить поведение декорированного объекта.

В функции декоратора мы можем использовать эти параметры для проверки и изменения поведения декорированного объекта.

Стоит отметить, что точная структура параметра descriptor может различаться в зависимости от типа декорируемого объекта (метод, свойство, метод доступа) и от того, является ли он членом-экземпляром или статическим членом.

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

  • Декораторы классов. Эти декораторы применяются к классам и позволяют нам изменять или расширять поведение самого класса. В качестве цели они получают конструктор класса. Мы можем использовать их для добавления дополнительных свойств или методов к классу, применения примесей или изменения метаданных класса. Вот пример:
function classDecorator(constructor: Function) {
  // Add a new property to the class
  constructor.prototype.newProperty = 'Hello, world!';
}

@classDecorator
class MyClass {
  // ...
}

const instance = new MyClass();
console.log(instance.newProperty); // Output: Hello, world
  • Декораторы методов. Эти декораторы используются для изменения или расширения поведения метода внутри класса. Они применяются к самому методу и получают три параметра: целевой класс (или прототип, если метод статический), ключ свойства и дескриптор свойства. Мы можем использовать их для выполнения действий до или после вызова метода, изменения параметров метода или возвращаемого значения или добавления дополнительной логики. Вот пример:
function methodDecorator(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${methodName} with arguments: ${args}`);
    const result = originalMethod.apply(this, args);
    console.log(`Method ${methodName} returned: ${result}`);
    return result;
  };
}

class MyClass {
  @methodDecorator
  greet(name: string) {
    return `Hello, ${name}!`;
  }
}

const instance = new MyClass();
instance.greet('John'); // Output: Calling greet with arguments: John
                        //         Method greet returned: Hello, John!
  • Декораторы параметров. Эти декораторы используются для изменения или добавления метаданных к параметру в методе или конструкторе. Они получают три параметра: целевой класс (или прототип, если параметр принадлежит методу), имя метода (или конструктора) и индекс параметра. Мы можем использовать их для выполнения действий или проверки значения параметра или добавления дополнительных метаданных. Вот пример:
function parameterDecorator(target: any, methodName: string, parameterIndex: number) {
  const args = target[methodName].toString().match(/\(([^)]*)\)/)[1].split(', ');
  console.log(`Parameter ${parameterIndex} of ${methodName} in ${target.constructor.name} is ${args[parameterIndex]}`);
}

class MyClass {
  greet(@parameterDecorator name: string) {
    console.log(`Hello, ${name}!`);
  }
}

const instance = new MyClass();
instance.greet('John'); // Output: Parameter 0 of greet in MyClass is name
                        //         Hello, John!
  • Декораторы свойств. Эти декораторы используются для изменения или добавления метаданных к свойству внутри класса. Он получает два параметра: целевой класс (или прототип, если свойство статическое) и имя свойства. Мы можем использовать их, чтобы добавить к свойству дополнительное поведение или метаданные.
  • Декораторы доступа. Декораторы доступа можно использовать для изменения или добавления метаданных в геттер или сеттер внутри класса. Декоратор доступа применяется к методу получения или установки и получает три параметра: целевой класс (или прототип, если метод доступа принадлежит статическому свойству), имя свойства и дескриптор свойства.
function accessorDecorator(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;

  descriptor.get = function () {
    console.log(`Getting ${propertyName}`);
    return originalGetter.call(this);
  };

  const originalSetter = descriptor.set;

  descriptor.set = function (value: any) {
    console.log(`Setting ${propertyName} to: ${value}`);
    originalSetter.call(this, value);
  };
}

class MyClass {
  private _myProperty: string;

  @accessorDecorator
  get myProperty(): string {
    return this._myProperty;
  }

  set myProperty(value: string) {
    this._myProperty = value;
  }
}

const instance = new MyClass();
instance.myProperty = 'Hello, world!'; // Output: Setting myProperty to: Hello, world!
console.log(instance.myProperty); // Output: Getting myProperty

В приведенном выше примере accessorDecorator применяется к методам получения и установки свойства myProperty. Декоратор изменяет поведение, добавляя журналы консоли перед получением и установкой значения свойства.

Фабрики декораторов

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

Чтобы лучше понять фабрики декораторов, давайте разберем концепцию на примере.

function validateRange(min: number, max: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      for (const arg of args) {
        if (arg < min || arg > max) {
          throw new Error(`Invalid argument ${arg} for "${propertyKey}".`);
        }
      }

      return originalMethod.apply(this, args);
    };
  };
}

class MathOperations {
  @validateRange(1, 10)
  multiply(a: number, b: number): number {
    return a * b;
  }
}

const math = new MathOperations();
console.log(math.multiply(5, 12)); // Throws an error: Invalid argument 12 for "multiply".

В этом примере у нас есть фабрика декораторов с именем validateRange. Он принимает два параметра, min и max, и возвращает функцию-декоратор. Функция декоратора применяется к методу multiply класса MathOperations с использованием синтаксиса @validateRange(1, 10).

Функция декоратора проверяет, не выходит ли какой-либо из аргументов метода за пределы указанного диапазона (min и max). Если какой-либо аргумент недействителен, он выдает ошибку. В противном случае он вызывает исходный метод.

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

Порядок выполнения декораторов

Когда несколько декораторов применяются к одной и той же цели (классу, методу, свойству и т. д.), порядок их выполнения соответствует определенному шаблону. Рассмотрим порядок выполнения на примере:

function classDecorator1(target: any) {
  console.log("Executing class decorator 1");
}

function classDecorator2(target: any) {
  console.log("Executing class decorator 2");
}

function methodDecorator1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing method decorator 1");
}

function methodDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing method decorator 2");
}

function propertyDecorator1(target: any, propertyKey: string) {
  console.log("Executing property decorator 1");
}

function propertyDecorator2(target: any, propertyKey: string) {
  console.log("Executing property decorator 2");
}

function parameterDecorator1(target: any, propertyKey: string | symbol, parameterIndex: number) {
  console.log("Executing parameter decorator 1");
}

function parameterDecorator2(target: any, propertyKey: string | symbol, parameterIndex: number) {
  console.log("Executing parameter decorator 2");
}

function accessorDecorator1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing accessor decorator 1");
}

function accessorDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing accessor decorator 2");
}

@classDecorator1
@classDecorator2
class ExampleClass {
  @propertyDecorator1
  @propertyDecorator2
  exampleProperty: string;

  constructor(
    @parameterDecorator1 parameter1: string,
    @parameterDecorator2 parameter2: number
  ) {}

  @methodDecorator1
  @methodDecorator2
  exampleMethod() {}

  @accessorDecorator1
  @accessorDecorator2
  get exampleAccessor() {
    return this.exampleProperty;
  }

  @accessorDecorator1
  @accessorDecorator2
  set exampleAccessor(value: string) {
    this.exampleProperty = value;
  }
}

// Output

// Executing property decorator 2
// Executing property decorator 1
// Executing method decorator 2
// Executing method decorator 1
// Executing accessor decorator 2
// Executing accessor decorator 1
// Executing parameter decorator 2
// Executing parameter decorator 1
// Executing class decorator 2
// Executing class decorator 1

Из приведенного выше примера мы можем наблюдать следующие два момента, касающиеся порядка выполнения декораторов:

  1. Декораторы того же типа, которые находятся ближе к цели, выполняются первыми. Например, в случае декораторов свойств @propertyDecorator2 выполняется до @propertyDecorator1. Это же правило относится ко всем типам декораторов.
  2. Порядок выполнения различных типов декораторов следующий:
    1. Декораторы свойств
    2. Декораторы методов
    3. Декораторы доступа
    4. Декораторы параметров
    5 , Декораторы классов

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

Приложения декораторов

  1. Ведение журнала.Декораторы можно использовать для добавления функции ведения журнала к функциям или методам. Например, мы можем создать декоратор log, который регистрирует входные аргументы и возвращаемое значение функции.
  2. Аутентификация. Их можно использовать для принудительной проверки подлинности перед предоставлением доступа к определенным функциям или методам. Например, мы можем создать декоратор authenticate, который проверяет статус аутентификации пользователя перед выполнением метода.
  3. Проверка: – можно использовать декораторы для проверки входных данных параметров функции. Например, мы можем создать декоратор validate, который проверяет, соответствуют ли аргументы определенным критериям.
  4. Мемоизация. Декораторы могут использоваться для реализации мемоизации, которая представляет собой метод, который кэширует результаты дорогостоящих вызовов функций для повышения производительности. Например, мы можем создать декоратор memoize, который кэширует возвращаемое значение на основе аргументов функции.
  5. Внедрение зависимостей. Декораторы можно использовать для обработки внедрения зависимостей, когда зависимости автоматически внедряются в класс или функцию. Например, мы можем создать декоратор inject, который автоматически внедряет зависимости на основе метаданных.

Заключение

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