Васильев Андрей Михайлович, 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();
}
}
Количество строк — это хорошая метрика, которая показывает сложность кодовой базы. Однако с точки зрения количества строк кода в вычислениях он является образцовым
Создание копии действительно более затратно нежели модификация массива. Однако это компенсируется современными средами выполнения с развитыми сборщиками мусора. Фактически копии объектов создаются постоянно, например при работе со строками в JavaScript
Польза от копирования превышает накладные расходы от копирования. Если код, использующий копирование, недостаточно производительный, то его можно оптимизировать. Но не стоит оптимизировать код преждевременно
Если избавиться от неявных входов и выходов, то получается вычисление
Однако если мы сможем преобразовать неявные входы и выходы у действия в явные, то его можно будет повторно использовать в другом окружении
Проблемы неявных входов и выходов:
Применим данный подход к методу 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 реализует стратегию 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);
}
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;
}
После изменения кода действий они уже не зависят от структуры наших данных. Также мы смогли выделить функции, которые легко повторно использовать