- PVSM.RU - https://www.pvsm.ru -
В процессе описания очередного набора тестов для модуля Node.js поймал себя на мысли "опять проверка типов". Каждый параметр метода класса, каждое свойство устанавливаемое с помощью сеттера надо проверять. Можно конечно просто забить или дополнять все кодом реализующим проверки или попробовать описать все декораторами. Но в этот раз поступим немного иначе.
Приход спецификации ES6 и реализации ее в различных движка дарит нам много удивительных вещей, по-моему глупо стоять в стороне от этого праздника. Вооружившись желанием поглубже погрузиться в новую спецификацию и немного упростить себе жизнь, попробуем реализовать анализ типов с помощью такой замечательной штуки как Proxy [1]. Конечно мы будем использовать и другие плюшки 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, но об этом после [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);
Как можно заметить передача неверного типа параметра теперь вызывает ошибку. Передача неверного количества параметров (не важно удовлетворяют ли они требованиям типа или нет) также вызывает ошибку.
extends) типизировать нужно конечный класс, а не всю цепочку. Ну во-первых зачем лишние переменные и лишний труд, а во-вторых у нас просто ничего не выйдет.//пункт 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]. Для тех кто сохранил желание понять как это работает в словах, попробую объяснить далее.

Как видно на рисунке и как было сказано выше необходимо установить прямую взаимосвязь между именами параметров методов и типами используемыми в нашем классе. То есть имя параметра, должно начинаться с последовательности символов соответствующих типу.
Это необходимо что бы заработала функция производящая проверку типа (о ней скажем немного позже) [4]. В принципе, если реализация данной функции вас не устраивает, можете передать свою третьим параметром, при создании типизированного класса.
const Typed = require('typedproxy');
class TestClass {
//описание класса...
};
const types = {
//описание типов...
};
const TypedTestClass = Typed(TestClass, types, (types, someFunction, ...args) => {/*реализация функции проверки типов*/});

При типизации класса создается и возвращается новый Proxy. Этот самый прокси и является классом осуществляющим анализ типов. Его суть состоит в применении функции проверки типов [4] и определении необходимых ловушек [5] для перехвата вызова статических методов, создания новых экземпляров и т.д.
Функция проверки типов (зелено-красные квадраты на рисунке) работает следующим образом:
Новых прокси содержит только три ловушки: get, set и 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
Нажмите здесь для печати.