[Из песочницы] Анализ типов с помощью Proxy
В процессе описания очередного набора тестов для модуля Node.js поймал себя на мысли «опять проверка типов». Каждый параметр метода класса, каждое свойство устанавливаемое с помощью сеттера надо проверять. Можно конечно просто забить или дополнять все кодом реализующим проверки или попробовать описать все декораторами. Но в этот раз поступим немного иначе.
Немного эмоций и сладости
Приход спецификации ES6 и реализации ее в различных движка дарит нам много удивительных вещей, по-моему глупо стоять в стороне от этого праздника. Вооружившись желанием поглубже погрузиться в новую спецификацию и немного упростить себе жизнь, попробуем реализовать анализ типов с помощью такой замечательной штуки как Proxy. Конечно мы будем использовать и другие плюшки ES6, нативно поддерживаемые Node.js версии 6.1.0, такие как: классы, map’ы, стрелочные функции и др.
Как этим пользоваться
-
Устанавливаем пакет
npm i typedproxy
. Подключаем модуль//пункт 1. const Typed = require('typedproxy');
-
Создаем класс, используем статические методы, методы, статические свойства и свойства. При описании параметров методов и сеттеров используем специальный синтаксис.
-
А именно: каждое имя параметра, используемое в методах (в т.ч. статических) должно начинаться с последовательности символов соответствующих типу. Тип — это ничто иное, как свойство так называемого объекта типов. Где имя свойства соответствует названию типа, а значение свойства является функцией реализующей проверку переданного значения. Другими словами можно определить сколько угодно своих типов переменных.
-
Подробнее о объекте типов. В данном объекте должны быть перечислены все типы которые используются, или планируются к использованию в вашем классе. Если вы забыли выполнить указанное условие — это приведет к 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.
-
Теперь, что бы включить проверку типов необходимо сделать класс типизированным (это понятие конечно используется в рамках используемого модуля, не стоит здесь притягивать понятия из спецификации). А делаем мы это так, имея ранее описанные класс TestClass и типы:
//пункт 5. const TypedTestClass = new Typed(TestClass, types);
-
Выше мы получили новый класс 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.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);
Как это работает
Для тех, кому код понятнее тысячи слов и пары картинок: проект с тестами здесь. Для тех кто сохранил желание понять как это работает в словах, попробую объяснить далее.
Взаимосвязь имен параметров и объекта описывающего типы:
Как видно на рисунке и как было сказано выше необходимо установить прямую взаимосвязь между именами параметров методов и типами используемыми в нашем классе. То есть имя параметра, должно начинаться с последовательности символов соответствующих типу.
Это необходимо что бы заработала функция производящая проверку типа (о ней скажем немного позже). В принципе, если реализация данной функции вас не устраивает, можете передать свою третьим параметром, при создании типизированного класса.
const Typed = require('typedproxy');
class TestClass {
//описание класса...
};
const types = {
//описание типов...
};
const TypedTestClass = Typed(TestClass, types, (types, someFunction, ...args) => {/*реализация функции проверки типов*/});
Принципиальная схема работы
При типизации класса создается и возвращается новый Proxy. Этот самый прокси и является классом осуществляющим анализ типов. Его суть состоит в применении функции проверки типов и определении необходимых ловушек для перехвата вызова статических методов, создания новых экземпляров и т.д.
Функция проверки типов (зелено-красные квадраты на рисунке) работает следующим образом:
- Получает список используемых типов, функцию и переданные в функцию параметры.
- Извлекает (при помощи регулярного выражения) имена параметров которые ожидает принять функция.
- Проверяет одинаково ли количество переданных и ожидаемых к принятию параметров. Если различно выкидывает исключение.
- Проверяет начинается ли имя ожидаемого параметра с последовательности символов соответствующих какому-либо из типов. Если не совпадает ни с одним выкидывает исключение.
- Исполняет функцию соответствующего типа.
Новых прокси содержит только три ловушки: get, set и construct.
- get — при доступе к статическому методу будет возвращать прокси который будет выполнять проверку типа, а лишь затем осуществлять перенаправление к статическому методу исходного класса.
- set — при попытке изменить значение статического свойства класса, при наличии сеттера, будет выполнять проверку типа, а лишь затем осуществлять установку указанного значения.
- construct — при вызове типизированного класса с оператором
new
производит проверку типов параметров передающихся в конструктор. После этого создает экземпляр первоначального класса и прокси на его основе (с двумя ловушками get и set, которые работаю похожими с указанными выше способами 1 и 2).
Конечно это выглядит достаточно монструозно и на самом деле так и есть. В этом модуле отображена лишь идея требующая доработок и правок. Мне даже не хочется думать о производительности, скорее этим можно пользоваться жертвуя ей в угоду удобству и экономии времени.