Стратифицированный дизайн. Часть № 1 #

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

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

Обзор #

  • Рабочее определение дизайна ПО
  • Что такое стратифицированный дизайн и как она может помочь команде
  • Как следует выделять функции, чтобы сделать код чище
  • Почему написание ПО в терминах слоёв (страт) позволяет лучше думать о задаче

Определение дизайна ПО #

В современной индустрии по разработке ПО зачастую под словом «дизайн ПО» могут подразумевать множество разных аспектов:

  • Дизайн, проектирование пользовательского интерфейса
  • Архитектура программного обеспечения
  • Подходы к решению задач разработки

В рамках книги автор предлагает нам использовать следующее определение:

Дизайн ПО — существительное, использование собственного эстетического чувства в процессе принятия решений по разработке ПО, чтобы облегчить написание исходного кода, его тестирование и поддержку

Данное определение будет использовано далее в книге

В рамках данной лекции мы будем оттачивать данное эстетическое чувство путём применения стратифицированного дизайна

Что такое стратифицированный дизайн? #

Стратифицированный дизайн — это техника для создания ПО в формате слоёв. Каждый слой содержит функции, которые оперируют терминами нижестоящего слоя

Важно отметить, что выделение слоёв это сложная задача

  • Необходимо знать что необходимо найти и как дальше поступить с найденной информацией
  • Слишком много факторов влияет на процесс, поэтому трудно сформулировать формулу «наилучшего дизайна»
  • Можно выработать чувство хорошего дизайна и следовать ему

Как выработать чувство хорошего дизайна?

  • В рамках лекции будем учиться смотреть на написанный код, чтобы выявить сигналы о том, где можно улучшить дизайн
  • Рассмотрим способы улучшения дизайна
  • Выполнив эту трудную работу, мы завершим эту главу с основами понимания хорошего дизайна, который можно оттачивать в следующие несколько лет

Развитие чувства к дизайну #

Проклятие экспертов #

  • Эксперт — высококлассный специалист в какой-либо предметной области
  • Экспертам бывает сложно описать способ достижения ими успешного результата
  • Одна из причин данной ситуации — высокая сложность умения или области

Маркеры стратифицированного дизайна #

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

  • Маркеры внутри функции: длина, сложность, уровень детализации, список вызываемых функций, использованные функции языка программирования
  • Маркеры структуры: длина стрелки, связность, уровень детализации

Действия на основании маркеров #

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

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

Организационные действия #

  • Где должна быть размещена новая функция?
  • Переместить функции в другие слои

Действия на уровне реализации #

  • Изменить реализацию метода
  • Выделить новую функцию
  • Изменить структуру данных

Изменения #

  • Где новый код должен быть написан?
  • Какой уровень детализации следует использовать?

Шаблоны стратифицированного дизайна #

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

Шаблон № 1: Несложная реализация #

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

Шаблон № 2: Барьеры из абстракций #

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

Шаблон № 3: Минимальный интерфейс #

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

Шаблон № 4: Удобные слои #

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

Шаблон № 1: Несложная реализация #

В данной лекции мы рассмотрим подходы к тому как мы можем сделать наши реализации более удобными для восприятия. Рассмотрим следующую функцию

// Функция добавляет бесплатную заколку для галстука
// если её нет в корзине
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));
}

Текущий граф вызовов

Решение #

Обсуждение #

Мой финальный граф похож, но другой. Я сделал что-то не так? #

Скорее всего нет. Если ваш граф удовлетворяет следующим требованиям, то всё в порядке:

  1. Все функции представлены на графе.
  2. Каждая функция связана с функциями, которые она вызывает.
  3. Все стрелки направлены сверху вниз, от верхних слоёв к нижним.

А почему организация была проведена именно на эти специфические слои? #

Слои на диаграмме отображают уровни абстракции, которые они предоставляют. Рассмотрим каждый из них далее.

Анализ выделенных слоёв #

Функции внутри слоя должны служить общей цели

Хотя процесс выделения слоёв может быть сложным, можно выделить критерий успешного завершения: у каждого слоя можно выделить его назначение

В нашем случае можно выделить: Функции языка JavaScript, Реализация КПЗ-операций, Базовые операции над товарами, Базовые операции над корзиной, Общие бизнес правила, Бизнес правила для корзины

  • Каждый из слоёв предоставляет свой уровень абстракций. При работе на уровне бизнес правил для корзины мы не должны решать вопросы организации корзины
  • Диаграмма отображает факты и набор интуитивных выводов: граф вызова функций и организация слоёв
  • Цель стратифицированного дизайна — формирование понятных реализаций

Три разных уровня масштаба #

Возможные проблемы стратифицированного дизайна #

  1. Проблемы во взаимодействии между слоями
  2. Проблемы в реализации конкретного слоя
  3. Реализация одной функции

Посмотрим какой уровень масштаба рассмотрения данной диаграммы может помочь для идентификации проблемы

1. Глобальный уровень масштаба #

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

2. Масштаб одного слоя #

На данном уровне масштаба мы выделяем интересующий нас слой. Из всего графа мы оставляем только этот слой и все элементы, от которых он зависит. Т.е. мы видим как слой устроен

3. Масштаб одной функции #

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

Слой базовых операций с корзиной #

  • Данная диаграмма может выглядеть запутанной. Но запутанность получается из-за стрелок, которые представляют факты о наших функциях
  • Запутанность диаграммы показывает запутанность внутри кода
  • Если бы реализация методов была простой, то длина всех стрелок была бы примерно одинаковой длины
  • В нашем случае ряд стрелок имеют длину в один слой, некоторые в 3 слоя

Слой одной функции #

Рассмотрим граф функции 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;
}
  1. Нарисуйте диаграммы вызовов оригинала и изменённых функций
  2. Сформулируйте утверждения, подтверждающие следующие точки зрения:
    • Использовать доступ по индексу — это лучший дизайн * * * *
    • Использовать функцию-обёртку — это лучший дизайн * * * *

Обзор шаблона №1: несложная реализация #

  • Несложные реализации решают проблемы на одном уровне детализации
    • Если писать код без раздумий о дизайне, то код зачастую сложно читать и модифицировать
    • Несложные реализации уменьшают количество уровней абстракций, с которыми приходится взаимодействовать на уровне кода
  • Стратифицированный дизайн позволяет нам ориентироваться на определённый уровень детализации
  • Граф вызовов является богатым источником подсказок об уровне детализации
    • Исходный код сам предоставляет много подсказок
    • Граф позволяет оценить связи между функциями, выделять слои
  • Выделение функций позволяет получить функции более общего назначения
  • Функции общего назначения легче переиспользовать
  • Мы не скрываем сложность получившегося кода
    • Сложность функций часто прячут внутрь «функций-помощников»
    • В рамках стратифицированного дизайна мы хотим получить понятные слои

Заключение #

  • В рамках стратифицированного дизайна мы группируем код по слоям абстракций. Каждый слой позволяет игнорировать разные детали реализации
  • При реализации новой функции, нам необходимо идентифицировать какие детали важны при решении задачи. Это позволяет определить слой
  • Существует множество подсказок, которые помогают определить место функции в слое: название, тело функции и граф вызовов
  • Название функции должно отражать предназначение функции
  • Тело функции указывает на детали, которые важны для работы функции
  • Граф вызовов может подсказать, что реализация не является простой. Если длина стрелок до зависимых функций различается по длине, это хороший указатель, что реализация не простая
  • Можно улучшить структуру слоёв путём выделения более общих функций
  • Шаблон несложных реализация направляет нас в сторону построения слоёв, состоящих из простых и элегантных функций