Выделение вычислений из действий

Выделение вычислений из действий #

Васильев Андрей Михайлович, 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


Тестирование логики #

  1. Запустить веб-браузер
  2. Загрузить страницу приложения
  3. Нажать на кнопки для добавления товаров корзину
  4. Дождаться изменения DOM-модели
  5. Прочитать значения из DOM-модели
  6. Преобразовать строки в числа
  7. Сравнить с ожидаемым значением

Это всё необходимо для тестирования логики вычисления величины налога

Предложения по облегчению тестирования #

  • Разделить бизнесс-логику от взаимодействия с 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;
}

Заключение #

  • Функции являются действиями, если у них есть неявные входы и выходы
  • Функции являются вычислениями, если у них только явные входы и выходы
  • Общие переменные (глобальные, поля класса) являются общими неявными входами и выходами
  • Неявные входы обычно могут быть заменены аргументами
  • Неявные выходы обычно могут быть заменены на возвращаемое значение
  • При применении принципов ФП количество вычислений в коде будет возрастать

© A. M. Васильев, 2024, CC BY-SA 4.0, andrey@crafted.su