Структурный дизайн. Древний секрет простого и быстрого кода

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

Примечание: Этот пост является адаптированной расшифровкой моего одноимённого доклада на Joker' 24.

Введение

Я пишу коммерческий код с 2005 года и с 2014 года ищу способ систематически писать хороший код.

В рамках этих поисков я изучил всю популярную литературу о хорошем коде и его дизайне — от «Чистого кода» Анкл Боба до «DDD» Эрика Эванса. Однако все популярные подходы в значительной степени субъективны: они не дают объективного и последовательного судьи, который бы решал, какой код лучше.

Например, в чистом коде я до сих пор не знаю способа за конечное время дать ответ на вопрос «Сколько уровней абстракции в этой функции?». А если взять DDD — то я до сих пор не знаю способа, который бы позволял стабильно и за конечное время находить границы между ограниченными контекстами (прошу прощения за каламбур) или агрегатами.

Эта неопределённость ведёт к длительным дискуссиям на ревью и в голове разработчика о том, какой из способов является наилучшим для решения задачи. А после этих дискуссий, каждый из участников (включая того дилетанта в собственной голове) остаётся при своём мнении.

Отчаявшись научиться писать стабильно хороший объектно‑ориентированный код, в 2016 году я пошёл в сторону функционального программирования и архитектуры. Там с детерминированностью было получше: если в коде нет побочных эффектов (ввода‑вывода, оператора присваивания и чтения глобальных переменных) — то код хороший, если есть — плохой. Однако как затащить в коммерческий проект и, главное, собственную голову свободные монады и их интерпретаторы — я так и не понял.

Поэтому в 2020 году поиски своего Святого Грааля я продолжил в «эзотерических» и древних книгах. Одной из таких книг стал «Структурный дизайн» Ларри Константина. И в этой книге я, наконец, нашёл простой и понятный принцип, который лёг в основу моего текущего подхода к проектированию и кодированию, и для которого можно быстро и однозначно дать ответ, соответствует ли тот или иной кусочек кода этому принципу или нет.

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

Экспресс-курс структурного дизайна

Структурный дизайн — это методология разработки ПО, которую Ларри Константин и Эдвард Йордан начали разрабатывать в 60-х годах прошлого века и в итоге опубликовали в одноимённой книге в 1977 году (доступна для чтения после бесплатной регистрации).

Самым известным наследием структурного дизайна стали понятия cohesion и coupling — они впервые были введены именно в этой книге.

Но на мой взгляд, незаслуженно была забыта другая идея из структурного дизайна — transform‑centered morphology (aka balanced system).

Примечание: Transform‑centered morphology — это термин из оригинальной книги. Однако он звучит неуклюже даже в английском варианте, поэтому далее по тексту я, вслед за Пейдж‑Джонсом, автором «The practical guide to structured systems design» (следующей книги по структурному дизайну), буду использовать термин balanced system — сбалансированная система.

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

И если систему, разработанную с разделением кода на эти типы, представить в виде структурной схемы, то получится примерно такая картинка (здесь и далее все картинки кликабельные):

transform centered morphology model
Transform-centered morphology model

Примечание: на картинке узлы называются модулями, однако следует помнить, что книге почти 50 лет и в ней термину модуль даётся следующее определение: «A module is a lexically contiguous sequence of program statements, bounded by boundary elements, having an aggregate identifier».

Если переводить на русский, то модуль — это лексически непрерывная последовательность программных выражений, ограниченная граничными элементами и имеющая совокупный идентификатор. То есть это метод, в современной терминологии.

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

Здесь четыре типа кода обозначены цветами:

  1. Красный — код чтения данных. По стрелкам потока данных видно, что этот код берёт данные «из ниоткуда» (на самом деле — с диска, из БД, из сети, из глобальных переменных и т. д.) и заносит их в систему.

  2. Зелёный — код трансформации. По стрелкам потока данных видно, что этот код что‑то получает на вход и что‑то возвращает. Притом по стрелкам вызовов видно, что код трансформаций сам не обращается к коду ввода или вывода, соответственно, все нужные ему данные он получает «сверху» и все результаты своей работы возвращает «наверх» (спойлер: если это вам напоминает чистые функции, то вы правы).

  3. Жёлтый — код записи данных. По стрелкам потока данных видно, что этот код «сливает данные в никуда» (на самом деле — на диск, в БД, в сеть, в глобальные переменные и т. д.).

  4. Голубой — код координации. Это код, который никто не вызывает (на самом деле в оригинале — код, который вызывается ОС, а в современных реалиях — код, который вызывается фреймворком), но который вызывает все другие виды кода и определяет поток данных между ними.

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

Если перекладывать эту абстрактную модель на современные реалии (Spring‑) разработки, то она будет выглядеть так:

abstract balanced system
Абстрактная сбалансированная система на Spring

На первый взгляд, все термины в ней привычные и знакомые — уверен, практически все разработчики если не работали с, то хотя бы слышали про сервисы приложения (Application Service), доменные сервисы (Domain Service), гейтвеи и репозитории (Gateway и Repository) и Data Transfer Objects.

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

Во‑первых, метод доменного сервиса не выполняет никакого ввода‑вывода. Во‑вторых, все типы кода обмениваются между собой DTO‑шками — простыми структурами данных, не содержащими в себе автомагии (такой как ленивая загрузка и дёрти чекинг в JPA‑сущностях).

Давайте посмотрим, что будет, если применять эту идею в современной разработке и приводить форму систем к сбалансированной.

Кейс №1: Project Barcoder

И начнём мы с кусочка кода из Project Barcoder — внутренней системы предприятия для автоматизации бизнес‑процесса архивации отчётных документов.

Бизнес-процесс

Обобщённо бизнес‑процесс следующий:

  1. Филиалы предприятия собирают отчётность за один или более периодов в подручную тару (коробки, в коде — Box);

  2. Экспедиторы доставляют коробки в пункты сбора (ПС);

  3. Специалисты ПС переупаковывают документы из коробок в стандартизированные коробы (в коде — OsgBox);

  4. Специалисты ПС передают коробы в специализированную организацию для долговременного хранения;

Бизнес‑правила:

  1. В одной коробке могут содержаться документы за несколько отчётных периодов;

  2. В одном коробе могут содержаться документы за несколько отчётных периодов;

  3. Для каждого отчётного периода коробки необходимо сохранять привязку, в какие коробы он попал;

  4. При переупаковке допустимы только два варианта:

    1. Переупаковка из одной коробки в несколько коробов;

    2. Переупаковка нескольких коробок в один короб.


В этом кейсе контроль сбалансированности системы помог мне на ревью отловить проблему в работе с БД, которую я пропустил при первой итерации ревью методом пристального вглядывания, а также помог в целом упростить код. Давайте посмотрим, как это было.

До применения принципов структурного дизайна

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

@Transactional
open fun repackage(boxIds: List<Long>, osgBoxes: List<OsgBoxDto>) {
    if (boxIds.isEmpty() || osgBoxes.isEmpty() || (boxIds.size > 1 && osgBoxes.size > 1)) {
        throw OsgRepackageIncorrectAmountException("Допустима перепаковка только одной коробки в несколько коробов, либо нескольких коробок в один короб")
    }
    val osgIds = osgBoxes.map { it.osgBoxId }
    if (osgService.hasAnyLinks(boxIds, osgIds)) {
        throw OsgRepackageBoxAlreadyLinkedException("Некоторые из коробок или коробов уже имеют привязку")
    }
    if (boxIds.size == 1) {
        repackageSingleBox(boxIds.single(), osgBoxes)
    } else {
        repackageMultipleBoxes(boxIds, osgBoxes.single())
    }
}

По большому счёту, про этот код сложно сказать что‑то плохое — он достаточно короткий, аккуратный и простой. Пожалуй, название ошибки OsgRepackageIncorrectAmountException не самое удачное, да условие boxIds.size == 1 можно было бы вынести в метод с названием, отражающим бизнес‑семантику — но нельзя сказать, что это кардинально улучшит качество кода. Тем не менее, всё равно напишем это в замечания и перейдём к следующему методу — обработке кейса перепаковки одной коробки в несколько коробов:

private fun repackageSingleBox(
    boxId: Long,
    osgBoxes: List<OsgBoxDto>,
) {
    val box = boxRepo.findByIdOrNull(boxId) ?: throw BoxNotFoundException("Короб не найден")
    box.reportingIntervals = toReportingIntervals(osgBoxes)
    boxRepo.save(box) // В проекте используется Spring Data JDBC, поэтому вызов save обязателен
    osgService.insertBoxesToOsgLinks(osgBoxes.map { it.osgBoxId }, listOf(boxId))
}

Тут код уже совсем простой и линейный — загрузили коробку, перепаковали, сохранили (в проекте используется Spring Data JDBC, поэтому save надо вызывать явно), сохранили привязку коробки к коробам.

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

Источник: https://habr.com/ru/companies/jugru/articles/858418/


Интересные статьи

Интересные статьи

И рассказываем, чтобы сэкономить Ваше времяЧтобы перелезть через этот высоченный забор понимания, как же все-таки работает сейчас КЭДО в Битрикс24 мы...• Просмотрели 384 минуты видеоуроков и рол...
Привет, Хабр! Это Даша Волкова. На днях власти Тайваня обвинили крупные полупроводниковые компании Китая в том, что те незаконно переманивают инженеров на свои фабрики и заводы. КНР, по мнению Тайваня...
Привет! Меня зовут Юля, я работаю в дирекции больших данных Билайн, недавно я познакомилась с фреймворком Flink и хочу рассказать о своих впечатлениях на примере простой с первого взгляда задачи.Итак,...
Много всякого сыпется в мой ящик, в том числе и от Битрикса (справедливости ради стоит отметить, что я когда-то регистрировался на их сайте). Но вот мне надоели эти письма и я решил отписатьс...
Мы публикуем видео с прошедшего мероприятия. Приятного просмотра.