Переменное количество аргументов: проблемы и решения

в 7:59, , рубрики: javascript, node.js, аргументы, функции, метки: , ,

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

Проблема

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

Пример — jQuery.get, который можно вызвать как $.get(url, callback), а можно и как $.get(url, data, callback).

JavaScript не особенно богат средствами работы с аргументами (в отличие, например, от Python), поэтому для реализации функций с интерфейсом как у jQuery.get приходится писать что-то вроде этого:

function openTheDoor(door, options, callback) {
    if (typeof options === 'function') {
        callback = options
        options = {}
    }
    // тут уже код нашей функции
    var handlePosition = door.getHandlePosition()
    // ...
}

Чем плох этот код?

Во-первых, начало функции не несет смысловой нагрузки, это просто надстройка над способом передачи аргументов. Чтобы увидеть что функция делает, нужно «промотать» в голове четыре строки кода.

Во-вторых, есть соблазн совместить жонглирование параметрами с присваиванием значений по умолчанию, и это может выйти боком — инициализация в следующий раз будет просто «промотана» как стандартный boilerplate.

Подобный подход можно найти в коде многих библиотек, работающих асинхронно. У этой проблемы должно быть решение.

Плохие решения

В реестре npm есть достаточно много модулей, решающих проблему путем обработки объекта arguments. Код выглядит примерно так:

var somehowParse = require('some-fancy-args')

function openTheDoor() {
    var args = somehowParse(arguments)
    // тут уже код нашей функции
    // обратите внимание на отсутствие имен параметров
    var handlePosition = args.first.getHandlePosition()
    // ...
}

Некоторые библиотеки предоставляют маленький «язык определения параметров»:

var parseArgs = require('another-fancy-args')

function openTheDoor() {
    var args = parseArgs(['door|obj', 'options||obj', 'callback||func'], arguments)
    // тут уже код нашей функции
    // по крайней мере, у параметров есть имена
    var handlePosition = args.door.getHandlePosition()
    // ...
}

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

var magicArgs = require('oh-so-magic-args')

function openTheDoor(door, options, callback) {
    return magicArgs(this, ['obj', '|obj', '|func'], function () {
        // тут уже код нашей функции
        // можно использовать объявленные параметры
        var handlePosition = door.getHandlePosition()
        // ...
    })
}

Что плохо в этих решениях? Да почти всё.

Начало функции в любом случае остается шаблоном, который нужно «промотать» чтобы добраться до кода. Этот шаблон вводит дополнительный уровень абстракции, а иногда и магии, в котором нужно разбираться отдельно.

«Немагические» библиотеки еще и немножко портят код самой функции — параметры приходят не в виде отдельных переменных, как это обычно бывает, а в виде объекта, и не всегда у параметров есть имена.

На пути к хорошему решению

Поговорим о функциях.

Во-первых, хорошо написанная функция не должна иметь слишком много параметров. Если функция имеет больше трех параметров, скорее всего ей необходим рефакторинг. Сложную функцию можно разделить на несколько отдельных, можно сгруппировать часть параметров в один параметр-объект, можно еще каким-либо образом её упростить и переписать (хорошая книга по теме — Рефакторинг. Улучшение существующего кода Мартина Фаулера).

Во-вторых, в JavaScript применяется простая и логичная схема работы с отсутствующими параметрами. Если при вызове функции для параметра не передано значение, он принимает значение undefined. Для указания значений параметров по умолчанию удобно применять конструкции вида options = options || {}.[1]

В-третьих, существует соглашение «Callback идет последним», упрощающее асинхронное программирование. В большинстве случаев именно это соглашение и порождает необходимость жонглировать параметрами — раз callback должен всегда идти последним, необязательные параметры вынуждены находиться в середине списка.

Принимая во внимание все три пункта, получаем достаточно простое решение: единственное, что следует сделать — это дополнить список аргументов значениями undefined так, чтобы callback встал на предназначенное ему последнее место. Именно этим и занимается модуль vargs-callback, который я написал для реализации найденного решения.

Модуль vargs-callback

Модуль экспортирует единственную функцию, которую следует использовать как декоратор.

Обычные (именованные) функции:[2]

var vargs = require('vargs-callback')

function openTheDoor(door, options, callback) {
    // тут код нашей функции
    // options будет иметь значение undefined, если при вызове указаны только door и callback
    var handlePosition = door.getHandlePosition()
    // ...
}
openTheDoor = vargs(openTheDoor) // Декорируем именованную функцию

Функции-выражения:

var vargs = require('vargs-callback')

var openTheDoor = vargs(function (door, options, callback) { // Декорируем функцию-выражение
    // тут код нашей функции
    // options будет иметь значение undefined, если при вызове указаны только door и callback
    var handlePosition = door.getHandlePosition()
    // ...
})

Декоратор vargs срабатывает в момент вызова декорированной функции и выполняет следующее:

  1. Если количество переданных аргументов меньше объявленного количества параметров и последний переданный аргумент имеет тип «функция» — поместить перед последним аргументом значения undefined до совпадения количества аргументов и количества параметров.
  2. Вызвать декорированную функцию с измененными аргументами.
  3. Если передано достаточно аргументов или последний аргумент не функция — не делать ничего.

Заключение

В найденном решении я могу отметить следующие плюсы:

Не используется дополнительный уровень абстракции для определения параметров функции. Нет нужды в магии или «языке определения параметров», используется только то, что есть в самом JavaScript.

Код становится чище — используются объявленные параметры, не нужно «проматывать» начало функции, а для установки значений параметров по умолчанию можно применять подходящий в каждом конкретном случае способ.

Как вам идея, коллеги?

Исходный код на github

Примечания

  1. Такой способ небезопасно использовать для параметров, которые могут принимать falsy-значения. Для них можно использовать проверку типа: options = typeof options !== 'undefined'? options: {}
  2. Имеется ввиду function declaration.

Автор: furagu

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js