Метапрограмування на Typescript, або як декоратори допомагають у вирішенні повсякденних задач

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всім привіт! Мене звати Віктор Мороз і останні 4 чотири роки я працюю в компанії Creatio. Ми розробляємо No-Code/Low-Code платформу, що дозволяє кінцевим користувачам кастомізувати продукт під свої вимоги. Однією із зон відповідальності моєї команди є безпосередньо API розширення системи сторонніми елементами.

Останні 5 років я активно розробляю UI з використанням фреймворку Angular і коли перед нами постала подібна задача, дуже хотілось надати нашій системі той рівень гнучкості, якого в Angular досягли використанням декораторів.

Декоратор як шаблон проєктування

Декоратор — структурний шаблон проєктування, призначений для динамічного додавання нової поведінки до об’єкта шляхом створення обгортки над вихідним об’єктом зі збереженням початкового API. Шаблон надає гнучку альтернативу методу визначення підкласів, якщо потрібно розширити функціональність.

Гарну аналогію з життя приводять автори в описі шаблону

Будь-який одяг — це аналог Декоратора. Застосовуючи Декоратор, ви не змінюєте початковий клас і не створюєте дочірніх класів. Так само з одягом: вдягаючи светра, ви не перестаєте бути собою, але отримуєте нову властивість — захист від холоду. Ви можете піти далі й одягти зверху ще один декоратор — плащ, щоб захиститися від дощу.

Зображення з ресурсу refactoring.guru

Декоратори в TypeScript

У мові TypeScript підтримка декораторів з’явилася з версії 1.5 й активно використовується в багатьох фреймворках і бібліотеках, таких як Angular, Nest.js, TypeORM, InversifyJS.

Наразі підтримуються такі види декораторів:

  1. Декоратор класу.
  2. Декоратор поля.
  3. Декоратор метода.
  4. Декоратор властивості.
  5. Декоратор параметрів.

Далі ми детально розглянемо кожен із них.

Декоратор класу

Використовується для розширення наявного класу деякими властивостями та методами.

Для прикладу можна розглянути декоратор, який робить клас недоступним для наслідування:

Декоратор поля

Декоратори поля можна використовувати для формування мета-інформації про властивості класу, а також додавання нових методів та властивостей.

Прикладом такого декоратора може бути декоратор для генерації id підчас створення екземпляра класу:

Декоратор метода

Відрізняється від декоратора поля тим, що в параметри додатково отримує дескриптор. Останній дозволяє перевизначити вихідну реалізацію і впровадити туди додаткову логіку.

У дескрипторі містяться такі властивості:

  • value
  • writable
  • enumerable
  • configurable

Дуже часто цей тип декораторів зустрічається в момент, коли необхідно додати якусь логіку до чи після виконання методу. Класичним прикладом такого використання можна назвати декоратор для логування:

Декоратор властивостей

Декоратор властивостей, в цілому, схожий на декоратор методу, різниця полягає у властивостях в дескрипторі.

У дескрипторі властивості містяться такі властивості:

  • get
  • set
  • enumerable
  • configurable

Декоратор параметра

Автономний декоратор параметра мало що може зробити. Зазвичай, він використовується для запису інформації, яку можуть використовувати інші декоратори.

Дуже часто їх створюють для реєстрації інформації, яка в подальшому використовується для трансформації значення параметра. Розробники фреймворку NestJS активно їх використовують під час створення контролерів.

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

Комбінуючи використання різних декораторів, можливо реалізовувати більш складні кейси, але для цього важливо розуміти порядок їх виконання.

Декоратори виконуються в наступному порядку:

  1. До кожного члена екземпляра застосовуються декоратори параметрів, а потім декоратори методу, властивості або поля.
  2. До кожного статичного члена застосовуються декоратори параметрів, а потім декоратори методу, властивості або поля.
  3. Для конструктора застосовуються декоратори параметрів.
  4. Для класу застосовуються декоратори класу.

Reflection

Рефлексія — це здатність мови програмування досліджувати та змінювати свої внутрішні властивості та поведінку класів. Ця можливість є у багатьох мовах програмування, таких як C#, Java та інших.

У JavaScript об’єкт Reflect з’явився порівняно недавно, його можна використати для виклику методів, конструювання об’єктів, читання та встановлення значення властивостей, маніпуляції та розширення властивостей.

Спільне використання рефлексії з декораторами дозволяє реалізовувати низку цікавих кейсів. Наприклад, за допомогою пакета reflect-metadata ми можемо отримати доступ до типу даних, який був задекларований під час написання коду для реалізації декоратора, що валідує вхідні параметри метода:

На поточний момент доступними є три типи design-time анотації:

  • design:type — тип властивості
  • design:paramtypes — тип параметра метода
  • design:returntype — повертаємий методом тип данных

Результатом цих трьох типів є функції-конструктори (такі як String та Number). Значення визначається за наступними правилами:

  • number -> Number
  • string — String
  • boolean — Boolean
  • void/null/never — undefined
  • Array/Tuple — Array
  • Class — конструктор класу
  • Enum — Number, коли перечислення числове, або Object
  • Function — Function

Використання декораторів в Creatio

Платформа Creatio передбачає можливість розширення набору системних елементів згідно з вимогами кожного користувача. Щоб спростити процес реєстрації користувальницьких елементів, в системі можна використовувати декоратори, не знаючи, куди саме декоровані елементи потрібно реєструвати й як надалі система буде їх використовувати.

Однією з таких задач може бути додавання нового візуального елемента для використання на сторінці.

Щоб система могла взаємодіяти з користувальницьким елементом, їй потрібно знати, які вхідні властивості та вихідні події підтримуються елементом.

Для реєстрації цієї інформації ми використовуємо декоратор CrtViewElement, CrtInput, CrtOutput.

Декоратори властивостей CrtInput та CrtOutput формують метаопис візуального елемента. Реалізація обох декораторів однакова, нижче наведено приклад декоратора для вхідної властивості.

Сформована інформація використовується декоратором CrtViewElement.

Візуальні елементи об’єднуються в модулі:

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

Висновок

Використання декораторів дозволило підвищити гнучкість системи і спростити реєстрацію елементів для використання у системі. Код використання декораторів виглядає лаконічно та дозволяє розробникам додавати метадані необхідних елементів декларативно, що дозволяє зосередитися на реалізації бізнес задачі, а не на питаннях інтеграції рішень в хост-систему.

Загалом кейсів використання декораторів дуже багато, наприклад:

  • Додавання логіки до/після виклику метода.
  • Трансформація параметрів перед викликом метода.
  • Додавання додаткових методів чи властивостей.
  • Валідація типів даних під час виконання.
  • Dependency Injection.

Якщо у вас є ідеї, в яких ще повсякденних задачах можливо та доцільно було б використати декоратори, поділіться, будь ласка, у коментарях.

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось24
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А взагалі цікавий приклад no-code платформи — вставити sql контрол, та ще навести як приклад безглуздий CTE, який не всі девелопери знають, не кажучи вже про менеджерів, які повинні «писати» логіку :)

Так як ТС зник, напевно піднявши KPI перед переглядом з/п, то задам питання фронт-енд девелоперам, які тут відписалися. «Декоратори», які тут навели, змінюють поведінку класу, і виходить, що коли я задекорую пропертю, метод, клас декораторами, то там де цей клас використовується має заново все перетестуватися? Тобто це не декоратори, в загальному розумінні як шаблон проєктування, де перетестуватися повиннен тільки задекорований інстанс, а якийсь цукор, який додає ентропію в проект?

В комментарии призывается Тимур Шемсединов

Вибачте, а навіщо тут другий абзац

Декоратор як шаблон проєктування

? Яке це має відношення до того, що ви описали в статті?

Спасибо за статью!
Вы не опасаетесь широко использовать TypeScript декораторы, учитывая сильные отличия от декораторов JavaScript, которые сейчас, если я не ошибаюсь, находятся на Stage 3?
Как вы думаете, что будет когда JavaScript декораторы станут стандартом?

Так JavaScript декоратори дійсно достатньо сильно відрізняються від TS, але за рахунок того что TS мова компілюється, то у її розробників залишається можливість додати підтримку декораторів як в новому вигляді, так і зберегти зворотню сумістність за поточним форматом.
На мою думку коли JavaScript декоратори стануть стандартом їх почнуть активно застосовувати, та з часом відбудеться відмова від поточного варінту, проте зараз складно сказати коли саме це відбудеться, адже для досягненя stage 3 знадобилося 5 років і весь цей цас концепція переживала досить суттєві зміни.

На мою думку коли JavaScript декоратори стануть стандартом їх почнуть активно застосовувати,

Их уже давно никто не мешает использовать, наличие нативной поддержки непринципиально.

Згадав 100500 мемів про warning’и)

А згодом ми отримуємо Decorator Driven Development як, наприклад, в Nest.js ))))) Я не стверджую що це погано, просто мені це наприклад не подобається візуально і ще тим що з великою кількістю декораторів можна легко заблудитися в контексті того що відбувається.

Щодо статті, розкладено гарно і зрозуміло, але хотілось би побачити більше прикладів з реального життя. Дякую!

Приклад із NestJS показує, що у людей запит на подібний фреймворк дуже великий — за останній рік кількість його скачувань зросла із 1 млн до 1.5 млн за тиждень. Через це йому пробачають і «Decorator Driven Development», і відсутність системи розширень (плагінів), і Dependency Injection по назві класів, а не по референсу на ці класи (в останній восьмій версії автор NestJS спробував це виправити, але не знаю на скільки вдало), і т.д.

Та в тому то і справа що це виглядає так наче вибору іншого нема от і беремо Nest. Ну і не можна відмовити їм у маркетингу і в досить гарній документації.

Вибір є (наприклад мій фреймворк Ditsmod), але звичайно ж людей цікавлять в першу чергу популярні фреймворки, а не якийсь ноунейм.

На Dismod я поверхнево дивився. Виглядає міцно і цікаво, але я поки не зрозумів що воно мені принципово змінить. Наприклад та ж сама Metarhia робить прям інший підхід до побудови застосунків. Але хочеться чогось накшталт Symfony/ApiPlatform або Apiato. Але покищо маємо шо маємо.

я поки не зрозумів що воно мені принципово змінить

Ну щоб аж принципово, то мабуть це про можливість писати розширення (плагіни), та можливість підключати чи відключати модулі без необхідності перезавантажувати NodeJS процес. Але, звичайно ж, Ditsmod на продакт поки що не годиться. Хоча я використовую його на mblog.dev

Metarhia робить прям інший підхід до побудови застосунків

Не напишете у двох словах про що йдеться?

Але хочеться чогось накшталт Symfony/ApiPlatform або Apiato.

На PHP я досить мало писав, не в курсі що тут мається на увазі.

В Metarhia (це платформа взагалі, але я просто так використовую цю назву бо так зрозуміліше) теж є hot reload, автоматичний резолвінг нових модулів, архітектура, генерація api-client, можливість використовувати застосунок через HTTP або WebSocket не пишучи для цього взагалі нічого ручками. Ну то я так, теж поверхнево дивився, але воно не схоже на те що є.

Apiato — Vertical Slicing architecture з можливістю розбивати застосунок на контейнери і сервіси.

ApiPlatform — ентерпрайзний комбайн теж з генерацією api-client кучею підкапотних генерацій (CRUD) і іншої магії. Так в двох реченнях не напишеш.

не пишучи для цього взагалі нічого ручками.

Вам мабуть і loopback 4 сподобається. Це вже обкатаний фреймворк від інженерів IBM. Хоча він повільніший навіть за ExpressJS.

А от loopback мені рік тому не сподобався. Дуже він хитрозроблений мені. Але треба освіжити погляд ))) З приводу швидкості — того що демонструє Express здебільшого вистачає на все. Але дійсно залежить від потреб і задач.

Ми у себе використовуємо покищо fastify, але багато чого поверх нього написали.

Стосовно Metarhia, то писати фреймворки не на TypeScript, а на JavaScript було нормою десь до 2014 року. Зараз я розглядаю це як критичній мінус, через який я точно не буду їх використовувати.

Щодо typescript, то у них точно було в планах написати під все d.ts, а далі вже і писати можливо нові модулі на TS. Хоча Шемседінов критично ставиться до використання TS де не попало. Так шо не зрозуміло до чого вони в цілому прийдуть.

було нормою десь до 2014 року.

до 12 :) Ну да, только в конце 12 появилось это чудо, и за год все на него прям побежали, а то жить не могли без выписывания километровых дженриков на каждый чих)

Визначення декоратора не зрозуміле, що таке вихідний об’єкт? З вікіпедії In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. Тут взагалі немає поняття вихідного об’єкту, а лише клас та об’єкти створені цим класом. Можливо автор плутає поняття класу та об’єкту?

JavaScript — это объектно-ориентированный язык, основанный на прототипировании, а не на классах. Прототипно-ориентированный язык, например JavaScript, не реализует данное различие: он имеет только объекты. Языки, основанные на прототипах, имеют понятие прототипа объекта — это объект, используемый в качестве шаблона, с целью получить изначальные свойства для нового объекта. Любой объект может иметь собственные свойства, присвоенные либо во время создания, либо во время выполнения. В дополнение, любой объект может быть указан в качестве прототипа для другого объекта, это позволит второму объекту использовать свойства первого.
P.s. Мне тоже стало интересно узнать ответ, просто скопировала его сюда.

Об’єкти породжуються функціями-конструкторами (це не єдиний спосіб створення об’єкту, але в контексті статті саме він). Тобто клас це синтаксичний цукор навколо функції-конструктора. З документації TS декораторів «The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition.» — це означає якщо прибрати синтаксичний цукор то декоратор буде застосований до функції конструктора. Досі не зрозміло що таке вихідний об’єкт. Якщо це прототип то варто так і написати.

Хоча у статті говориться про «велику кількість кейсів використання декораторів», але, на скільки я знаю, усі згадані тут фреймворки (Angular, Nest.js, TypeORM, InversifyJS) використовують декоратори майже виключно для статичного записування та читання метаданих. Дуже рідко декоратори використовуються для безпосередньої композиції поведінки коду, за яким вони закріплені (наприклад, для заборони наслідування класу і т.п.).

Раніше анотації з метаданими приходилось писати під класами, а з появою декораторів це можна робити прямо перед тим кодом, для якого ці анотації призначаються. Ця особливість покращує читабельність коду, через це, мабуть у 80-99% випадків, використовується саме вона, а не композиція поведінки коду.

Так философия ооп же усложнить разработку :)

Підписатись на коментарі