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

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

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

Содержание #

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

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

Напомним, что мы разбираем стратифицированный дизайн через призму из четырёх шаблонов. Первый мы разобрали на прошлом занятии

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

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

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

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

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

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

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

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

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

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

До применения барьеров #

  • У нас надвигается большая распродажа, а команда по разработке ещё написала нужный код!
  • Мы пишем код для распродаж каждую неделю. Мы работаем настолько быстро насколько можем. Будьте терпеливы.

После применения барьеров #

  • Мы давно не слышали запросов на разработку нового кода. Как поживает ваш код для обслуживания маркетинговых компаний?
  • Всё отлично! После реализации барьеров из абстракций, мы смогли написать весь код для обслуживания распродаж самостоятельно.

Барьеры из абстракций скрывают детали реализации #

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

Программисты на ФЯП стратегически применяют барьеры из абстракций, так как они позволяют решать проблемы в более абстрактных терминах

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

Польза от игнорирования деталей #

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

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

Если барьер из абстракций сделан корректно, то его реализацию можно заменять, а зависимые части не должны увидеть изменения

Замена структур данных корзины #

Корзина реализована на основе массива, но товары индексируются по названию

  • Для поиска элемента необходимо обойти весь массив
  • Ассоциативный массив лучше подходит для хранения данных

Вопрос: Какие функции необходимо изменить, чтобы реализовать изменение структуры данных для корзины?

  • Только лишь функции в слое абстракии по работе с корзиной необходимо изменить
  • Все остальные функции не знают деталей реализации корзины

В JavaScript для реализации ассоциативных массивов используются объекты

Реализация корзины на объектах #

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

// Оринигальная реализация
function add_item(cart, item) {
  return add_element_last(cart, item);
}

// Новая реализация
function add_item(cart, item) {
  return objectSet(cart, item.name, 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 calc_total(cart) {
  var total = 0;
  var values = Object.values(cart);
  for(var i = 0; i < values.length; i++) {
    var item = values[i];
    total += item.price;
  }
  return total;
}

// Оригинальная реализация
function setPriceByName(cart, name, price) {
  var cartCopy = cart.slice();
  for(i = 0; i < cartCopy.length; i++) {
    if(cartCopy[i].name === name)
      cartCopy[i] = setPrice(cartCopy[i], price);
  }
  return cartCopy;
}

// Новая реализация
function setPriceByName(cart, name, price) {
  if(isInCart(cart, name)) {
    var item = cart[name];
    var copy = setPrice(item, price);
    return objectSet(cart, name, copy);
  } else {
    var item = make_item(name, price);
    return objectSet(cart, name, item);
  }
}

// Оригинальная реализация
function remove_item_by_name(cart, name) {
  var index = indexOfItem(cart, name);
  if(index !== null)
    return splice(cart, index, 1);
  return cart
}

// Новая реализация
function remove_item_by_name(cart, name) {
  return objectDelete(cart, name);
}

// Оригинальная версия
function indexOfItem(cart, name) {
  for(var i = 0; i < cart.length; i++) {
    if(cart[i].name === name)
      return i;
  }
  return null;
}

// Новая реализация не нужна

// Оригинальная версия
function isInCart(cart, name) {
  return indexOfItem(cart, name) !== null;
}

// Новая реализация
function isInCart(cart, name) {
  return cart.hasOwnProperty(name);
}

Барьеры из абстракций позволяют игнорировать детали #

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

Когда стоит (и не стоит) использовать барьеры из абстракций #

№ 1: Для возможности замены реализации #

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

№ 2: Чтобы сделать код легче для написания и чтения #

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

№ 3: Для снижения нагрузки при координации команд #

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

№ 4: Для фокусировке над задачей #

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

Обзор шаблона № 2, барьеры из абстракций #

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

Код стал более понятным #

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

function add_item(cart, item) {
  return objectSet(cart, item.name, item);
}

functions gets_free_shipping(cart) {
  return calc_total(cart) >= 20;
}

function cartTax(cart) {
  return calc_tax(calc_total(cart));
}

function remove_item_by_name(cart, name) {
  return objectDelete(cart, name);
}

function isInCart(cart, name) {
  return cart.hasOwnProperty(name);
}

// Пока ещё сложные в реализации функции
function calc_total(cart) {
  var total = 0;
  var values = Object.values(cart);
  for(var i = 0; i < values.length; i++) {
    var item = values[i];
    total += item.price;
  }
  return total;
}

function setPriceByName(cart, name, price) {

if(isInCart(cart, name)) {
    var copy = objectSet(cart[name], 'price', price);
    return objectSet(cart, name, copy);
  } else {
    var item = make_item(name, price);
    return objectSet(cart, name, item);
  }
}

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

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

Новая маркетинговая компания #

Предлагается скидка в 10% тем, у кого в товаров в корзине на сумму больше 100 долларов, и если в корзине есть часы

Выбор места реализации #

  • Внутри барьера из абстракций
  • Над барьером из абстракций

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

// На уровне барьера
function getsWatchDiscount(cart) {
  var total = 0;
  var names = Object.keys(cart);
  for(var i = 0; i < names.length; i++) {
    var item = cart[names[i]];
    total += item.price;
  }
  return total > 100 && cart.hasOwnProperty("watch");
}

// Над барьером
function getsWatchDiscount(cart) {
  var total = calcTotal(cart);
  var hasWatch = isInCart("watch");
  return total > 100 && hasWatch;
}

Какая из реализаций лучше? Почему?

Реализация над барьером лучше #

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

Ведение журнала по добавлению товаров в корзину #

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

Для хранения информации о покупках была создана таблица в базе данных и реализован метод по добавлению информации:

logAddToCart(user_id, item)

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

function add_item(cart, item) {
  logAddToCart(global_user_id, item);
  return objectSet(cart, item.name, item);
}

Влияние расположения кода на архитектуру приложения #

  • Первоначально предложение выглядит осмысленно: при добавлении товара в корзину будет вызываться данный метод и запись будет попадать в журнал
  • Однако вызов logAddToCart — это действие, т.к. происходит запись в базу данных. Следовательно add_item тоже становится действием и т.д.
  • Данный метод используется внутри update_shipping_icons:
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);
    if(gets_free_shipping(new_cart))
      button.show_free_shipping_icon();
    else
      button.hide_free_shipping_icon();
  }
}
  • Данный метод вызывается не только для нового товара, но и для каждого товара, который просматривает пользователь. Это явно не то поведение, которого мы хотим достичь
  • Добавление вызова logAddToCart внутрь add_item ломает интерфейс к корзине, который до этого состоял исключительно из вычислений и решал одну чётко поставленную задачу

Лучшее место для вызова метода #

  • Решение о размещении вызова метода — это архитектурное решение, которое не имеет однозначно верного решения
  • В рамках данного приложения хорошим местом является обработчик нажатия кнопки в пользовательском интерфейсе, add_item_to_cart
    • Данный метод вызывается только после действия пользователя
    • Данный метод является действием
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(shopping_cart);
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart);
  update_tax_dom(total);
  logAddToCart(global_user_id, item);
}
  • Данное решение не является наилучшим, но хорошим для текущей архитектуры приложения
  • Более хорошее решение потребовало бы изменения архитектуры приложения
  • Мы не ухудшили архитектуру, так как add_item_to_cart уже была действием
  • Шаблон минимального интерфейса заставляет фокусироваться на чувстве чистого, простого и надёжного интерфейса. И позволяет использовать его как посредника для предотвращения непредвиденного поведения в других частях приложения

Обзор шаблона № 3 минимальный интерфейс #

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

Мы хотим, чтобы интерфейс был минимальным:

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

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

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

При формировании слоистой структуры часто хочется выделить множество слоёв

  • Однако задача создания хорошего слоя абстракции — сложная
  • Часто оказывается, что барьер не был настолько полезен, не был полон
  • Или он не был настолько удобен в использовании
  • Слишком «высокие» башни из абстракций зачастую разваливаются в результате более глубокого исследования предметной области

Абстракции могут обеспечить реализацию очень сложных идей. Примером такой абстракции может служить язык JavaScript по сравнению с машинным кодом. На создание такой абстракции ушло много человеко-лет

Удобные слои #

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

Проверка заключается в ответе на вопрос: «удобно ли работать с текущими слоями?»

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

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

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

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

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

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

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

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

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

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

Шаблон № 4: Комфортные слои #

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

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

Анализ графа вызова исходного кода #

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

Посмотрим какие факты можно получить из анализа структуры самого графа

Структура графа сама по себе может рассказать много фактов о нефункциональных требованиях

  • Функциональные требования — это функции, которые приложение должно выполнять корректно. Например вычисление налогов
  • Нефункциональные требования — требования, определяющие свойства, которые система должна демонстрировать и которые не относятся к её поведению

Структура графа может рассказать о

  • Удобстве сопровождения, что код легко изменять при изменении требований
  • Пригодности к тестированию, какие части кода надо протестировать
  • Возможности к повторному использованию

Код вверху графа легче изменять #

  • Часто меняющийся код (реализация бизнес-правил) должен находиться сверху графа
  • Редко меняющийся код внизу

  • При изменении кода внизу графа может потребоваться изменять все функции, которые транзитивно зависят от этого кода
  • Это верно при изменении интерфейса данных методов
  • Чем длиннее путь от верхушки графа к выделенной функции, тем дороже будет внесение изменений в данную функцию
  • Применение техник копирования при записи позволяет создавать функции, которые не требуют частых изменений
  • Шаблон № 1 призывает нас выделять функции в нижележащих слоях
  • Шаблон № 3 призывает использовать существующие абстракции

Тестирование кода внизу графа важнее #

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

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

Если написать их «вверху», то мы сможем протестировать больше функций

Если написать их «внизу», то мы сделаем надёжнее функции, которые опираются на работу этих функций

  • При правильном разделении на слои, код сверху будет меняться чаще кода внизу
  • Тесты, написанные к «верхним функциям» придётся чаще изменять, так как требования будут меняться
  • Тесты, написанные к «нижним функциям», будут редко меняться
  • Корректно работающие функции нижнего слоя позволят быстрее разрабатывать функции верхнего слоя
  • Написание тестов к функциям внизу графа более выгодно в долгосрочной перспективе

Код внизу графа легче повторно использовать #

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

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

Резюме анализа структуры графа #

В результате анализа структуры графа мы можем оценить

Удобство сопровождения #

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

Пригодность к тестированию #

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

Повторное применение #

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

Заключение #

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

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