Вы, наверное, уже наслышаны о такой странной вещи для js/ts разработчика, как декораторы. Вообще это паттерн проектирования, который можно использовать в любом языке. Но некоторые языки программирования, например python, притащили этот паттерн в свой синтаксис, чем вызвали противоречивую реакцию среди разработчиков. TypeScript уже зарекомендовал себя как сорока, тянущая удачный синтаксис из разных языков программирования. Но пойдут ли ему на пользу декораторы?


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

Давайте разберемся для чего нам вообще нужны декораторы:

  • Изменить поведение функции, чтобы никто не разобрался
  • Проверить состояние окружения перед исполнением функции
  • Провести отложенную инициализацию
  • Логгирование/трассировка
  • Аккуратное расширение чужого кода
  • [Тут ваш вариант]

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

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

Для сегодняшнего рецепта нужно обновить tsconfig.json:

// tsconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

В общем, это все. Можно делать магию с декораторами!
Но нельзя просто так взять и написать трассировку с одними только декораторами. Давайте напишем функцию трассировки.

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

// Начнем с переключателя. Мы же не хотим глушить trace
// на продакшене руками каждый раз
class TraceExperiment{
  private isTrace: boolean = true;

  // Нам интересно видеть в трасировке и входные данные, и результат
  // исполнения функции. Безусловно, это не весь стек вызова сразу, но
  // стоит знать меру, насколько сильно забивать консоль
  function traceMe(functionName: string, parameters: string, result: string): void {
    if (this.isTrace)
      console.log( new Date().toString() +
        ` TRACE:  ${functionName}(${parameters}) => ${result}`);
  }

  //Ну и конечно же вставлять без декоратора придется примерно так
  function doWork(param_one: any, param_two: any): any{
    let result:any;
    // тут происходит действительно полезная работа
    this.traceMe(arguments.callee.name,arguments.join(),result);
  }

Нам пришлось дергать тяжелый argument два раза, а еще придется следить, чтобы вывод функции никто не отвел от трассировщика. Сложно и грустно. Особенно если это потом придется долго обслуживать и поддерживать. Я уверен, что такие решения писал или поддерживал каждый.

Вот, у нас есть восхитительный логгер и какое-то неправильное, тяжелое его использование.

Если вам хотелось бы:

  1. Включать трассировщик в функцию одной строкой
  2. Иметь гарантированное получение аргументов и результата выполнения функции
  3. Добиться максимальной отказоустойчивости
  4. Иметь возможность передавать отдельные парaметры при декларации


Вот тут-то самое время использовать декораторы, которые дает нам typescript, и которые в скором будущем будут доступны в javascript из коробки:

  @Logger.trace("MODULE1")
  function doWork(param_one: any, param_two: any): any {
    // тут происходит действительно полезная работа
  }


Да, настолько просто. Это пример использования фабрики декораторов, почти как из новой версии нашего web sdk. Параметром в скобках я пользуюсь для группировки вывода по модулям. Как такая фабрика выглядит:

// Фабрика декоратор
public static trace(category:string)
{
  // в этой части мы можем предварительно обработать условия инициализации
  //
  // Возвращаем декораторов
  // в нем нас интересует
  // propertyKey - имя вызываемой функции
  // descriptor - дескриптор вызываемой функции
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    return {
      // Обернем вызов исходной функции. Наша обертка будет
      // вызываться каждый раз, когда мы будем вызывать исходную функцию
      value: function (...args: any[]) {
        // соберем вместе параметры вызываемой функции
        let params = "";
        // Каждый раз проверяем, что у нас нет циклических ссылок
        try {
          params = args.map(a => JSON.stringify(a)).join();
        } catch (e) {
          params = "[circular structure]";
        }
        // выполним декорируемую фунцию, чтобы вернуть ее
        // результат, а за одно его залогировать
        let result = descriptor.value.apply(this, args);
        let r;
        // Каждый раз проверяем, что у нас нет циклических ссылок
        try {
          r = JSON.stringify(result);
        } catch (e) {
          r = "[circular structure]";
        }
        // Отдаем все это нашему логеру
        Logger.traceMe(propertyKey, params, r);
        //Возвращаем результат исходной функции, чтобы ничего не сломать
        return result;
      }
    };
  }
}


После преобразования, код декорируемой функции будет обернут в конструктор декораторов:

  __decorate([Logger_1.LogManager.d_trace(Logger_1.LogCategory.RTC)], SeriousModule.prototype, "doWork", null);


Даже в преобразованном виде код выглядит аккуратно и приятно. Как можно видеть, Python показал нам, что декораторы могут быть не анти-паттерном разработки, а полезным инструментом для борьбы со сложностью. Они повышают читаемость кода и позволяют «выносить» поведение из функции, при этом не усложняя читаемость. Но, как и любой инструмент, декораторы хорошо работают только по назначению. Если увлечься и начать использовать их направо и налево, то легко получается нечитаемый код с десятком декораторов у функций. Надеюсь, с JavaScript и TypeScript такое не случится.

В качестве иллюстрации использован фрагмент обложки книги «Сам себе декоратор» Махмутовой Х. И. «Издательство „Эксмо“

Комментарии (6)


  1. Usikoff
    14.03.2016 09:49

    Небольшое замечание: много опечаток по тексту.


  1. sanex3339
    14.03.2016 11:12

    Декораторы — отличная штука. Ну и стоит посмотреть, как отлично они используются на примере Angular2.


  1. vaniaPooh
    14.03.2016 11:23

    На декораторах (аннотациях) можно писать еще много чего полезного. Например, в Java на них построены популярные фреймворки тестирования JUnit и TestNG и фрейворк разработки больших приложений Spring Framework. Поскольку все больше бекенда переезжает на фронтенд, то и методы разработки больших проектов становятся все более востребованными. Angular 2 уже содержит фреймворк dependency injection и сдается мне, что через год-два использование таких штук будет уже стандартом написания больших фронтенд-приложений.


    1. Googolplex
      14.03.2016 12:13

      Аннотации — это немного из другой оперы. Аннотации — это просто статические метаданные, которые можно проанализировать через отражение. А декораторы — это активные преобразователи функций. Декоратор оборачивает одну функцию в другую. Вероятно, в каком-то виде они могут добавлять метаданные по типу аннотаций, но это зависит от языка, я думаю. С точки зрения Java декораторы ближе к AOP-фреймворкам, чем к аннотациям. Просто так получилось, что синтаксис у них похожий.


  1. wickedweasel
    14.03.2016 12:05

    по поводу картинки:

    decorate = decorate(decorate)


  1. BearOff
    14.03.2016 12:35

    // Каждый раз проверяем, что у нас нет циклических ссылок

    Там дальше, вероятно, надо использовать r, а не result.