Выделение вычислений из действий #
Васильев Андрей Михайлович, 2022
Версии презентации
Обзор лекции #
В рамках данной лекции мы рассмотрим каким образом можно выделить вычисления из данных
- Мы посмотрим на существующее приложение
- В него будет добавлена новая функциональность
- Будут выделены вычисления из действий
Онлайн магазин #
Одна из ключевых функций приложения — отображение общей стоимости товаров, которые были добавлены в корзину
Рассмотрим реализацию данной функции
var shopping_cart = []; // Глобальная переменная
var shopping_cart_total = 0; // Глобальная переменная
function add_item_to_cart(name, price) {
shopping_cart.push({
name: name,
price: price
});
cart_calc_total();
}
function cart_calc_total() {
shopping_cart_total = 0;
for(var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
shopping_cart_total += item.price;
}
set_cart_total_dom(); // Обновление DOM
}
Бесплатная доставка #
Магазин хочет предоставить бесплатную доставку, если общая стоимость заказа превышает 20 долларов
Наша задача — показать иконку рядом с товаром, если его добавление в корзину приведёт к увеличению стоимости товаров в корзине больше 20 долларов
Рассмотрим императивную реализацию:
function update_shipping_icons() {
var buy_buttons = get_buy_buttons_dom();
for(var i = 0; i < buy_buttons.length; i++) {
var button = buy_buttons[i];
var item = button.item;
if(item.price + shopping_cart_total >= 20)
button.show_free_shipping_icon();
else
button.hide_free_shipping_icon();
}
}
Беслпатная доставка продолжение #
Созданную функцию будем вызывать сразу после вычисления
function cart_calc_total() {
shopping_cart_total = 0;
for(var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
shopping_cart_total += item.price;
}
set_cart_total_dom();
update_shopping_icons(); // Обновление иконок
}
Вычисление налогов #
Далее к нам пришло требование вычислять значение налогового сбора и обновлять его каждый раз при изменении общей суммы корзины
function update_tax_dom() {
set_tax_dom(shopping_cart_total * 0.10);
}
Добавим вызов этой функции в конец метода calc_cart_total
Тестирование логики #
- Запустить веб-браузер
- Загрузить страницу приложения
- Нажать на кнопки для добавления товаров корзину
- Дождаться изменения DOM-модели
- Прочитать значения из DOM-модели
- Преобразовать строки в числа
- Сравнить с ожидаемым значением
Это всё необходимо для тестирования логики вычисления величины налога
Предложения по облегчению тестирования #
- Разделить бизнесс-логику от взаимодействия с DOM
- Не опираться на глобальные переменные
Вопрос повторного использования кода #
Бухгалтерия и отдел доставки хотят использовать код, но не могут:
- Код считывает корзину из глобальных переменных и не может работать с БД
- Код записывает информацию сразу в DOM
Предложения по обобщению кода #
- Код не должен зависеть от глобальных переменных
- Код не должен предполагать запись в DOM
- Функции должны возвращать результат вычисления
Определение типа кода #
Определим тип кода для каждого ключевого компонента
var shopping_cart = [];
var shopping_cart_total=[];
function add_item_to_cart(name, price) {
shopping_cart.push({
...
}
function update_shipping_icons() {
var buy_buttons = get_buy_buttons_dom();
...
}
function update_tax_dom() {
set_tax_dom(shopping_cart_total * 0.10);
}
function calc_cart_total() {
shopping_cart_total = 0;
...
}
Определение типа кода #
// Изменяемая глобальная переменная
var shopping_cart = [];
// Изменяемая глобальная переменная
var shopping_cart_total=[];
function add_item_to_cart(name, price) {
// Изменяем глобальную переменную
shopping_cart.push({
...
}
function update_shipping_icons() {
// Чтение данных из DOM, глобального состояния
var buy_buttons = get_buy_buttons_dom();
...
}
function update_tax_dom() {
// Запись данных в DOM, глобальное состояние
set_tax_dom(shopping_cart_total * 0.10);
}
function calc_cart_total() {
// Изменение глобальной переменной
shopping_cart_total = 0;
...
}
Входы и выходы функций #
- Входами функций называются данные, которые используются функцией во время вычислений
- Выходами функций называются данные или действия, которые возникают в результате выполнения функции
var total = 0;
function add_to_total(amount) { // Вход
console.log("Old total: " + total); // Вход
total += amount; // Выход
return total; // Выход
}
Явные и неявные входы и выходы #
- Аргументы функции являются явными входами
- Возвращаемое значение является явным выходом
- Все остальные данные являются неявными входами и выходами
var total = 0;
function add_to_total(amount) { // Явный вход
console.log("Old total: " + total); // Неявный вход
total += amount; // Неявный выход
return total; // Явный выход
}
- Если у функции есть неявные входы и выходы, то она является действием
- Если у функции-действия заменить неявные входы и выходы на явные, то она станет вычислением
Неявные входы и выходы также можно назвать побочными эффектами
Предложения по улучшению кода #
-
Тестирование 1: отделить бизнес-правила от изменения DOM
Взаимодействие с DOM является неявным взаимодействием. Его необходимо выполнить, но отдельно от вычислений
-
Тестирование 2: не использовать глобальные переменные
Глобальные переменные являются неявными входами для всего кода
-
Переиспользование 1: не использовать глобальные переменные
-
Переиспользование 2: не предполагать запись данных в DOM
-
Переиспользование 3: возвращайте результат вычисления из функций
Выделение вычислений из действий #
- Изолируем код для вычислений в отдельном методе
- Преобразуем неявные входы и выходы в явные
// Оригинал
function calc_cart_total() {
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
shopping_cart_total += item.price;
}
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
// Выделение метода
function calc_cart_total() {
calc_total();
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
function calc_total() {
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
shopping_cart_total += item.price;
}
}
Займёмся преобразований неявных входов и выходов
// Оригинал
function calc_total() {
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
shopping_cart_total += item.price;
}
}
// Вызов метода
calc_total();
// Добавляем явный вывод
function calc_total() {
var total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
total += item.price;
}
return total;
}
// Вызов метода
shopping_cart_total = calc_total();
// Добавляем явный вывод
function calc_total() {
var total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
var item = shopping_cart[i];
total += item.price;
}
return total;
}
// Вызов метода
shopping_cart_total = calc_total();
// Добавляем явный вход
function calc_total(cart) {
var total = 0;
for (var i = 0; i < cart.length; i++) {
var item = cart[i];
total += item.price;
}
return total;
}
// Вызов метода
shopping_cart_total = calc_total(shopping_cart);
Выделение вычисления в add_item_to_cart #
Рассмотрим метод add_item_to_cart
// Оригинал
function add_item_to_cart(name, price) {
shopping_cart.push({
name: name,
price: price
});
}
// Выделение метода
function add_item_to_cart(name, price) {
add_item(name, price);
calc_cart_total();
}
function add_item(name, price) {
shopping_cart.push({
name: name,
price: price
});
}
// Избавляемся от неявных выходов
function add_item_to_cart(name, price) {
add_item(shopping_cart, name, price);
calc_cart_total();
}
function add_item(cart, name, price) {
cart.push({
name: name,
price: price
});
}
Модификация аргумента #
Текущая реализация модифицирует переданный аргумент, делая его неявным выводом
// Избавляемся от неявных выходов
function add_item_to_cart(name, price) {
add_item(shopping_cart, name, price);
calc_cart_total();
}
function add_item(cart, name, price) {
cart.push({
name: name,
price: price
});
}
// Избавляемся от неявного вывода
function add_item_to_cart(name, price) {
shopping_cart = add_item(shopping_cart, name, price);
calc_cart_total();
}
function add_item(cart, name, price) {
var new_cart = cart.slice(); // Создаём копию
new_cart.push({ // Модифицируем копию
name: name,
price: price
});
return new_cart; // Возвращаем новый список
}
Секция вопросов-ответов #
Проблема увеличения объёма исходного кода
- Считается, что чем меньше кода тем лучше, но объём кода только увеличился
- Выделение функций улучшило архитектуру приложения, облегчило тестирование
Какую ещё пользу кроме тестирования и повторного использования несут ФЯП?
Подходы ФЯП также полезны для организации архитектуры, многопоточного программирования и моделирования данных
Ряд разработанных методов можно применить за рамками целевой задачи, так и задумывалось?
Чем больше небольших функций, тем легче переиспользовать, тестировать и понимать
Функции-вычисления модифицируют локальные переменные, где же чистота функций и неизменность?
- Данные необходимо как минимум инициализировать, а это является изменением
- Переменные изменяются в локальном контексте функции, снаружи это недоступно
Процедура выделения вычислений из действий #
№1 Выделите код вычисления в отдельную функцию
- Определите код вычисления, перенести его в новую функцию
- Выделите код в новую функцию, определив аргументы
- Вызовите новую функцию, передав необходимые аргументы
№2 Определите неявные входы и выходы функции
- Неявными входами являются чтение данных из переменных вне функции, общение с базой данных и т.д.
- Неявными выходами являются модифицирование глобальной переменной, общего объекта, отправка веб-запроса, модифицирование аргументов
№3 Преобразование неявных входов и выходов в явные
- Неявные входы должны стать аргументами
- Неявные выходы должны стать возвращаемыми значениями
- Вместо модификации аргументов выполните их копирование и модифицируйте только копию
Практика #
Задачи для самостоятельной проработки. Выделите вычисления из данных действий
function update_tax_dom() {
set_tax_dom(shipping_cart_total * 0.10);
}
function update_shipping_icons() {
var buy_buttons = get_buy_buttons_dom();
for(var i = 0; i < buy_buttons.length; i++) {
var button = buy_buttons[i];
var item = button.item;
if(item.price + shopping_cart_total >= 20)
button.show_free_shipping_icon();
else
button.hide_free_shipping_icon();
}
}
Результат преобразования #
var shopping_cart = []; // Действие
var shopping_cart_total = 0; // Действие
// Действие
function add_item_to_cart(name, price) {
shopping_cart = add_item(shopping_cart, name, price);
calc_cart_total();
}
// Действие
function calc_cart_total() {
shopping_cart_total = calc_total(shopping_cart);
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
// Действие
function update_shipping_icons() {
var buttons = get_buy_buttons_dom();
for(var i = 0; i < buttons.length; i++) {
var button = buttons[i];
var item = button.item;
if(gets_free_shipping(shipping_cart_total, item.price))
button.show_free_shipping_icon();
else
button.hide_free_shipping_icon();
}
}
// Действие
function update_tax_dom() {
set_tax_dom(calc_tax(shopping_cart_total));
}
// Вычисление
function add_item(cart, name, price) {
var new_cart = cart.slice();
new_cart.push({
name: name,
price: price
});
return new_cart;
}
// Вычисление
function calc_total(cart) {
var total = 0;
for(var i = 0; i < cart.length; i++) {
var item = cart[i];
total += item.price;
}
return total;
}
// Вычисление
function gets_free_shipping(total, item_price) {
return item_price + total >= 20;
}
// Вычисление
function calc_tax(amount) {
return amount * 0.10;
}
Заключение #
- Функции являются действиями, если у них есть неявные входы и выходы
- Функции являются вычислениями, если у них только явные входы и выходы
- Общие переменные (глобальные, поля класса) являются общими неявными входами и выходами
- Неявные входы обычно могут быть заменены аргументами
- Неявные выходы обычно могут быть заменены на возвращаемое значение
- При применении принципов ФП количество вычислений в коде будет возрастать