Функции как объекты первого рода. Часть № 2 #
Васильев Андрей Михайлович, 2022
Версии презентации
Содержание #
- Варианты применения рефакторинга замена тела функцией обратного вызова
- Выработка понимания техники возвращения новых функций в качестве работы функции и мощности её применения
- Выполнить практические задания по написанию функций высшего порядка для оттачивания навыков
Один маркер в исходном коде и два рефакторинга #
Вспомним ключевые моменты предыдущего занятия
Маркер кода: неявный аргумент в названии фукнции #
Если в рамках тела функции идёт обращение к некоторому значению, и это значение также присутствует в названии функции, то скорее всего у функции есть неявный аргумент
- Существует несколько функций с похожей реализацией
- Разница между этими функциями описывается в названии функции
Рефакторинг: выделение неявного аргумента #
Цель данного рефакторинга — избавление от неявного аргумента. Это может помочь в выражении назначения кода и потенциально уменьшить дублирование
- Идентифицировать неявный аргумент в названии функции
- Добавить явный аргумент
- Использовать новый аргумент в теле функции вместо жёстко записанного значения
- Изменить вызывающий код
Рефакторинг: замена тела функцией обратного вызова #
Данный рефакторинг позволяет выделить «тело функции», часть функции, которая различается между функциями. Она заменяется функцией, которая передаётся в качестве аргумента
- Определить части функции: общее начало, тело, общее окончание
- Выделение всего кода под вопросом в функцию
- Выделение тела функции в функцию, которая передаётся в качестве аргумента
Рефакторинг копирования при записи #
При реализации шаблона копирование при записи мы часто дублировали код по созданию копий массивов и объектов. Шаблон копирования при записи:
- Создать копию
- Модифицировать копию объекта
- Вернуть модифицированную копию
К этому шаблону легко применить рефакторинг по замену тела функцией обратного вызова: пункт «модифцировать копию объекта» является телом
Реализация КПЗ для массивов #
Шаг № 1: определение частей функции
function arraySet(array, index, value) {
var copy = array.slice();
copy[index] = value;
return copy;
}
function push(array, value) {
var copy = array.slice();
copy.push(value);
return copy;
}
function drop_last(array) {
var array_copy = array.slice();
array_copy.pop();
return array_copy;
}
function drop_first(array) {
var array_copy = array.slice();
array_copy.shift();
return array_copy;
}
// Общая часть
function action(...) {
var array_copy = array.slice(); // Общее начало
// Тело
return array_copy; // Общее окончание
}
Реализация КПЗ для массивов #
Шаг № 2: выделение тела в функцию
// Оригинал
function arraySet(array, index, value) {
var copy = array.slice();
copy[index] = value;
return copy;
}
// Техническое выполнение шага
function arraySet(array, index, value) {
return withArrayCopy(array);
}
function withArrayCopy(array) {
var copy = array.slice();
copy[index] = value; // Не будет работать
return copy;
}
Реализация КПЗ для массивов #
Шаг № 3: выделение функции для обратного вызова
// Результат шага № 2
function arraySet(array, index, value) {
return withArrayCopy(array);
}
function withArrayCopy(array) {
var copy = array.slice();
copy[index] = value;
return copy;
}
// Выделение функции для обратного вызова
function arraySet(array, index, value) {
return withArrayCopy(
array,
function(copy) {
copy[idx] = value;
});
}
function withArrayCopy(array, modify) {
var copy = array.slice();
modify(copy);
return copy;
}
Реализация КПЗ для массивов #
Сравним оригинал функции и модификации
// Оригинал
function arraySet(array, index, value) {
var copy = array.slice();
copy[index] = value;
return copy;
}
// Модификация
function arraySet(array, index, value) {
return withArrayCopy(array, function(copy) {
copy[index] = value;
});
}
function withArrayCopy(array, modify) {
var copy = array.slice();
modify(copy);
return copy;
}
Результат рефакторинга #
- В результате рефакторинга количество строк кода увеличилось
- Однако нам удалось реализовать стандартную КПЗ процедуру для массивов, которую не надо повторять во многих местах
Реализация быстрой сортировки с помощью специальной версии
var sortedArray = withArrayCopy(array, function(copy) {
SuperSorter.sort(copy)
});
Можно выполнять несколько действий в рамках модификации
var a1 = drop_first(array);
var a2 = push(a1, 10);
var a3 = push(a2, 11);
var a4 = arraySet(a3, 0, 42);
var a4 = withArrayCopy(array, function(copy) {
copy.shift();
copy.push(10);
copy.push(11);
copy[0] = 42;
});
Практика #
Реализуйте с помощью реализованного метода withArrayCopy
модифицируйте ранее созданные КПЗ-методы push
, drop_last
, drop_first
Решение
function push(array, element) {
return withArrayCopy(array, function(copy) {
copy.push(element);
});
}
function drop_last(array) {
return withArrayCopy(array, function(copy) {
copy.pop();
});
}
function drop_first(array) {
return withArrayCopy(array, function(copy) {
copy.shift();
});
}
Практика. Реализация КПЗ для объектов #
Реализуйте метод withObjectCopy(), который может быть использован для реализации КПЗ для объектов. В качестве основы для создания данного метода используйте следующие функции:
function objectSet(object, key, value) {
var copy = Object.assign({}, object);
copy[key] = value;
return copy;
}
function objectDelete(object, key) {
var copy = Object.assign({}, object) ;
delete copy[key];
return copy;
}
Вариант решения задачи
function withObjectCopy(object, modify) {
var copy = Object.assign({}, object);
modify(copy);
return copy;
};
function objectSet(object, key, value) {
return withObjectCopy(object, function(copy) {
copy[key] = value;
});
}
function objectDelete(object, key) {
return withObjectCopy(object, function(copy) {
delete copy[key];
});
}
Практика #
При реализации функции withLogging()
мы смогли обобщить подход к выполнению журналирования произвольных ошибок во внешнюю систему
Постараемся сделать более общий метод, который позволит реализовывать разные стратегии по обработке исключительных ситуаций. Цель — написание кода
tryCatch(sendEmail, logToSnapErrors)
// вместо
try {
sendEmail();
} catch(error) {
logToSnapErrors(error);
}
Задача состоит в написании функции tryCatch()
Подсказка: данный метод должен принимать две функции в качестве аргументов
Вариант решения:
function tryCatch(f, errorHandler) {
try {
return f();
} catch(error) {
return errorHandler(error);
}
}
Практика #
В качестве упражнения выполним оборачивание ещё одного элемента синтаксиса: условного оператора. Для упрощения задачи рассмотрим условный оператор без else
Пример кода, который необходимо модифицировать:
// Оригинал
if(array.length === 0) {
console.log("Array is empty");
}
// Замена
when(array.length === 0, function() {
console.log("Array is empty");
});
// Оригинал
if(hasItem(cart, "shoes")) {
return setPriceByName(cart, "shoes", 0);
}
// Замена
when(hasItem(cart, "shoes"), function() {
return setPriceByName(cart, "shoes", 0);
});
Вариант решения
function when(test, then) {
if(test)
return then();
}
Практикум #
После реализации функции when
люди стали её использовать и захотели полноправный аналог if
с else
-выражением. Ваша задача — реализация функции IF:
IF(array.length === 0, function() {
console.log("Array is empty");
}, function() {
console.log("Array has something in it.");
});
IF(hasItem(cart, "shoes"), function() {
return setPriceByName(cart, "shoes", 0);
}, function() {
return cart;
});
Вариант решения
function IF(test, then, ELSE) {
if(test)
return then();
else
return ELSE();
}
Возвращение функций из функций #
При реализации метода withLogging()
мы смогли избавиться от большой части дублирования, однако для её применения всё-равно приходится изменять много кода
try {
saveUserData(user);
} catch(error) {
logToSnapErrors(error);
}
withLogging(function() {
saveUserData(user);
});
Мы экономим 2 строчки кода, однако всё ещё надо писать по 3 дополнительные строчки
function withLogging(f) {
try {
f();
} catch (error) {
logToSnapErrors(error);
}
}
Дублирование с withLogging #
withLogging(function() {
saveUserData(user);
});
withLogging(function() {
fetchProduct(productID);
});
Данная система формирует стандартный подход к решению проблемы, но он имеет следующие проблемы:
- Можно забыть вызвать данный метод
- Данный код всё-равно необходимо вызывать везде
В примерах видно, что проблема дублирования существует
Было бы хорошо просто вызывать функции и автоматически получить желаемое поведение
Оригинальный код #
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logSnapErrors(error);
}
Сделаем код более чётким, изменив название функций, чтобы они детальнее отражали отсутствие журналирования
Понятные имена #
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logSnapErrors(error);
}
try {
saveUserDataNoLogging(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProductNoLogging(productId);
} catch (error) {
logSnapErrors(error);
}
Функции с журналированием #
try {
saveUserDataNoLogging(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProductNoLogging(productId);
} catch (error) {
logSnapErrors(error);
}
function saveUserDataWithLogging(user) {
try {
saveUserDataNoLogging(user);
} catch (error) {
logToSnapErrors(error);
}
}
function fetchProductWithLogging(productId) {
try {
fetchProductNoLogging(productId);
} catch (error) {
logSnapErrors(error);
}
}
Теперь мы чётко оделили функции с журналированием от оригинальных функций, но при вызове новых функций их точно не надо оборачивать
Потенциальное обобщение #
function (arg) {
try {
saveUserDataNoLogging(arg);
} catch (error) {
logToSnapErrors(error);
}
}
function (arg) {
try {
fetchProductNoLogging(arg);
} catch (error) {
logSnapErrors(error);
}
}
Применим первые шаги рефакторинга по замене тела с функцией обратного вызова
Определим общие и частные части
function (arg) { // Начало
try { //
saveUserDataNoLogging(arg);
} catch (error) { // Окончание
logToSnapErrors(error); //
} //
} //
function (arg) { // Начало
try { //
fetchProductNoLogging(arg);
} catch (error) { // Окончание
logSnapErrors(error); //
} //
} //
Функция высшего порядка #
Вместо последнего шага рефакторинга, вызов функции, переданной в качестве аргумента, будем возвращать новую функцию-обёртку
function (arg) {
try {
saveUserDataNoLogging(arg);
} catch (error) {
logToSnapErrors(error);
}
}
function wrapLogging(f) {
return function(arg) {
try {
f(arg);
} catch (error) {
logSnapErrors(error);
}
}
}
var saveUserDataWithLogging =
wrapLogging(saveUserDataNoLogging);
Теперь можно легко создавать функции-обёртки:
var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);
var fetchProductWithLogging = wrapLogging(fetchProductNoLogging);
Использование функции высшего порядка #
Ручной подход
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
Автоматический подход
saveUserDataWithLogging(user);
Последнее стало возможно благодаря работе функции wrapLogging
:
var saveUserDataWithLogging = wrapLogging(saveUserData);
saveUserData
— оригинальная функцияwrapLogging
— функция высшего порядка, которая создаёт функцию-обёртку и возвращает еёsaveUserDataWithLogging
— функция-обёртка, созданная функцией высшего порядка
Обсуждение #
Результат работы функции withLogging
сохраняется в переменную. Ранее в коде использовались функции, определённые с помощью слова function
#
Данный подход действительно поначалу может быть неудобен. Однако стандарты наименования функций и переменных в большинстве языков программирования совпадают. Ключевое отличие — использование глаголов для функций, существительных для данных
Надо быть готовым к расширенным подходам определения функций
Функция withLogging
может делать обёртку только для функций с одним аргументом. А как её можно применить для функций с большим количеством аргументов? Как получить возвращаемое значение?
#
Для возвращения значения — достаточно добавить в соответствующее место ключевое слово return
к вызову функции
Для работы с произвольным количеством аргументов мы можем обратиться к возможностям языка, описанным в стандарте ES6 (2015 год):
function wrapLogging(f) {
return function(...args) {
try {
return f(...args);
} catch (error) {
logToSnapErrors(error);
}
}
}
В других языках поддержка функций с произвольным количеством аргументов может быть реализована по-разному
Практика #
Создайте функцию высшего порядка, которая будет перехватывать ошибки и игнорировать их (т.е. реализовывать стандартную стратегию обработки ошибок в приложении). Если происходит ошибка, то метод должен вернуть null
Подсказка. Для решения этой задачи без функций можно использовать код:
try {
codeThatMightThrow();
} catch(e) {
// Игнорируем ошибку
}
Вариант решения
function wrapIgnoreErrors(f) {
return function(...args) {
return f(...args);
} catch (error) {
return null;
}
}
Практика #
Реализуйте функцию высшего порядка makeAdder
которая создаёт функцию, которая добавляет число к переданному аргументу. Пример работы:
var increment = makeAdder(1);
increment(10); // 11
var plus10 = makeAdder(10);
plus10(12); // 22
Вариант решения
function makeAdder(number) {
return function(x) {
return number + x;
};
}
Обсуждение #
Кажется, что с подходом создания новых функций из функций мы можем решить множество задач. Можем ли мы написать всё приложение таким образом? #
Если подойти технически к данному вопросу, то ответ скорее всего: да. Более правильный вопрос: стоит ли так поступать?
Подход с использованием функций высшего порядка более общий. Этот подход также радует программистов, так как позволяет им поиграться со сложными технологиями. Однако задача программистов — решать бизнес-задачи
Если видно, что после применения функций высшего порядка можно решить проблемы кодовой базы, то их стоит применять
При знакомстве с данным подходом следует попытаться применять его везде, чтобы понять сильные и слабые стороны данного подхода. Однако эксперименты должны проводиться вне продуктовой кодовой базы
Если в результате анализа применения данного подхода есть объективные положительные моменты, то стоит использовать данный подход
Заключение #
В рамках данной лекции мы углубили знание функций как объектов первого порядка, и функций высшего порядка. Потенциал этих идей будет рассмотрена на следующих занятиях
- Функции высшего порядка могут фиксировать в коде шаблоны и подходы, которые иначе пришлось бы повторять множество раз в коде. После написания кода их можно повторить путём простого вызова функции
- Мы можем создавать новые функции путём возвращения их из других функций, функций высшего порядка. Возвращаемое значение можно сохранить в переменную и вызвать функцию через неё
- Функции высшего порядка не бесплатны и требуют аккуратного обращения. Они могут убрать большое количество дублирования, но они могут также и ухудшить, усложнить код. Их надо использовать осмысленно