Аналіз типів з допомогою Proxy

В процесі опису чергового набору тестів для модуля Node.js зловив себе на думці "знову перевірка типів". Кожен параметр методу класу, кожне властивість встановлюється з допомогою сетера треба перевіряти. Можна звичайно просто забити або доповнювати всі кодом реалізує перевірки або спробувати описати всі декораторами. Але в цей раз зробимо трохи інакше.
Трохи емоцій і солодощі


Прихід специфікації ES6 і реалізації її в різних движка дарує нам багато дивовижних речей, по-моєму, безглуздо стояти осторонь від цього свята. Озброївшись бажанням глибше зануритися в нову специфікацію і трохи спростити собі життя, спробуємо реалізувати аналіз типів з допомогою такої чудової штуки як Proxy. Звичайно, ми будемо використовувати і інші плюшки ES6, нативно підтримувані Node.js версії 6.1.0, такі як: класи, map'и, стрілочні функції та ін
Як цим користуватися


  1. Встановлюємо пакет
    npm i typedproxy
    . Підключаємо модуль
    //пункт 1.
    const Typed = require('typedproxy');

  2. Створюємо клас, використовуємо статичні методи, методи, статичні властивості. При описі параметрів методів і сеттерів використовуємо спеціальний синтаксис.
  3. А саме: кожне ім'я параметра, що використовується в методах (у т. ч. статичних) має починатися з послідовності символів відповідних типу. Тип — це ніщо інше, як властивість так званого об'єкта типів. Де ім'я властивості відповідає назві типу, а значення властивості є функцією яка реалізує перевірку переданого значення. Іншими словами можна визначити скільки завгодно своїх типів змінних.
  4. Докладніше об'єкті типів. В даному об'єкті повинні бути перераховані типи які використовуються або плануються до використання у вашому класі. Якщо ви забули виконати вказане умова — це призведе до RangeError у процесі виконання.
    І так трохи коду для розуміння принципів:
    //пункт 2. Створюємо клас, описуючи тільки конструктор.
    class TestClass {
    //пункт 3. Конструктор повинен приймати параметр з типом myRange. 
    constructor(myRangeValue){
    this.value = myRangeValue;
    }
    };
    //пункт 4. Створюємо об'єкт типів. Тут ми бачимо використовуваний раніше myRange.
    const types = {
    'myRange' : (value) => {
    if(value < 0 && value > 10) {
    throw new TypeError(`parameter must be more than 0 and less than 10, not ${value}`);
    }
    }
    }; 

    Як ми бачимо myRangeValue — це ім'я параметра, перевірка якого визначається властивості об'єкта типів з відповідним ім'ям myRange.
  5. Тепер, що б включити перевірку типів необхідно зробити клас типізованих (це поняття звичайно використовується в рамках використовуваного модуля, не варто тут притягувати поняття специфікації). А робимо ми це так, маючи раніше описані клас TestClass та типи:
    //пункт 5.
    const TypedTestClass = new Typed(TestClass, types);

  6. ми отримали новий клас TypedTestClass, який насправді є екземпляром Proxy, але про це після. Його ми використовуємо замість TestClass, тобто створюємо примірники, викликаємо статичні методи, як самого класу, так і його примірником. Вообщем роблячи все те, що хотіли зробити з початковим класом TestClass.
    //пункт 6. Створимо кілька екземплярів класу.
    /*ok - параметр конструктора проходить перевірку*/
    const instance1 = new TypedTestClass(5);
    /*TypeError - параметр конструктора не пройшов перевірку*/
    const instance2 = new TypedTestClass(11);
    /*RangeError - кількість параметрів очікуваних конструктором не відповідає
    кількістю переданих у нього параметрів*/
    const instance3 = new TypedTestClass();
    /*RangeError - кількість параметрів очікуваних конструктором не відповідає 
    кількістю переданих у нього параметрів*/
    const instance3 = new TypedTestClass(1, 2);

    Як можна помітити передача невірного типу параметра тепер викликає помилку. Передача невірного кількості параметрів (не важливо чи відповідають вони вимогам типу чи ні) також викликає помилку.
  7. Зауваження по використанню:
    7.1. Якщо ви використовуєте спадкування (наприклад через
    extends
    ) типізувати потрібно кінцевий клас, а не весь ланцюжок. Ну по-перше навіщо зайві змінні і зайва праця, а по-друге у нас просто нічого не вийде.
    7.2. Якщо ви використовуєте параметри за замовчуванням, то даний модуль поки вам не підходить (ми працює над цим).
Всі разом
//пункт 1.
const Typed = require('typedproxy');
//пункт 2. Створюємо клас, описуючи тільки конструктор.
class TestClass {
//пункт 3. Конструктор повинен приймати параметр з типом myRange. 
constructor(myRangeValue){
this.value = myRangeValue;
}
};
//пункт 4. Створюємо об'єкт типів. Тут ми бачимо використовуваний раніше myRange.
const types = {
'myRange' : (value) => {
if(value < 0 && value > 10) {
throw new TypeError(`parameter must be more than 0 and less than 10, not ${value}`);
}
}
}; 
//пункт 5.
const TypedTestClass = new Typed(TestClass, types);
//пункт 6. Створимо кілька екземплярів класу.
/*ok - параметр конструктора проходить перевірку*/
const instance1 = new TypedTestClass(5);
/*TypeError - параметр конструктора не пройшов перевірку*/
const instance2 = new TypedTestClass(11);
/*RangeError - кількість параметрів очікуваних конструктором не відповідає
кількістю переданих у нього параметрів*/
const instance3 = new TypedTestClass();
/*RangeError - кількість параметрів очікуваних конструктором не відповідає 
кількістю переданих у нього параметрів*/
const instance3 = new TypedTestClass(1, 2);

Як це працює


Для тих, кому код зрозуміліше тисячі слів і пари картинок: проект з тестами тут. Для тих хто зберіг бажання зрозуміти як це працює в словах, спробую пояснити далі.
Взаємозв'язок імен параметрів та об'єкта описує типи:
relation
Як видно на малюнку і як було сказано вище необхідно встановити пряму взаємозв'язок між іменами параметрів методів і типами використовуваними в нашому класі. Тобто ім'я параметра, має починатися з послідовності символів відповідних типом.
Це необхідно що б заробила функція виробляє перевірку типу (про неї скажемо трохи пізніше). В принципі, якщо реалізація даної функції вас не влаштовує, можете передати свою третім параметром, при створенні типізованого класу.
const Typed = require('typedproxy');
class TestClass { 
//опис класу...
};
const types = {
//опис типів...
};
const TypedTestClass = Typed(TestClass, types, (types, деяка функція, ...args) => {/*реалізація функції перевірки типів*/});

Принципова схема роботи
workingscheme
При типізації класу створюється і повертається новий Proxy. Цей самий проксі і є класом здійснює аналіз типів. Його суть полягає у застосуванні функції перевірки типів і визначенні необхідних пасток для перехоплення виклику статичних методів, створення нових екземплярів і т. д.
Функція перевірки типів (зелено-червоні квадрати на малюнку) працює наступним чином:
  1. Отримує список використовуваних типів, функцію і передані в функцію параметри.
  2. Витягує (за допомогою регулярного виразу) імена параметрів, які очікує прийняти функція.
  3. Перевіряє однаково кількість переданих та очікуваних до прийняття параметрів. Якщо різна викидає виключення.
  4. Перевіряє починається ім'я очікуваного параметра з послідовності символів відповідних будь-якого з типів. Якщо не збігається ні з одним викидає виключення.
  5. Виконує функцію відповідного типу.
Нових проксі містить тільки три пастки: get, set і construct.
  1. get — при доступі до статичного методу буде повертати проксі, який буде виконувати перевірку типу, а лише потім здійснювати перенаправлення до статичного методу початкового класу.
  2. set — при спробі змінити значення статичного властивості класу, при наявності сетера, буде виконувати перевірку типу, а лише потім здійснювати установку вказаного значення.
  3. construct — при виклику типізованого класу з оператором
    new
    виробляє перевірку типів параметрів, що передаються в конструктор. Після цього створює екземпляр початкового класу і проксі на його основі (з двома пастками get і set, які працюю схожими з зазначеними вище способами 1 і 2).
Звичайно, це виглядає досить монстроподібно і насправді так і є. В цьому модулі відображена лише ідея вимагає доопрацювань і правок. Мені навіть не хочеться думати про продуктивності, швидше за цим можна користуватися жертвуючи їй на догоду зручності та економії часу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.