- PVSM.RU - https://www.pvsm.ru -

Анализ типов с помощью Proxy

В процессе описания очередного набора тестов для модуля Node.js поймал себя на мысли "опять проверка типов". Каждый параметр метода класса, каждое свойство устанавливаемое с помощью сеттера надо проверять. Можно конечно просто забить или дополнять все кодом реализующим проверки или попробовать описать все декораторами. Но в этот раз поступим немного иначе.

Немного эмоций и сладости


Приход спецификации ES6 и реализации ее в различных движка дарит нам много удивительных вещей, по-моему глупо стоять в стороне от этого праздника. Вооружившись желанием поглубже погрузиться в новую спецификацию и немного упростить себе жизнь, попробуем реализовать анализ типов с помощью такой замечательной штуки как Proxy [1]. Конечно мы будем использовать и другие плюшки 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, но об этом после [2]. Его мы используем вместо 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);

Как это работает


Для тех, кому код понятнее тысячи слов и пары картинок: проект с тестами здесь [3]. Для тех кто сохранил желание понять как это работает в словах, попробую объяснить далее.

Взаимосвязь имен параметров и объекта описывающего типы:

relation

Как видно на рисунке и как было сказано выше необходимо установить прямую взаимосвязь между именами параметров методов и типами используемыми в нашем классе. То есть имя параметра, должно начинаться с последовательности символов соответствующих типу.
Это необходимо что бы заработала функция производящая проверку типа (о ней скажем немного позже) [4]. В принципе, если реализация данной функции вас не устраивает, можете передать свою третьим параметром, при создании типизированного класса.

const Typed = require('typedproxy');
class TestClass { 
   //описание класса...
};
const types = {
   //описание типов...
};
const TypedTestClass = Typed(TestClass, types, (types, someFunction, ...args) => {/*реализация функции проверки типов*/});

Принципиальная схема работы

workingscheme

При типизации класса создается и возвращается новый Proxy. Этот самый прокси и является классом осуществляющим анализ типов. Его суть состоит в применении функции проверки типов [4] и определении необходимых ловушек [5] для перехвата вызова статических методов, создания новых экземпляров и т.д.

Функция проверки типов (зелено-красные квадраты на рисунке) работает следующим образом:

  1. Получает список используемых типов, функцию и переданные в функцию параметры.
  2. Извлекает (при помощи регулярного выражения) имена параметров которые ожидает принять функция.
  3. Проверяет одинаково ли количество переданных и ожидаемых к принятию параметров. Если различно выкидывает исключение.
  4. Проверяет начинается ли имя ожидаемого параметра с последовательности символов соответствующих какому-либо из типов. Если не совпадает ни с одним выкидывает исключение.
  5. Исполняет функцию соответствующего типа.

Новых прокси содержит только три ловушки: get, set и construct.

  1. get — при доступе к статическому методу будет возвращать прокси который будет выполнять проверку типа, а лишь затем осуществлять перенаправление к статическому методу исходного класса.
  2. set — при попытке изменить значение статического свойства класса, при наличии сеттера, будет выполнять проверку типа, а лишь затем осуществлять установку указанного значения.
  3. construct — при вызове типизированного класса с оператором new производит проверку типов параметров передающихся в конструктор. После этого создает экземпляр первоначального класса и прокси на его основе (с двумя ловушками get и set, которые работаю похожими с указанными выше способами 1 и 2).

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

Автор: antonecma

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/120965

Ссылки в тексте:

[1] Proxy: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

[2] после: #underthehood

[3] здесь: https://github.com/antonecma/TypedProxy

[4] (о ней скажем немного позже): #typeTester

[5] ловушек: #interceptions

[6] Источник: https://habrahabr.ru/post/301122/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox