2.2

Смотреть на английском

2.2 Принципы проектирования программ

Обзор и мотивация

Принципы проектирования программ это эвристики расположения кода так, чтобы его можно было понимать, менять и расширять со временем. К ним относятся именованные аббревиатуры (SOLID для пяти принципов объектно-ориентированного проектирования, DRY для «не повторяйся», KISS для «делай проще», YAGNI для «тебе это не понадобится»), структурные понятия (сцепление, связность, разделение ответственности), каталогизированные паттерны проектирования, подходы к моделированию более высокого уровня, такие как предметно-ориентированное проектирование (моделирование программ на языке предметной области бизнеса), и выбор между объектно-ориентированным, функциональным и ориентированным на данные стилями. Ничто из этого не законы. Это спрессованный опыт, и применять его нужно с суждением.

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

Важнейший навык не заучивание принципов. Это знание того, когда каждый из них вводит в заблуждение. У каждого принципа есть режим отказа: DRY может породить неверную абстракцию, SOLID может породить ненужную косвенность, YAGNI может лишить расширяемости, которая действительно нужна. Эта глава рассматривает принципы как инструменты с областью применимости и подчёркивает сцепление и связность как более глубокие свойства, которым аббревиатуры пытаются служить.

Ключевые принципы

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

Рекомендации

Используйте SOLID как линзу, а не контрольный список

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

Применяйте DRY к знанию, а не к тексту

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

Пусть KISS и YAGNI сопротивляются домыслам

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

Проектируйте явно под слабое сцепление и сильную связность

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

Используйте паттерны проектирования как словарь, антипаттерны как предупреждения

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

Принимайте предметно-ориентированное проектирование там, где предметная область сложна

Для систем с богатыми бизнес-правилами используйте тактические и стратегические инструменты DDD: единый язык, общий с экспертами предметной области, ограниченные контексты, разрезающие систему на независимо моделируемые части, и карты контекстов, описывающие, как эти части соотносятся. Ограниченные контексты особенно ценны в корпоративном масштабе, потому что выравнивают владение команд с границами моделей. DDD избыточно для простых систем CRUD (создание, чтение, обновление, удаление).

Выбирайте парадигмы по соответствию

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

Компромиссы: плюсы и минусы

Принцип / подходПри хорошем примененииРежим отказа
SOLIDЯсные швы там, где происходят изменения; тестируемые единицыРазмножение интерфейсов и слоёв; косвенность без отдачи
DRYЕдиный источник истины для настоящего знанияНеверная абстракция, сцепляющая несвязанный код
KISS / YAGNIСкромные, понятные системыНедопроектированные швы; дорогое дооснащение нужной гибкостью
Паттерны проектированияОбщий словарь; проверенные структурыКарго-культ паттернов; случайная сложность
Предметно-ориентированное проектированиеСогласованные модели и команды; укрощённая сложностьТяжёлые церемонии на простых областях; неверно проведённые границы контекстов
Функциональный / неизменяемый стильПредсказуемость; безопаснее конкурентностьНеудобно для по сути состоятельных задач; сюрпризы производительности

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

Вопросы для обсуждения с командой

  1. Каков ваш конкретный порог для выделения общей абстракции и как не дать DRY породить неверную? Эта глава прямо говорит, что дублирование дешевле неверной абстракции и что нужно ждать, пока закономерность появится два-три раза, прежде чем выделять. В большой команде опасность в том, что кто-то выносит два похожих фрагмента в общий модуль через границы команд, и тогда каждое будущее изменение одного вызывающего расходится рябью к другому. Сигнал, который нужно принести: меняются ли дубликаты по одной причине или просто выглядят похоже сейчас. Согласуйте правило трёх и требуйте, чтобы кандидат в абстракцию действительно менялся вместе, прежде чем сцеплять вызывающих. Одна эта договорённость предотвращает класс сцепления, дорогой для расплетения, когда от него зависят многие команды.

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

  3. Где в ваших системах проходит граница между предметной областью, достаточно богатой для оправдания предметно-ориентированного проектирования, и простым CRUD-приложением, где оно избыточно? Глава рекомендует ограниченные контексты DDD именно потому, что они выравнивают владение команд с границами моделей, и предупреждает, что DDD избыточно для простых систем создания-чтения-обновления-удаления и вырождается в церемонию без настоящего моделирования. Ошибка в любом направлении дорога: тяжёлое DDD на тонкой области хоронит простое приложение в церемониях, а разросшаяся общая модель между многими командами вынуждает к постоянной межкомандной координации. Принесите сигналы, действительно решающие это: плотность бизнес-правил и сколько команд должны независимо владеть частями. Резервируйте стратегическую машинерию для сложного ядра и оставляйте простые края простыми. Это уберегает и от театра DDD, и от большого шара грязи.

  4. Когда абстракция, интерфейс или паттерн проектирования стоят добавляемой косвенности и у кого есть полномочие назвать проект перепроектированным? Эта глава прямо говорит, что косвенность имеет цену, которую вы платите при каждом чтении, и что изготовление интерфейсов, фабрик и слоёв ради SOLID или изощрённого вида это режим отказа. В большой команде давление идёт в обратную сторону: рецензенты пропускают лишнюю абстракцию, потому что она выглядит дисциплинированно, и никто не хочет быть тем, кто спорит за меньшую структуру. Противоположное соображение реально, потому что некоторые швы действительно оправдывают себя, а убирать их позже дорого. Принесите конкретные свидетельства: сколько реализаций у интерфейса на самом деле сегодня, как часто точка расширения хоть раз проявляла гибкость и сколько файлов читателю нужно открыть, чтобы проследить один путь кода. Согласуйте, что единственная реализация без видимой второй по умолчанию причина встроить, и назовите, кто может назвать проект перепроектированным, чтобы это не читалось как оскорбление. В корпоративных и государственных системах, переживающих авторов на десятилетие, беспричинная косвенность налог, который платит каждый будущий сопровождающий, поэтому воспринимайте вопрос «что нам даёт эта абстракция» как постоянный вопрос ревью, а не личный вызов.

  5. Как вы решаете, какую парадигму использует каждый компонент (объектно-ориентированную, функциональную или ориентированную на данные) и как держите границы между ними чистыми? Глава утверждает, что большие системы прагматично смешанные и что выбирать нужно по компонентам по соответствию: объектную ориентацию для областей с состоянием, функциональный стиль для преобразований и конкурентности, а проектирование, ориентированное на данные, там, где доминируют производительность и поведение кэша. Без управления выбор парадигмы становится вопросом того, кто первым написал модуль, а изменяемое состояние просачивается в то, что должно быть чистыми преобразованиями, или функциональный пуризм борется с по сути состоятельной задачей. Свидетельства, которые стоит принести: где ваша настоящая боль, какие компоненты трудно тестировать из-за скрытого состояния, какие горячие пути ограничены кэшем и где нынешний стиль вынуждает к неуклюжим обходам. Решите умолчание парадигмы для каждого слоя намеренно и запишите, где проходят швы между стилями, чтобы функциональное ядро и императивный край не затекали друг в друга. Для регулируемой или государственной системы, где вычисление должно быть проверяемым и воспроизводимым за данный период, неизменяемое функциональное ядро часто требование соответствия, а не вкус, и это ограничение должно вести границу, а не следовать за ней.

  6. Как не дать этим принципам застыть в догму и где записывать рассуждение за проектным решением, чтобы будущая команда могла его пересмотреть? У каждого принципа в этой главе есть область применимости и режим отказа, и вся рамка рассматривает их как инструменты, применяемые с суждением, а не законы для принуждения. В большой команде принцип тихо превращается в правило: DRY запрещает любое дублирование, SOLID предписывает интерфейс на каждый класс, а прагматичные исключения блокируются на ревью людьми, ссылающимися на аббревиатуру, а не на результат. Напряжение в том, что некоторая согласованность действительно помогает сотням инженеров координироваться, поэтому нельзя просто объявить любой принцип необязательным. Принесите примеры, где следование принципу буквально дало худший проект, и записи о решениях, если они есть, объясняющие, почему существует данная граница или абстракция. Согласуйте, что принципы это умолчания, от которых инженер может отклониться с записанной причиной, и фиксируйте значимые проектные выборы в короткой записи об архитектурном решении, чтобы следующая команда унаследовала рассуждение, а не только код. В корпоративных и государственных системах, где первоначальных авторов давно нет, а аудит спрашивает, почему система устроена так, эта письменная запись разница между проектом, который будущие команды могут безопасно менять, и тем, к которому они боятся прикоснуться.

Взгляд по секторам

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

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

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

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

Примеры

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

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

Государственный сектор. Национальная система обработки налогов намеренно выбирает ориентированное на данные функциональное ядро для своего вычислительного движка. Налоговые правила выражены как чистые преобразования над неизменяемыми входными записями, что делает их проверяемыми, тестируемыми и воспроизводимыми за данный налоговый год. Императивные состоятельные части (рабочий процесс, уведомления) держатся на краях. Аудиторы могут указать на конкретную версию правила и точно воспроизвести любое историческое вычисление, что юридическое требование, которого запутанный граф объектов со скрытым изменяемым состоянием гарантировать не мог.

Бизнес-обоснование: мотивы, ROI и TCO

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

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

Антипаттерны и ловушки

  • Спекулятивная обобщённость: построение расширяемости под воображаемые требования, которые никогда не приходят.
  • Неверная абстракция: принудительное объединение несвязанного кода ради DRY, создающее сцепление хуже дублирования.
  • Карго-культ паттернов: применение паттернов проектирования ради них самих, добавляющее косвенность без пользы.
  • Анемичные или божественные объекты: модели без поведения или объекты, делающие всё; оба сигнализируют о неуместно распределённых ответственностях.
  • Распределённый монолит: сервисы, разделённые физически, но по-прежнему жёстко сцепленные, сочетающие издержки обоих подходов.
  • Большой шар грязи: нет различимой структуры; каждое изменение рискует всем.
  • Театр DDD: принятие словаря и структуры папок без предметного моделирования, которое даёт ценность.

Модель зрелости

  • Уровень 1, Начальный: Проектирование стихийно и реактивно; сцепление копится без контроля; принципы неизвестны или произносятся как лозунги, а абстракции появляются или исчезают по привычке отдельных людей.
  • Уровень 2, Развитие: Команды знают принципы и применяют их, но непоследовательно и часто догматично; одни группы управляют сцеплением и связностью намеренно, а другие нет, и нет общего словаря по организации.
  • Уровень 3, Стандартизация: Общий словарь проектирования, правило трёх для выделения абстракций, анализ сцепления и связности и ограниченные контексты, выровненные по командам, задокументированы и ожидаются по всей организации, последовательно применяются на ревью проектов, а не оставляются личному вкусу.
  • Уровень 4, Управление: Здоровье проекта измеряется относительно базовых значений: данные о сцеплении и совместных изменениях, доля неудачных изменений и время реализации сопоставимых функций отслеживаются во времени, так что абстракции и границы добавляются, сохраняются или убираются на основании свидетельств, а перепроектирование и неверная абстракция ловятся данными, а не мнением.
  • Уровень 5, Оркестрация: Дисциплина проектирования интегрирована с планированием поставки и рисков по организации; принципы применяются с нюансами и знанием режимов отказа; выбор парадигм и границ намерен и постоянно пересматривается, а организация регулярно рефакторит, пересматривает область и выводит абстракции по мере смещения предметной области и свидетельств.

Идеи для обсуждения

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

Главные выводы

  • Сцепление и связность это свойства, которые имеют значение; аббревиатуры средства для этих целей.
  • У каждого принципа есть режим отказа; знайте, когда каждый вводит в заблуждение.
  • Предпочитайте немного дублирования преждевременной или неверной абстракции.
  • Используйте DDD и ограниченные контексты, чтобы выровнять сложные области с владением команд.
  • Выбирайте парадигмы по соответствию; большие системы прагматично смешанные.
  • Проектируйте под изменения, которые вам действительно понадобятся, избегая и недопроектирования, и перепроектирования.

Источники и дополнительное чтение

  • Robert C. Martin, Clean Architecture and Agile Software Development, Principles, Patterns, and Practices
  • Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
  • Vaughn Vernon, Implementing Domain-Driven Design
  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software
  • Martin Fowler, Refactoring: Improving the Design of Existing Code and Patterns of Enterprise Application Architecture
  • David L. Parnas, On the Criteria to Be Used in Decomposing Systems into Modules
  • Sandi Metz, Practical Object-Oriented Design