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

Декораторы в JavaScript и Typescript

Идея декораторов существует уже давно (например, в Java они существуют уже много лет), и, похоже, время этой идеи пришло. Что касается поддержки JavaScript, предложение TC39 находится на этапе 3, что означает, что оно полностью разработано, и никаких изменений не предвидится без данных о реализации или использовании, поэтому вскоре оно может перейти на этап 4, что будет означать включение в будущем. в стандарте языка. Однако на сегодняшний день ни один браузер не поддерживает эту функцию, как сообщает www.caniuse.com:

С TypeScript проблема в другом; язык уже поддерживает предложение, поэтому мы можем их использовать. Конечно, когда мы получим официальный стандарт декораторов, могут быть некоторые изменения, но не будем забегать вперед; у нас может быть 100% совместимость. Однако в настоящее время эта функция классифицируется как экспериментальная, поэтому вы должны добавить experimentalDecorators опцию компилятора в свой tsconfig.json файл:

{
  "compilerOptions": {
    ...
    "experimentalDecorators": true
    ...
  }
}

До сих пор мы толком не объяснили, что такое декораторы; давайте исправим это прямо сейчас.

Что такое декораторы?

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

Декоратор позволяет вам аннотировать (модифицировать) классы и их члены, скорее всего, внося в них некоторые изменения. Применение декоратора к методу эквивалентно применению HOF к функции для создания нового (модифицированного) метода.

Существует пять видов декораторов, которые работают с разными частями класса, позволяя нам наблюдать за ними или модифицировать их:

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

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

class Person {
  name: string = "";
  birthYear: number = 0;

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

  asString() {
    return `I'm ${this.name}, ${this.age(new Date().getFullYear())} year(s) old.`
  }

  age(currentYear: number) {
    return currentYear - this.birthYear; // not very precise...
  }
}

Мы определяем класс Person с двумя атрибутами (имя человека и год рождения) и парой методов для возврата строки, описывающей человека, и для (приблизительно...) вычисления его возраста через какой-то год. Давайте попробуем метод asString(), который мы будем использовать в других примерах.

const myself = new Person("John Doe", 2001)
console.log(myself.asString());
// I'm John Doe, 22 year(s) old.

Наш код работает, так что давайте начнем украшать его методы.

Декоратор логов

В нашей предыдущей статье мы написали HOF с логированием на JavaScript, который добавил логирование при вызове функции и при ее возврате, как обычно, так и с выдачей ошибки.

const addLogging =
  (fn) =>
  (...args) => {
    console.log("Enter", fn.name, ...args);
    try {
      const toReturn = fn(...args);
      console.log("Exit ", fn.name, toReturn);
      return toReturn;
    } catch (err) {
      console.log("Error", fn.name, err);
      throw err;
    }
  };

Давайте превратим этот код в декоратор @withLogging(). Декоратор — это функция, которая получает три аргумента: target объект, с которым она будет работать, propertyKey имя декоративного метода и descriptor дополнительную информацию о свойстве. В нашем случае мы хотим заменить исходный метод новым, который будет включать ведение журнала, как показано выше.

function addLogging(
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;                 // (1)
  descriptor.value = function (...args: any[]) {           // (2)
    console.log("Enter", originalMethod.name, ...args);    // (3)
    try {
      const toReturn = originalMethod.call(this, ...args); // (4)
      console.log("Exit ", originalMethod.name, toReturn);
      return toReturn;
    } catch (err) {
      console.log("Error", originalMethod.name, err);
      throw err;
    }
  };
}

Мы получаем исходный метод, используя descriptor.value в (1), и заменяем его собственной версией в (2). Вместо регистрации fn.name мы используем originalMethod.name в (3). То, как мы вызываем метод, также меняется: вместо fn(...args) мы должны использовать .call() в (4), чтобы обеспечить правильный контекст; вместо этого мы могли бы использовать .apply().

Давайте применим этот декоратор к нашим методам asString() и age().

class Person {
  .
  .
  .
  @addLogging
  asString(): string { ... }

  @addLogging
  age(currentYear: number): number { ... }
}

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

const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Enter asString
// Enter age 2023
// Exit  age 22
// Exit  asString I'm John Doe, 22 year(s) old.
// I'm John Doe, 22 year(s) old.

Большой! Лесозаготовительные работы; у нас также есть второй декоратор, который мы будем использовать для тайминга.

Повтор сеанса для разработчиков

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

Удачной отладки! Попробуйте использовать OpenReplay сегодня.

Декоратор времени

Вернемся к нашему хронометражу HOF из предыдущей статьи:

const addTiming = (fn) => (...args) => {
  let start = performance.now();  
  try {
    const toReturn = fn(...args); 
    console.log("Normal exit", fn.name, performance.now()-start, "ms");
    return toReturn;
  } catch (thrownError) {         
    console.log("Exception thrown", fn.name, performance.now()-start, "ms");
    throw thrownError;
  }
};

Мы можем применить подобное преобразование для создания декоратора @addTiming.

function addTiming(
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    let start = performance.now();
    try {
      const toReturn = originalMethod.apply(this, args);
      console.log("Normal exit", originalMethod.name, performance.now() - start, "ms");
      return toReturn;
    } catch (err) {
      console.log("Exception thrown", originalMethod.name, performance.now() - start, "ms");
      throw err;
    }
  };
}

Использовать этот декоратор так же просто.

class Person {
  .
  .
  .
  @addTiming
  asString(): string { ... }

  @addTiming
  age(currentYear: number): number { ... }
}

const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Normal exit age 0.0205250084400177 ms
// Normal exit asString 1.1684779822826385 ms
// I'm John Doe, 22 year(s) old.

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

Составление декораторов

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

class Person {
  .
  .
  .
  @addLogging
  @addTiming
  age(currentYear: number): number {
    return currentYear - this.birthYear; // not very precise...
  }
}

const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Enter  2023
// Normal exit age 0.01621299982070923 ms
// Exit   22
// I'm John Doe, 22 year(s) old.

В данном случае мы применяем @addLogging к результату применения @addTiming к методу age() — вывод подтверждает этот результат. Имейте в виду, что последний из перечисленных декораторов применяется первым; это как если бы работая с функциями мы написали что-то вроде addLogging(addTiming(age)).

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

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

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

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

function addLogging2(
  when: { onEnter?: boolean, onReturn?: boolean, onError?: boolean } = {
    onEnter: true,
    onReturn: true,
    onError: true,
  }
) {
  return function (
    target: Object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      if (when?.onEnter) {
        console.log("Enter", originalMethod.name, ...args);
      }
      try {
        const toReturn = originalMethod.apply(this, args);
        if (when?.onReturn) {
          console.log("Exit ", originalMethod.name, toReturn);
        }
        return toReturn;
      } catch (err) {
        if (when?.onError) {
          console.log("Error", originalMethod.name, err);
        }
        throw err;
      }
    };
  };
}

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

class Person {
  .
  .
  .
  @addLogging2({ onEnter: true })
  asString(): string { ... }

  @addLogging2()
  age(currentYear: number): number { ... }
}

Мы хотим, чтобы asString() регистрировался только при входе, но разрешим все журналы для age(). - вывод подтверждает, что наши декораторы работают так, как ожидалось; нет журналов «Выход» для asString().

const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Enter asString
// Enter age 2023
// Exit  age 22
// I'm John Doe, 22 year(s) old.

Мы сделали это!

Заключение

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

Первоначально опубликовано на https://blog.openreplay.com.