Статьи

Шаблоны для DRY-er JavaScript


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

// config is defined outside of this snippet,
// and may contain more than the properties
// we care about

$('#myCheckbox').click(function() {
if (this.checked) {
$('#field_foo').val(config.foo);
$('#field_bar').val(config.bar);
$('#field_baz').val(config.baz);
} else {
$('#field_foo').val('');
$('#field_bar').val('');
$('#field_baz').val('');
}
});

Это полностью читаемый фрагмент кода — почти нет сомнений, что здесь происходит. С другой стороны, довольно легко увидеть безудержное повторение; этот код не заинтересован в том, чтобы «не повторяться» (СУХОЙ). Мы вызываем один и тот же метод для каждого сделанного нами выбора, и наши выборы повторяются в блоке if и else.

Когда я увидел этот код, я сразу же захотел переписать его. Вот что я придумал первым:

// config is defined outside of this snippet,
// and may contain more than the properties
// we care about

$('#myCheckbox').click(function() {
// note whether the checkbox is checked
var checked = this.checked;

// iterate over the keys we care about
$.each(['foo', 'bar', 'baz'], function(i,v) {
// find the field for the given key
$('#field_' + v)
// and set its value either to the string
// stored for the key, or to an empty string,
// depending on whether the checkbox was checked
.val(checked ? config[v] : '');
});
});

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

В этой итерации мы ввели два шаблона для кода DRY-er: итерацию по литералу массива (или, альтернативно, объекту) для достижения повторения без повторения, и использование троичного оператора вместо оператора if / else, когда простота нашей логики позволяет это.

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

(Примечание: Является ли это преждевременной оптимизацией? Я бы сказал, что нет, если вы научитесь видеть эти шаблоны до того, как начнете писать код. Как только вы научитесь определять эти шаблоны в требовании, написание кода, охватывающего их, на самом деле может быть проще, чем писать код, который использует более «буквальный» подход к проблеме. Например, представьте, если флажок затрагивает 20 других полей вместо одного? Вы, несомненно, обнаружите, что копируете и вставляете код, если вы выбрали более «буквальный» подход к проблеме, и это будет ваша первая подсказка, что вы делаете что-то неэффективно.)

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

Допустим, мы гордимся тем, что высушили наш код, используя умный JavaScript, который могут прочитать только супер-умные люди. Теперь есть еще один флажок, который требует аналогичного поведения, но он будет использовать другой объект конфигурации и другой список полей. Нет проблем! Вы уже написали этот код, так что вы можете просто скопировать и вставить его, а затем изменить то, что отличается. Милая. Э-э … вдруг ты не выглядишь так сухо в конце концов.

Это когда в игру вступает другой шаблон: создание функции, которая возвращает другую функцию с уже запеченными определенными переменными (то есть создание замыкания). Мы выполним эту функцию создателя, а затем используем функцию, которую она возвращает, вместо анонимной функции, которую мы использовали ранее, когда мы связывались с событием click.

// handleClick accepts a config object
// and a makeSelector function; it returns
// a function that can be bound to
// a click event, using the config object
// and the makeSelector function to react
// appropriately to the click
var handleClick = function(fields, config, makeSelector) {
return function() {
var checked = this.checked;

fields && $.each(fields, function(i, v) {
// build the selector using the provided
// makeSelector function
$(makeSelector(v))
// set the value using the
// config object, depending
// on whether the checkbox
// is checked
.val(checked ? config[v]: '');
});
};
};

$('#myCheckbox').click(
// use handleClick to create a function
// that has these variables baked in;
// pass the created function as the
// click handling function
handleClick(
['foo','bar','baz'],
myCheckboxConfig,
function(field) {
return '#field_' + field;
}
)
);

$('#myOtherCheckbox').click(
handleClick(
['bim','bar','bop'],
myOtherCheckboxConfig,
function(field) {
return 'input[name="' + field + '"]';
}
)
);

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

JavaScript предлагает множество шаблонов для написания кода DRY-er; важно научиться распознавать и использовать их. Во-первых, также важно понимать, когда вы пишете не-СУХОЙ код — копирование и вставка кода являются одним кристально чистым индикатором, но другие являются более тонкими, и вы можете не идентифицировать их при первом обходе. Например, возьмите эти две функции; каждый получает элемент списка в качестве единственного аргумента и возвращает следующий или предыдущий элемент списка, возвращаясь к началу или концу списка, если следующий или предыдущий элемент отсутствует.

var getNextItem = function($item) {
return $item.next().length ?
$item.next() : $items.first();
};

var getPrevItem = function($item) {
return $item.prev().length ?
$item.prev() : $items.last();
};

Когда я впервые написал это, это казалось повторением, но я не мог быстро придумать единственную функцию, которая бы работала. Небольшое размышление об этом, однако, привело меня к этой единственной функции, которая получает второй аргумент: направление. Этот аргумент используется, чтобы решить, следует ли запускать следующий или предыдущий метод элемента, а затем запускать первый или последний метод элемента. Помимо объединения двух функций в одну, это также исключает вызовы next или prev дважды внутри каждой функции.

var getItem = function($item, direction) {
var $returnItem = $item[direction]();
return $returnItem.length ?
$returnItem :
$items[(direction == 'next') ? 'first' : 'last']();
};

Изучение шаблонов, а затем поиск возможностей их использования — одна из самых приятных частей моей работы. Я надеюсь, что это поможет вам определить некоторые из этих возможностей для себя