Васильев Андрей Михайлович, 2022
Версии презентации
В современной индустрии по разработке ПО зачастую под словом «дизайн ПО» могут подразумевать множество разных аспектов:
В рамках книги автор предлагает нам использовать следующее определение:
Дизайн ПО — существительное, использование собственного эстетического чувства в процессе принятия решений по разработке ПО, чтобы облегчить написание исходного кода, его тестирование и поддержку
Данное определение будет использовано далее в книге
В рамках данной лекции мы будем оттачивать данное эстетическое чувство путём применения стратифицированного дизайна
Стратифицированный дизайн — это техника для создания ПО в формате слоёв. Каждый слой содержит функции, которые оперируют терминами нижестоящего слоя
Важно отметить, что выделение слоёв это сложная задача
Как выработать чувство хорошего дизайна?
Мы можем читать исходный код и находить маркеры для применения стратифицированного дизайна. Мы смотрим на код, находим маркеры и используем их для выполнения действий
После идентификации маркеров мы можем выполнить ряд действий, чтобы улучшить дизайн исходного кода
Мы пока что не говорим о конкретных алгоритмах, а только о возможных действиях
Мы посмотрим на стратифицированный дизайн с разных углов. Однако остановимся в основном на четырёх подходах. Первый будет рассмотрен в рамках этого занятия, три остальных в рамках следующего
Слоёная структура стратифицированного дизайна должна помочь в построении несложных реализаций. Такие функции реализуют свою задачу с должным уровнем детализации в своём теле. Слишком большое количество деталей реализации является маркером
Некоторые страты предоставляют интерфейс, который позволяет спрятать важные детали реализации. Эти слои позволяют писать код на более высоком уровне, освободив ментальную энергию, чтобы решать задачи на более высоком уровне
Вместе с развитием системы мы хотим, чтобы интерфейсы к важным бизнес концепциям сходились к небольшому, но мощному набору операций. Другие операции должны быть определены в терминах того интерфейса прямо или косвенно
Шаблоны и практики стратифицированного дизайна должны служить нашим требованиям как программистам, кто в свою очередь служит интересам бизнеса. Мы должны вкладывать усилия в слои, которые будут помогать доставлять ПО быстрее и качественнее. Реализация слоёв не является самоцелью. Исходный код и уровни абстракции, которые он формирует должен быть удобным для работы
В данной лекции мы рассмотрим подходы к тому как мы можем сделать наши реализации более удобными для восприятия. Рассмотрим следующую функцию
// Функция добавляет бесплатную заколку для галстука
// если её нет в корзине
function freeTieClip(cart) {
var hasTie = false;
var hasTieClip = false;
for(var i = 0; i < cart.length; i++) {
var item = cart[i];
if(item.name === "tie")
hasTie = true;
if(item.name === "tie clip")
hasTieClip = true;
}
if(hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
Особенности метода
Разработчики приложения решили выполнить реорганизацию кода и привести код для работы с корзиной в порядок. Первый этап: сформировать список операций над корзиной
add_item(cart, item)
remove_item_by_name(cart, name)
calc_total(cart)
setPriceByName()
cartTax(cart)
gets_free_shipping(cart)
Если бы у нас была функция, которая ответит о наличии товара в корзине, то нам было бы проще реализовать функцию freeTieClip()
// Оригинал
function freeTieClip(cart) {
var hasTie = false;
var hasTieClip = false;
for(var i = 0; i < cart.length; i++) {
var item = cart[i];
if(item.name === "tie")
hasTie = true;
if(item.name === "tie clip")
hasTieClip = true;
}
if(hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
// Новая функция общего назначения для корзины
function isInCart(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return true;
}
return false;
}
// Используем метод
function freeTieClip(cart) {
var hasTie = isInCart(cart, "tie");
var hasTieClip = isInCart(cart, "tie clip");
if (hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
Новая реализация написана в терминах интерфейса корзины
Рассмотрим оригинальную реализацию функции freeTieClip()
с помощью графа вызовов. В графе будем указывать как использованные методы, так и использованные средства языка
Все ли вызываемые элементы имеют один и тот же уровень абстракции? Нет, функции предоставляют более высокий уровень абстракции
freeTieClip()
В данном случае код явно был переусложнённым. Граф вызовов это только подтвердил.
Однако у нас сейчас только 2 слоя в графе. Когда мы станем добавлять новые функции, у диаграммы станет больше слоёв. Диаграмма предоставляет высокоуровневый взгляд на устройство слоёв с точки зрения системы
Большую часть времени нет. Мы представляем их. Этот навык очень легко сформировать
Такую диаграмму удобно изобразить на доске для обсуждения архитектуры, чтобы визуализировать проектные решения. Визуализация помогает фокусировать обсуждения
Статифицированный дизайн предлагает определённый взгляд на код, который люди научились успешно применять. Это позволяет находить пути для повышения повторной применимости, улучшения тестирования и облегчения поддержки исходного кода. Если такой взгляд не помогает в текущей ситуации с кодом, значит не стоит его использовать
Нет, вы делаете всё верно. Может быть вы фокусируетесь на уровне детализации, который важен для ваших целей, но не важным для целей курса. Можете попытаться выделить слои на том уровне, который считаете нужным. Нет никаких серьёзных ограничений.
remove_item_by_name
#
Сформируем граф вызовов, выделим все функции и особенности ЯП, которые он вызывает
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name == name)
idx = i;
}
if(idx !== null)
return removeItems(cart, index, 1);
return cart;
}
Предположим, что мы хотим объединить предыдущий граф и граф вызовов freeTieClip()
Где должна расположиться функция remove_item_by_name
: выше freeTieClip(), на уровне, ниже, на уровне функций make_item
или ниже их? Обоснуйте своё решение
Метод freeTieClip()
описывает правила маркетинговой компании. Метод remove_item_by_name
явно не о маркетинге, это метод более общего назначения, он может быть использован в пользовательском интерфейсе, в другой маркетинговой компании и т.д.
Таким образом эта функция должна находиться ниже freeTieClip()
. Первые 2 варианта были ликвидированы
По названию метод работает на уровне функций add_item
, isInCart
, так как предоставляет абстракцию для работы с корзиной. Пока что это наилучший кандидат.
Функция remove_item_by_name
точно не находится ниже данных методов, так как он не нужен для их реализации.
freeTieClip()
?
#
Мы поняли, что функция не находится выше или ниже существующих. Можем ли мы с уверенностью сказать, что функция принадлежит новому слою?
С определённостью в 100% нет. Однако мы можем посмотреть детально на функции нижнего слоя, выяснив их зависимости. Если у методов будут общие зависимости, значит они скорее всего принадлежат одному слою
Методы isInCart
и remove_item_by_name
оба зависят от двух методов, что является хорошим доказательством их принадлежности к одному слою.
Далее мы рассмотрим ещё способы для подтверждения данного факта
function isInCart(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return true;
}
return false;
}
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name == name)
idx = i;
}
if(idx !== null)
return removeItems(cart, index, 1);
return cart;
}
Рассмотрим все методы, которые сейчас реализованы для работы с корзиной. Часть из них мы уже добавили в диаграмму, часть нет. Задача состоит в том, чтобы добавить их к диаграмме
function freeTieClip(cart) {
var hasTie = isInCart(cart, "tie");
var hasTieClip = isInCart(cart, "tie clip");
if (hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
function isInCart(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return true;
}
return false;
}
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name == name)
idx = i;
}
if(idx !== null)
return removeItems(cart, index, 1);
return cart;
}
function add_item(cart, item) {
return add_element_last(cart, item);
}
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_gree_shipping(cart) {
return calc_total(cart) >= 20;
}
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice();
for(var i=0; i < cartCopy.length; i++) {
if(cartCopy[i].name === name)
cartCopy[i] = setPrice(cartCopy[i], price);
}
return cartCopy;
}
function cartTax(cart) {
return calc_tax(calc_total(cart));
}
Текущий граф вызовов
Скорее всего нет. Если ваш граф удовлетворяет следующим требованиям, то всё в порядке:
Слои на диаграмме отображают уровни абстракции, которые они предоставляют. Рассмотрим каждый из них далее.
Функции внутри слоя должны служить общей цели
Хотя процесс выделения слоёв может быть сложным, можно выделить критерий успешного завершения: у каждого слоя можно выделить его назначение
В нашем случае можно выделить: Функции языка JavaScript, Реализация КПЗ-операций, Базовые операции над товарами, Базовые операции над корзиной, Общие бизнес правила, Бизнес правила для корзины
Посмотрим какой уровень масштаба рассмотрения данной диаграммы может помочь для идентификации проблемы
На данном уровне мы видим граф вызовов целиком. Это масштаб по умолчанию. Он позволяет нам видеть всё, включая взаимодействие между слоями.
На данном уровне масштаба мы выделяем интересующий нас слой. Из всего графа мы оставляем только этот слой и все элементы, от которых он зависит. Т.е. мы видим как слой устроен
На данном уровне масштаба мы начинаем с рассмотрения одной функции и выделяем все элементы, от которых она зависит. Так мы можем выявить проблемы структуры функции.
Рассмотрим граф функции remove_item_by_name
Данная функция зависит от двух слоёв, это не простая, не понятная реализация
Можем ли мы упросить эту реализацию?
Наиболее общий способ решения данной проблемы — создание промежуточных функций. В нашем случае достаточно создать промежуточную функцию на уровне КПЗ-операций
Данная функция является такой же как выделение цикла for
в отдельную функцию. Такое действие мы уже выполняли
for
#
Мы можем выделить цикл из метода remove_item_by_name
. Цикл используется для линейного поиска элемента в массиве. Результат — индекс элемента
// Оригинал
function remove_item_by_name(cart, name) {
var index = null;
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name) {
index = i;
}
}
if(index !== null)
return removeItems(cart, index, 1);
return cart;
}
// Выделение функции
function remove_item_by_name(cart, name) {
var index = indexOfItem(cart, name);
if(index !== null)
return removeItems(cart, index, 1);
return cart;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
remove_item_by_name
легче к восприятию
indexOfItem()
находится выше removeItems
, так как знает о структуре товаровМетоды isInCart()
и indexOfItem()
содержат очень похожий код. Можно ли их как-то объединить? Может ли одна функция реализована в терминах другой?
function isInCart(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return true;
}
return false;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
Реализуйте одну из функций в терминах другой и оформите диаграмму вызовов, включающую обращение к циклу for
Метод indexOfItem
находится на слое, который ниже слоя isInCart
. Первую функцию можно использовать в коде, который знает, что корзина представлена массивом. Для второй достаточно узнать логическое свойство
function isInCart(cart, name) {
return indexOfItem(cart, name) !== null;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
Если мы посмотрим, то метод setPriceByName()
тоже очень похож на цикл внутри indexOfItem()
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice();
for(var i=0; i < cartCopy.length; i++) {
if(cartCopy[i].name === name)
cartCopy[i] = setPrice(cartCopy[i], price);
}
return cartCopy;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
Реализуйте один из методов в терминах другого и нарисуйте диаграмму вызовов, включая обращение к циклу for и индексам
Функция indexOfItem
также находится ниже функции setPriceByName
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice();
var index = indexOfItem(cart, name);
if(index !== null)
cartCopy[index] = setPrice(cartCopy[index], price);
return cartCopy;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
setPriceByName
всё-равно зависит от двух слоёвВ рамках предыдущего занятия мы реализовали КПЗ-метод для объектов и массивов. Используем его в setPriceByName
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice();
var index = indexOfItem(cart, name);
if(index !== null)
cartCopy[index] = setPrice(cartCopy[index], price);
return cartCopy;
}
function arraySet(array, index, value) {
var copy = array.slice();
copy[index] = value;
return copy;
}
function setPriceByName(cart, name, price) {
var index = indexOfItem(cart, name);
if(index !== null)
return arraySet(cart, index,
setPrice(cart[index], price));
return cart;
}
function arraySet(array, index, value) {
var copy = array.slice();
copy[index] = value;
return copy;
}
.slice
, заменив её более короткой до arraySet
setPriceByName
? Кажется, что граф связей становится всё сложнее, не проще
#
Мы можем легко заменить обращение к индексу функцией
// Оригинал
function setPriceByName(cart, name, price) {
var index = indexOfItem(cart, name);
if(index !== null)
return arraySet(cart, index,
setPrice(cart[index], price));
return cart;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
// Введём новую функцию
function arrayGet(array, index) {
return array[index];
}
function setPriceByName(cart, name, price) {
var index = indexOfItem(cart, name);
if(index !== null)
return arraySet(cart, index,
setPrice(arrayGet(cart, i), price));
return cart;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.name; i++) {
if(arrayGet(cart, i).name === name)
return i;
}
return null;
}