Вы, наверное, уже наслыщаны о такой странной вещи для 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 два раза, а еще придется следить, чтобы вывод функции никто не отвел от трассировщика. Сложно и грустно. Особенно если это потом придется долго обслуживать и поддерживать. Я уверен, что такие решения писал или поддерживал каждый.
Вот, у нас есть восхитительный логгер и какое-то неправильное, тяжелое его использование.
Если вам хотелось бы:
- Включать трассировщик в функцию одной строкой
- Иметь гарантированное получение аргументов и результата выполнения функции
- Добиться максимальной отказоустойчивости
- Иметь возможность передавать отдельные парметры при декларации
Вот тут-то самое время использовать декораторы, которые дает нам 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, result);
//Возвращаем результат исходной функции, чтобы ничего не сломать
return result;
}
};
}
}
После преобразования, код декорируемой фунции будет обернут в конструктор декораторов:
__decorate([Logger_1.LogManager.d_trace(Logger_1.LogCategory.RTC)], SeriousModule.prototype, "doWork", null);
Даже в преобразованном виде код выглядит аккуратно и приятно. Как можно видеть, Python показал нам, что декоработы могут быть не анти-паттерном разработки, а полезным инструментом для борьбы со сложностью. Они повышают читаемость кода и позволяеют «выносить» поведение из функции, при этом не усложняя читаемость. Но, как и любой инструмент, декораторы хорошо работают только по назначению. Если увлечься и начать использовать их направо и налево, то легко получается нечитаемый код с десятком декораторов у функций. Надеюсь, с JavaScript и TypeScript такое не случится.
В качестве иллюстрации использован фрагмент обложки книги «Сам себе декоратор» Махмутовой Х. И. «Издательство „Эксмо“
Автор: Voximplant