Улучшение структуры действий

Улучшение структуры действий #

Васильев Андрей Михайлович, 2022

Версии презентации


Содержание #

  • Уменьшение неявных входов и выходов для повышения повторного использования
  • Улучшение архитектуры кода путём разделения его на части

Соотнесение архитектуры и бизнес-требований #

При формировании сигнатуры метода необходимо выбирать уровень абстракции, который соотносится с её применением

В предыдущей лекции мы выполнили выделение вычислений, которое было достаточно механическим. Такой подход не обязательно приведёт к наилучшей архитектуре

Рассмотрим метод gets_free_shipping. Он должен отвечать на вопрос: будет ли доступна бесплатная доставка для корзины с добавленным предметом или нет. Но интерфейс функции сейчас написан в терминах цен:

function gets_free_shipping(total, item_price) {
    return item_price + total >= 20;
}

Логика вычисления стоимости корзины продублирована также в другом методе:

function calc_total(cart) {
  var total = 0;
  for(var i = 0; i < cart.length; i++) {
    var item = cart[i];
    total += item.price;
  }
  return total;
}

Метод для вычисления бесплатной доставки лучше описать в терминах корзины:

gets_free_shipping(total, item_price)
gets_free_shipping(cart)

Изменение сигнатуры метода #

Изменение метода не является рефакторингом, так как мы изменяем сигнатуру метода

// Оригинал
function gets_free_shipping(total, item_price) {
    return item_price + total >= 20;
}
// С новой сигнатурой
function gets_free_shipping(cart) {
    return calc_total(cart) >= 20;
}

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


Рассмотрим изменение в использовании данной функции

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_shipping_icons() {
  var buttons = get_buy_buttons_dom();
  for(var i = 0; i < buttons.length; i++) {
    var button = buttons[i];
    var item = button.item;
    var new_cart = add_item(shopping_cart, item.name,
        item.price);
    if(gets_free_shipping(new_cart))
      button.show_free_shipping_icon();
    else
      button.hide_free_shipping_icon();
  }
}

Обсуждение #

Количество строк кода увеличивается, разве это улучшение? #

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

При выполнении add_item всегда создаётся копия массива, не является ли это дорогой операцией? #

Создание копии действительно более затратно нежели модификация массива. Однако это компенсируется современными средами выполнения с развитыми сборщиками мусора. Фактически копии объектов создаются постоянно, например при работе со строками в JavaScript

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


Принцип: минимизируйте неявные входы и выходы #

Если избавиться от неявных входов и выходов, то получается вычисление

Однако если мы сможем преобразовать неявные входы и выходы у действия в явные, то его можно будет повторно использовать в другом окружении

Проблемы неявных входов и выходов:

  • Неявные входы и выходы могут быть заняты
  • Они могут быть в неправильном состоянии
  • Их невозможно подменить: вывод не в DOM, а в файл
  • Их трудно тестировать

Уменьшение неявных входов и выходов #

Применим данный подход к методу update_shippping_icons()

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;
    var new_cart = add_item(shopping_cart, item.name,
        item.price);
    if(gets_free_shipping(new_cart))
      button.show_free_shipping_icon();
    else
      button.hide_free_shipping_icon();
  }
}

function update_shipping_icons(cart) {
  var buttons = get_buy_buttons_dom();
  for(var i = 0; i < buttons.length; i++) {
    var button = buttons[i];
    var item = button.item;
    var new_cart = add_item(cart, item.name, item.price);
    if(gets_free_shipping(new_cart))
      button.show_free_shipping_icon();
    else
      button.hide_free_shipping_icon();
  }
}

// Изначальное
function calc_cart_total() {
  shopping_cart_total = calc_total(shopping_cart);
  set_cart_total_dom();
  update_shipping_icons();
  update_tax_dom();
}

// Изменённое
function calc_cart_total() {
  shopping_cart_total = calc_total(shopping_cart);
  set_cart_total_dom();
  update_shipping_icons(shopping_cart);
  update_tax_dom();
}

Тренировка #

Применим данную технику к другим действиям

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 set_cart_total_dom() {
  ...
  shopping_cart_total
  ...
}

function update_shipping_icons(cart) {
  var buttons = get_buy_buttons_dom();
  for(var i = 0; i < buttons.length; i++) {
    var button = buttons[i];
    var item = button.item;
    var new_cart = add_item(cart, item.name, item.price);
    if(gets_free_shipping(new_cart))
      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_to_cart(name, price) {
  shopping_cart = add_item(shopping_cart, name, price);
  calc_cart_total(shopping_cart);
}

// Является логическим продолжением предыдущей функции
function calc_cart_total(cart) {
  var total = calc_total(cart);
  set_cart_total_dom(total);
  update_shipping_icons(cart);
  update_tax_dom(total);
  // Запись в глобальную переменную, которая не используется
  shopping_cart_total = total;
}

funciton set_cart_total_tom(total) {
  ...
}

function update_shipping_icons(cart) {
  var buy_buttos = get_buy_buttons_dom();
  for(var i = 0; i < buy_buttons.length; i++) {
    var button = buy_buttons[i];
    var item = button.item;
    var new_cart = add_item(cart, item.name, item.price);
    if(gets_free_shipping(new_cart)) {
      button.show_free_shipping_icon();
    } else {
      button.hide_free_shipping_icon();
    }
  }
}

function update_tax_dom(total) {
  set_tax_dom(calc_tax(total));
}

function add_item_to_cart(name, price) {
  shopping_cart = add_item(shopping_cart, name, price);
  var total = calc_total(shopiing_cart);
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart);
  update_tax_dom(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(cart) {
  return calc_total(cart) >= 20;
}

// Б
funciton tax_amount(amount) {
  return amount * 0.10;
}

Проектирование архитектуры — как правильно отделить части друг от друга #

Функции являются естественным способом для разделения проблем. Они отделяют место появления аргументов от места их использования

Зачастую программисты стремятся создать что-то монументальное и сложное, соединяя множество вещей вместе. Однако если стремиться разделять задачи и делать функции небольшими, то создавать сложные вещи станет просто путём композиции функций

  • Небольшие функции легче повторно использовать
  • Небольшие функции легче понимать и поддерживать
  • Небольшие функции легче тестировать

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


Улучшение структуры метода add_item #

Проанализируем работу метода add_item:

function add_item(cart, name, price) {
  var new_cart = cart.slice(); // Создание копии
  new_cart.push({ // Добавить объект корзины
    name: name, // Создать объект корзины
    price: price
  });
  return new_cart; // Возвращение копии
}

В настоящий момент функция знает о структуре корзины и структуре элемента корзины


Выделение функции-конструктора #

Разделим знания о частях предметной области, выделив функцию-конструктор

function make_cart_item(name, price) {
  return {
    name: name,
    price: price
  };
}

function add_item(cart, item) {
  var new_cart = cart.slice();
  new_cart.push(item);
  return new_cart;
}

add_item(shopping_cart, make_cart_item("shoes", 3.45));

Разделение областей ответственности #

  • Новая функция не знает ничего о структуре корзины
  • Функция add_item оперирует исключительно над корзиной

Таким образом структуры корзины и элемента корзины могут эволюционировать независимо друг от друга

Работа метода add_item #

В текущем виде add_item реализует стратегию copy-on-write, копирование при записи. Эта стратегия позволяет добиться неизменяемости данных


Выделение шаблона копирование при записи #

Функция add_item в настоящий момент может быть повторно использована, так как оперирует только над массивами и реализует шаблон копирование при записи

Для обобщения функции необходимо изменить её сигнатуру, так как текущая сигнатура связана с корзиной

function add_item(cart, item) {
  var new_cart = cart.slice();
  new_cart.push(item);
  return new_cart;
}

function add_element_last(array, element) {
  var new_array = array.slice();
  new_array.push(element);
  return new_array;
}

function add_item(cart, item) {
  return add_element_last(cart, item);
}

Вызов изменённого метода add_item #

function add_item_to_cart(name, price) {
  shopping_cart = add_item(shopping_cart, name, price);
  var total = calc_total(shopiing_cart);
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart);
  update_tax_dom(total);
}

function add_item_to_cart(name, price) {
  var item = make_cart_item(name, price);
  shopping_cart = add_item(shopping_cart, item);
  var total = calc_total(shopiing_cart);
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart);
  update_tax_dom(total);
}

Категоризация вычислений #

Добавим новую категорию: М — работа с массивами

// М
function add_element_last(array, element) {
  var new_array = array.slice();
  new_array.push(element);
  return new_array;
}

// К
function add_item(cart, item) {
  return add_element_last(cart, item);
}

// Т
function make_cart_item(name, price) {
  return {
    name: name,
    price: price
  };
}

// К Т Б
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(cart) {
  return calc_total(cart) >= 20;
}

// Б
funciton tax_amount(amount) {
  return amount * 0.10;
}

Вопрос-ответ #

Зачем выполнять категоризацию функций? #

Это предварительный взгляд на стратификационную архитектуру методов, которая будет детально рассмотрена позднее

В чём разница между бизнес-правилами и операциями над корзиной для онлайн магазина? #

Большинство онлайн магазинов содержат в себе корзину для товаров. Правила работы с корзиной будут общими для всех этих магазинов.

Правила бесплатной доставки применимы исключительно для магазина из примера

Может ли функция быть одновременно и бизнесс-правилом и операцией над корзиной? #

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


Разделение функции #

Данная функция работает с кнопками, с корзиной и взаимодействует с DOM

function update_shipping_icons(cart) {
  var buy_buttos = get_buy_buttons_dom();
  for(var i = 0; i < buy_buttons.length; i++) {
    var button = buy_buttons[i];
    var item = button.item;
    var new_cart = add_item(cart, item.name, item.price);
    if(gets_free_shipping(new_cart)) {
      button.show_free_shipping_icon();
    } else {
      button.hide_free_shipping_icon();
    }
  }
}

Более мелкие функции и больше вычислений #

Определим типы исходного кода

// Действие, глобалная переменная
var shopping_cart = [];

// Действие
function add_item_to_cart(name, price) {
  var item = make_cart_item(name, price);
  shopping_cart = add_item(shopping_cart, item);
  var total = calt_total(shopping_cart);
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart);
  update_tax_dom(total);
}

// Действие
function update_shipping_icons(cart) {
  var buy_buttos = get_buy_buttons_dom();
  for(var i = 0; i < buy_buttons.length; i++) {
    var button = buy_buttons[i];
    var item = button.item;
    var new_cart = add_item(cart, item.name, item.price);
    if(gets_free_shipping(new_cart)) {
      button.show_free_shipping_icon();
    } else {
      button.hide_free_shipping_icon();
    }
  }
}

// Действие
function update_tax_dom(total) {
  set_tax_dom(calc_tax(total));
}

// Вычисление
function add_element_last(array, element) {
  var new_array = array.slice();
  new_array.push(element);
  return new_array;
}

// Вычисление
function add_item(cart, item) {
  return add_element_last(cart, item);
}

// Вычисление
function make_cart_item(name, price) {
  return {
    name: name,
    price: price
  };
}

// Вычисление
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(cart) {
  return calc_total(cart) >= 20;
}

// Вычисление
function calc_taxt(amount) {
  return amount * 0.10;
}

Заключение #

После изменения кода действий они уже не зависят от структуры наших данных. Также мы смогли выделить функции, которые легко повторно использовать

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

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