Статьи

О достижении полиморфизма в JavaScript

В выходные я начал работать над клиентской библиотекой Node.js для нового API Тошла . Старый побочный проект недавно перестал работать, и мне нужно это исправить, потому что жизнь без частых писем о моих деньгах дезориентирует меня до чертиков.

Бета API Toshl открыт в июле хорошо продумана, имеет хорошую документацию и предоставляет все, возможно , хотят. Я люблю хорошо раздробленную систему разрешений.

Но для пользователей моей библиотеки Node.js я хотел сделать жизнь еще проще. Давайте посмотрим на получение расходов.

После аутентификации — тестирование клиентов OAuth — отстой, и Toshl очень часто делает ваш токен недействительным . Думаю, мне пришлось обновить его четыре раза за пять часов веселья. — вы можете поговорить, чтобы https://api.toshl.com/expensesполучить список расходов для текущего пользователя.

Это вернет последние 30 записей.

Но есть куча вариантов. У вас есть нумерация страниц, вы можете установить конкретные toи fromдаты, вы можете фильтровать вещи по тегам и не теги.

Как библиотека может сделать это простым в использовании?

Один из подходов состоит в том, чтобы предложить способ указания хэша опций при вызове функции, но можем ли мы сделать его еще проще?

Ответ заключается в полиморфизме.

Допустим, у вас есть функция с именем toshl.expenses, в Haskell вы можете сделать что-то вроде этого:

expenses::Result
expenses = general_expenses ""
 
expenses::Number -> Result
expenses N = general_expenses "?per_page="+str(N)
 
expenses::[String] -> Result
expenses tags = general_expenses "?tags="+(tags.join "&")
 
expenses::Date -> Date -> Result
expenses from to = general_expenses "?from="+str(from)+"&"+str(to)
 
expenses::Json -> Result
expenses params = general_expenses "?"+to_query(params)
 
general_expenses::String -> Result
general_expenses query = make_request "/expenses" query
 
make_request::String -> String -> Result
make_request endpoint query = ;; do stuff to read from full URL

Синтаксис, скорее всего, неправильный, но вы понимаете, на что я указываю. Вы всегда можете вызвать expensesаргумент, который вас интересует, и он волшебным образом создаст вызов более обобщенной версии функции.

Вы можете сразу определить, что expensesбудет делать вызов в каждом случае, и проверить, какой тип аргументов принимает функция, тривиально. Даже тот, кто не знаком с Haskell, может понять этот код.

Вот тот самый полиморфный код в Javascript . На этот раз это проверено, рабочий код.

exports.Toshl.prototype.expenses = function (params, to, callback) {
    var options = {},
        query = '';
    callback = arguments[arguments.length-1];
 
    if (params) {
        if (typeof params == 'number') {
            options['per_page'] = params;
        }else if (arguments.length == 3) {
            options['from'] = util.iso_date(params);
            options['to'] = util.iso_date(to);
        }else if (params instanceof Array) {
            var tags = util.transform_tags(params);
            options[tags.type] = tags.tags;
        }else if (params instanceof Object) {
            options = params;
 
            ['from', 'to'].forEach(function (key) {
                if (options[key]) {
                    options[key] = util.iso_date(options[key]);
                }
            });
        }
 
        query = "?"+querystring.stringify(options);
    }
 
    this._request('/expenses'+query, callback);
};

Ух ты, что?

Даже если вам очень удобно работать с Javascript, вам будет сложно понять, что происходит. Кажется, что большая часть функции имеет дело с переводом argumentsв a query, а затем она предназначена this._requestдля тяжелой работы.

Это самая чистая реализация, о которой я только мог подумать. Давайте исследуем.

Сначала мы убедились, что callbackэто всегда последний предоставленный аргумент, который имеет смысл, когда вы ожидаете переменного количества аргументов. Все с одного до трех все в порядке.

Затем, если paramsэто число, мы используем его для построения ?per_page=Nзапроса. Если есть три аргумента, мы используем первые два для построения ?from=Date&to=Dateзапроса. Если первый аргумент является массивом, мы используем его для получения тегов, а если это объект, мы предполагаем, что он представляет хэш параметров для API.

В результате мы можем сделать это:

var toshl = new Toshl();
 
toshl.expenses(console.log); // prints last 30 expenses
 
toshl.expenses(5, console.log); // prints last 5 expenses
 
toshl.expenses("2013-10-01", new Date(), console.log); // prints all expenses between October 1st and now
 
toshl.expenses(["coffee", "food"], console.log); // prints last 30 expenses tagged with coffee or food
 
toshl.expenses({per_page: 10, tags: ["coffee", "food"]}, console.log) // prints last 10 expenses tagged coffee or food

Волшебство даты выполняется с помощью замечательной библиотеки moment.js — вы можете предоставить объект Date, строку даты или объект момента, но мы можем явно улучшить это, предполагая, что toдата «сейчас», если ничего не указано.

Но код становится все сложнее.

Кто-нибудь знает лучший способ достижения полиморфизма в Javascript? Мне действительно нравится, когда я использую библиотеки, но я ненавижу реализовывать это таким образом …

Вы должны следовать за мной в твиттере, здесь .