Васильев Андрей Михайлович, 2022
Версии презентации
В рамках первой части мы ознакомились с тем как разделять вычисления на три категории: действия, вычисления и данные. Познакомились со стратифцированной архитектурой.
В рамках второй части мы познакомимся с идеями объектов первого рода, в частности функций как объектов первого рода. На основании этого навыка мы познакомимся:
В рамках данной главы мы будем в основном бороться с дублированием и будем искать более качественные абстракции. Сделаем краткий обзор
Для решения проблем рекомендуется использовать следующие рефакторинги
При наличии неявного аргумента сначала его необходимо сделать явным: добавить новый аргумент для функции, чтобы он стал объектом первого рода
Шаги:
Синтаксис языка часто не является объектом первого рода. Данный рефакторинг позволяет заменить тело функции (часть, которая изменяется) на обратный вызов
Поведение функции будет определяться другой функцией, которая будет передана в качестве аргумента. Это мощный способ для создания функций высшего порядка
Шаги:
Реализованный ранее барьер из абстракций выполнял свои задачи, но не так хорошо, как предполагалось. Иногда команда маркетинга просит реализовать новые функции в API
Примеры новых запросов:
Все запросы выглядят похожими, да и код для их реализации тоже выглядит похожим. Почему барьер из абстракций не помог решить данную проблему?
Ранее отдел маркетинга мог просто работать со структурами данных, а теперь им приходится ждать пока отдел разработки реализует необходимые функции. Это тормозит работу
Отделу маркетинга необходимо изменять товары в корзине, чтобы реализовать планы по их продвижению. Им необходимо делать доставку бесплатной для товара или делать его стоимость равной нулю
Рассмотрим функции, которые были реализованы для решения этих задач
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, 'price', price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setQuantitiyByName(cart, name, quantity) {
var item = cart[name];
var newItem = objectSet(item, 'quantity', quantity);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setShippingByName(cart, name, shipping) {
var item = cart[name];
var newItem = objectSet(item, 'shipping', shipping);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setTaxByName(cart, name, tax) {
var item = cart[name];
var newItem = objectSet(item, 'tax', tax);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
У неявного аргумента в названии метода есть две характеристики:
Если мы определили, что у функций присутствует неявный аргумент в названии, то мы можем преобразовать его в явный
Шаги рефакторинга:
Реализуем функцию setFieldByName
, которая позволит изменять любое поле товара
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, 'price', price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setFieldByName(cart, name, field, value) {
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
cart = setPriceByName(cart, "shoe", 13);
cart = setQuantityByName(cart, "shoe", 3);
cart = setShippingByName(cart, "shoe", 0);
cart = setTaxByName(cart, "shoe", 2.34);
cart = setFieldByName(cart, "shoe", "price", 13);
cart = setFieldByName(cart, "shoe", "quantity", 3);
cart = setFieldByName(cart, "shoe", "shipping", 0);
cart = setFieldByName(cart, "shoe", "tax", 2.34);
Мы получили очень гибкий инструмент, возможно даже слишком гибкий, так как для обозначения поля мы используем строки
Общение между отделом маркетинга (М) и разработчиками (Р)
В языке JavaScript и во многих других языках программирования многие функции не являются объектами первого рода
Посмотрим на действия, которые мы можем сделать с числом: передать в функцию, вернуть из функции, сохранить в переменную, поместить как значение в массиве или ассоциативном массиве. Аналогично для других структур данных
Мы не можем сделать это для (например):
if
, ключевого слова for
Они не являются объектами первого рода
Мы создали в прошлый раз следующее изменение:
function setPriceByName(cart, name, price)
function setFieldByName(cart, name, field, value)
Мы смогли взять элемент языка, название функции, которое не является объектом первого рода, и заменили её на аргумент функции, строку, которая является объектом первого рода. Это позволило избавиться от дублирования
Данный подход к преобразованию элементов языка в объекты первого рода будет использован во второй части курса
Сейчас имя поля передаётся в качестве строки. При наборе данной строки легко совершить ошибку
Проверка кода может осуществляться:
Если бы язык программирования поддерживал концепцию перечисления, то мы могли бы заменить строку на элемент перечисления. Тогда проверку можно было бы провести в фазе статического анализа
В рамках языка JavaScript, в котором нет перечисления, мы могли бы реализовать проверку во время выполнения
var validItemFields = ['price', 'quantity', 'shipping',
'tax'];
function setFieldByName(cart, name, field, value) {
if(!validItemFields.includes(field))
throw "Not a vaild item field: '" + field "'.";
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Мы в барьер из абстракций переносим имена полей объекта товар. Ранее они находились за барьером и пользователи их не видели. Не ломаем ли мы барьер из абстракций?
Рассмотрим ситуацию, когда мы хотим изменить поле quantity
на number
. В этом случае мы можем реализовать замену на уровне реализации
var validItemFields = ['price', 'quantity', 'shipping',
'tax'];
var translations = { 'quantity': 'number' }
function setFieldByName(cart, name, field, value) {
if(!validItemFields.includes(field))
throw "Not a vaild item field: '" + field "'.";
if(translations.hasOwnProperty(field))
field = translations[field];
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Кто-то в команде написал следующий набор функций, имеющих неявный аргумент в именах функций. Примените к ним рефакторинг по выделению явного аргумента, чтобы убрать дублирование
function multiplyByFour(x) {
return x * 4;
}
function multiplyBySix(x) {
return x * 6;
}
function multiplyBy12(x) {
return x * 12;
}
function multiplyByPi(x) {
return x * 3.14159;
}
Ответ:
function multiply(x, y) {
return x * y;
}
В рамках разработки пользовательского интерфейса были реализованы две функции-обработчика нажатия на кнопки увеличения количества и увеличения размера одежды. Примените к данному коду такой же рефакторинг:
function incrementQuantityByName(cart, name) {
var item = cart[item];
var quantity = item['quantity'];
var newQuantity = quantity + 1;
var newItem = objectSet(item, 'quantity', newQuantity);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function increementSizeByName(cart, name) {
var item = cart[name];
var size = item['size'];
var newSize = size + 1;
var newItem = objectSet(item, 'size', newSize);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Ответ:
function incrementFieldByName(cart, name, field) {
var item = cart[item];
var value = item[field];
var newValue = value + 1;
var newItem = objectSet(item, field, newValue);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Команда разработки озабочена тем, что люди начинают использовать последний метод для увеличения значения других полей. Однако это не разумно для полей названия или цены.
Реализуйте защиту от попыток изменения других полей элемента корзины
function incrementFieldByName(cart, name, field) {
if(field !== 'size' && field !== 'quantity') {
throw "This item field can not be incremented " +
field + "'.";
}
var item = cart[item];
var value = item[field];
var newValue = value + 1;
var newItem = objectSet(item, field, newValue);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Мы добились возможности описания свойств с помощью объектов первого рода
В любом случае мы стараемся обращаться с данными напрямую, без создания специфических обёрток. Созданные интерфейсы только обеспечивают интерпретацию этих данных в рамках данного сценария использования
Корзина и товар в корзине сами по себе описывают достаточно общие концепции и находятся «внизу» графа вызовов
На основании этих общих структур можно построить различные специфические интерфейсы и барьеры из абстракций
Ведётся активное противостояние между сторонниками каждого из подходов к формированию особенностей языка, ни одна из сторон не одержала победу
Есть исследование, согласно которому на качество результирующего продукта сильнее влияет качество сна программиста
В рамках книги и курса используется динамически типизированный язык JavaScript ввиду своей популярности и использования распространённого Си-подобного синтаксиса
Выбор языка программирования для разработки конкретного приложения обусловлен множеством факторов. Главное, чтобы финальный выбор устраивал всех членов команды и они могли выспаться
Во многих языках программирования используются строки (или подобные им сущности) для идентификации полей в рамках ассоциативных массивов (JavaScript, Ruby, Clojure, Python). Ошибки в данных строках могут приводить к ошибкам
Эмпирический факт: на таких языках разработаны критически-важные сервисы
В современных веб-приложениях для общения между клиентом и сервером используется JSON. Для общения приложения с базой данных SQL
То есть функционирование систем зависит от корректности формирования строковых запросов друг другу. Даже если бы они были бинарными, то это оставляет широкое пространство для появления ошибок
Даже в языках со статической типизацией необходимо проверять данные, приходящие извне: от пользователя, посредством HTTP-запроса и т.д.
Ранее мы рассмотрели, что в языке JavaScript много элементов не являются сущностями первого рода
Например, мы не можем присвоить оператор сложения в переменную
Однако мы можем реализовать эквивалент данной операции на основе функции:
function plus(a, b) {
return a + b;
}
На первый взгляд это может показаться глупым, но давайте рассмотрим возможные использования для функции сложения, которую будем использовать как сущность первого рода
Реализуйте похожие функции для других математических операций: умножение, вычитание и деление
function times(a, b) {
return a * b;
}
function divideBy(a, b) {
return a / b;
}
function minus(a, b) {
return a - b;
}
При разработке своих функций отдел маркетинга совершает множество ошибок, в частности при реализации обхода по списку товаров. Они хотят иметь возможность не использовать for-циклы
Для решения этой задачи мы можем воспользоваться подходом к оборачиванию данного действия функцией. Однако данная функция не будет использована как функция первого рода, она будет использована как функция высшего порядка
Функцией высшего порядка называют функцию, которая либо принимает другую функцию в качестве аргумента, либо возвращает новую функцию в качестве возвращаемого значения
Функции высшего порядка не могут быть реализованы, если язык не позволяет использовать функции как объекты первого рода
Рассмотрим два следующих метода, которые используют цикл for для решения типичных задач по приготовлению еды и мытья посуды
for(var i = 0; i < foods.length; i++) {
var food = foods[i];
cook(food);
eat(food);
}
for(var i = 0; i < dishes.length; i++) {
var dish = dishes[i];
wash(dish);
dry(dish);
putAvay(dish);
}
function cookAndExatFoods() {
for(var i = 0; i < foods.length; i++) {
var food = foods[i];
cook(food);
eat(food);
}
}
cookAndEatFoods();
function cleanDishes() {
for(var i = 0; i < dishes.length; i++) {
var dish = dishes[i];
wash(dish);
dry(dish);
putAvay(dish);
}
}
cleanDishes();
function cookAndExatFoods() {
for(var i = 0; i < foods.length; i++) {
var item = foods[i];
cook(item);
eat(item);
}
}
cookAndEatFoods();
function cleanDishes() {
for(var i = 0; i < dishes.length; i++) {
var item = dishes[i];
wash(item);
dry(item);
putAvay(item);
}
}
cleanDishes();
function cookAndExatFoods(array) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
cook(item);
eat(item);
}
}
cookAndEatFoods(foods);
function cleanDishes(array) {
for(var i = 0; i < array.length; i++) {
var item = arary[i];
wash(item);
dry(item);
putAvay(item);
}
}
cleanDishes(dishes);
function cookAndExatFoods(array) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
cookAndEat(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
cookAndEatFoods(foods);
function cleanDishes(array) {
for(var i = 0; i < array.length; i++) {
var item = arary[i];
clean(item);
}
}
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
cleanDishes(dishes);
function operateOnArray(array, f) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
f(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
cookAndEatFoods(foods, cookAndEat);
function operateOnArray(array, f) {
for(var i = 0; i < array.length; i++) {
var item = arary[i];
f(item);
}
}
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
cleanDishes(dishes, clean);
function forEach(array, f) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
f(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
forEach(foods, cookAndEat);
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
forEach(dishes, clean);
for(var i = 0; i < foods.length; i++) {
var food = foods[i];
cook(food);
eat(food);
}
for(var i = 0; i < dishes.length; i++) {
var dish = dishes[i];
wash(dish);
dry(dish);
putAway(dish);
}
forEach(foods, function (food) {
cook(food);
eat(food);
});
forEach(dishes, function (dish) {
wash(dish);
dry(dish);
putAway(dish);
});
forEach
позволяет пройтись по всем элементам массива без использования индексов, больше не надо писать цикл forforEach
является функцией высшего порядка, так как она принимает другую функцию в качестве своего аргументаРассмотрим общий процесс, который был проведён для создания функции высшего порядка на примере forEach
Кажется, что шагов очень много, рассмотрим другой подход
В рамках решения задачи повышения надёжности работы приложения было принято решение о сохранении информации об ошибке во внешней системе
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error); // Нужный вызов
}
Основная проблема — необходимо добавлять стереотипный код во многие места
Стоит отметить, что добавление информации об ошибке в систему журналирования не является нормальной стратегией для обработки исключительных ситуаций. Однако ситуации, которые программно обработать невозможно, необходимо записывать в такие системы, чтобы их можно было решить
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logToSnapErrors(error);
}
Порядок выполнения рефакторинга:
В языке JavaScript функция, которую передают в качестве аргумента, называется функцией обратного вызова, callback
// Оригинал
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
// Шаг № 1: Выделение частей
try { // Общий заголовок
saveUserData(user); // Тело
} catch (error) { // Общее окончание
logToSnapErrors(error);
}
// Шаг № 2: Выделение кода в функцию
function withLogging() {
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
}
withLogging();
// Шаг № 3: Выделение функции
function withLogging(f) {
try {
f();
} catch(error) {
logToSnapErrors(error);
}
}
withLogging(function() {
saveUserData(user);
});
Глобально определённые функции могут быть использованы везде в приложении
function saveCurrentUserData() {
saveUserData(user);
}
withLogging(saveCurrentUserData);
var anotherSave = function() {
saveUserData(user);
};
withLogging(anotherSave);
Функции могут быть определены внутри другой функции. Это позволяет им взаимодействовать с другими переменными, определёнными внутри данной функции
function someFunction() {
var saveCurrentUserData = function() {
saveUserData(user);
};
withLogging(saveCurrentUserData);
}
Функцию можно определить сразу в том месте, где она должна использоваться, она будет называться встроенной функции
Если функции после определения не было дано имя, то она является анонимной
withLogging(function() { saveUserData(user); });
При оборачивании кода функцией он не будет вызван в месте декларирования функции, а в месте вызова данной функции
После определения функций с ними можно выполнять различные действия:
// Сохранять в переменные
var f = function() {
saveUserData(user);
};
// Сохранять в массиве
array.push(function() {
saveUserData(user);
});
// Передавать в другие функции
withLogging(function() {
saveUserData(user);
});
Функции высшего порядка самостоятельно определяют время вызова переданных им функций
// Не вызывать функцию
function callOnThursday(f) {
if(today === "Thursday")
f(); // Вызов только по четвергам
}
// Вызвать функцию позже
function callTomorrow(f) {
sleep(oneDay);
f();
}
// Вызвать функцию в новом контексте
function withLogging(f) {
try {
f();
} catch(error) {
logToSnapErrors(error);
}
}
В некотором плане да: он позволяет устранить дублирование
Аналогично с функциями высшего порядка: они позволяют выполнить запуск функции вместо дублирования тела метода
Важный аспект: мы запускаем переданный код в нужном контексте
Предположим, что мы выполним следующий код
withLogging(saveUserData(user));
В этом случае метод withLogging не сможет выполнить свою функцию — обработать исключение