Разбираем идеи Рича Хики и Clojure: почему простые модели данных, неизменяемость и «лучшие умолчания» уменьшают сложность систем.

Рич Хики — автор языка Clojure и один из самых цитируемых спикеров о том, почему сложность ПО растет быстрее, чем ценность, которую оно дает. Его выступления цепляют не тем, что предлагают «еще один стиль программирования», а тем, что возвращают разговор к практической цели: делать системы понятными, устойчивыми к изменениям и предсказуемыми в работе.
Хики не продвигает «серебряную пулю». Его позиция прагматична: многие проблемы возникают не из-за «плохих разработчиков», а из-за решений «по умолчанию», которые перестают работать, когда система становится распределенной, параллельной и живет годами.
Clojure стал для него способом зафиксировать эти выводы в инструментах: акцент на моделирование данных, неизменяемость, простые абстракции и композицию функций. Даже если вы не пишете на Clojure, сами принципы легко проверить на своем проекте.
У Хики сложность — это не «много строк» и не «сложные алгоритмы». Это запутанность (часто переводят как complecting): когда мы связываем вещи, которые можно было держать раздельно.
Типичные источники запутанности:
Ниже — три опоры, которыми Хики предлагает снижать запутанность: простота в архитектуре, неизменяемость данных и «лучшие умолчания» в инструментах и стиле.
Материал рассчитан на разработчиков, тимлидов и архитекторов, которым важно держать сложность под контролем: при росте команды, появлении конкуррентности, увеличении числа интеграций и постоянном изменении требований. Здесь не будет «священной войны» за функциональное программирование — скорее набор идей, которые можно перенести в любой стек и применять точечно.
Рич Хики постоянно разводит два похожих слова: простое (simple) и легкое (easy). В русском это особенно легко перепутать, потому что «простое» часто звучит как «без усилий». Но у Хики «simple» — про структуру, а «easy» — про ощущение прямо сейчас.
«Простое» — это когда вещи не склеены друг с другом: меньше взаимных зависимостей, меньше скрытых эффектов, меньше условий вида «если тут так, то там иначе». Такая система может требовать дисциплины — но она легче удерживается в голове и проще изменяется.
«Легкое» — это когда в моменте удобно: быстрее написать, приятнее использовать, меньше думать. Проблема в том, что «легкость» часто покупается ценой новых связей и неявных правил.
Типичные решения из категории easy:
Эти ходы ускоряют старт, но увеличивают число «узлов» в системе — и стоимость любого изменения растёт нелинейно.
Полезная проверка: можете ли вы объяснить модель системы за 2–3 минуты?
Не «в общих словах», а так, чтобы человек понял:
Если для объяснения нужно десять оговорок и исключений, вероятно вы сделали «легко» в моменте, но не «просто» по структуре. Это именно та сложность, против которой Хики предлагает бороться.
Идея Хики проста: данные должны жить дольше кода и API. Код переписывают, сервисы дробят и объединяют, команды меняются — а данные (события, заказы, пользователи, статусы) остаются. Если смысл «зашит» в методах и классах, со временем теряется возможность безопасно развивать интеграции и делать миграции без боли.
Вместо сложных иерархий удобно описывать предметную область через базовые структуры данных: карты, векторы, множества. У них понятные свойства, они легко сериализуются и лучше переживают изменения.
Например, «заказ» — это карта с ключами и значениями; позиции — вектор карт; теги — множество. Такой подход заставляет формулировать доменные факты явно: что именно хранится, какие поля обязательны, что может отсутствовать.
Когда данные первичны, поведение превращается в набор функций, которые читают и преобразуют структуры. Это дает два практичных выигрыша:
Частая проблема объектного подхода — данные прячутся за методами, а реальный формат становится неявным «протоколом»: чтобы понять, что есть в сущности и как это использовать, нужно знать набор методов, их побочные эффекты, порядок вызовов, а иногда и внутреннее состояние.
В результате интеграции превращаются в привязку к конкретным классам, а не к понятному контракту данных. Центрирование дизайна вокруг данных делает контракт явным: структуру можно обсуждать и версионировать так же, как API, а реализацию функций менять без переписывания всей системы.
Неизменяемость (immutability) — это договор: после создания значение не меняется. Если нужно «изменить» данные, вы создаёте новую версию, а старая остаётся как была. Рич Хики ценит этот подход не как модную идею, а как практичный способ уменьшить количество ошибок, связанных с состоянием.
Большая часть неприятных багов рождается из вопроса «кто и когда это поменял?». Неизменяемые структуры переводят проблему в более понятную плоскость: вместо мутирующего объекта у вас появляется последовательность снимков состояния.
Это особенно помогает в:
Когда структура может меняться «внутри», любой кусок кода потенциально влияет на любой другой. Неизменяемость снижает количество таких невидимых связей: если функция получила значение, оно не «подменится» из-за параллельного кода или неожиданного побочного эффекта.
В многопоточности ключевой источник сложности — гонки и блокировки. Неизменяемые данные можно безопасно шарить между потоками без локов: чтение не конфликтует с чтением, потому что никто не пишет. Это сокращает потребность в синхронизации и делает поведение системы более предсказуемым.
Главный страх — «копирование дорого». На практике неизменяемые коллекции часто используют структурное разделение: новая версия переиспользует большую часть старой структуры, копируя только изменившиеся части. Цена — немного больше абстракций и иногда более высокая стоимость отдельных операций, зато выигрыши в надёжности и ясности обычно окупают эти затраты в сложных системах.
Состояние в программе кажется «просто данными», пока не выясняется, что оно живёт во времени: меняется, имеет историю, зависит от порядка операций и часто ломается именно из‑за этого порядка. Хики предлагает смотреть на проблему честно: изменяемое состояние — не норма, а дорогая редкость, которую стоит размещать там, где без неё невозможно.
Практичный вопрос для любого модуля: «Это значение должно меняться, или мы просто пересчитываем результат по новым входным данным?» Во многих местах «изменяемость» возникает по привычке (кэш, глобальная переменная, объект, который “накапливает” поля), хотя задача решается вычислениями над неизменяемыми структурами.
Если изменения неизбежны (например, баланс счёта, статус заказа, очередь задач), сделайте изменяемым не всё вокруг, а один понятный “узел” — минимальную точку синхронизации.
Полезная рамка: «состояние — это производная от событий». То есть мы думаем не “как мутировать объект”, а “какие события произошли” и “как из них получить текущую картину”. Даже без полного event sourcing это дисциплинирует: появляется чёткая причинность и возможность восстановить/переиграть изменения.
Стабильная схема:
Такой подход повышает предсказуемость (меньше скрытых зависимостей), упрощает отладку (можно воспроизводить сценарии по входам/событиям) и даёт повторяемость результатов (тот же ввод → тот же вывод), что особенно важно в конкурентных системах.
«Умолчания» — это то, что происходит, если вы ничего специально не настраиваете: как создаются структуры данных, как передаются значения, что считается нормой при работе с состоянием, ошибками и параллельностью. «Лучшие умолчания» — когда язык и его стандартные практики по умолчанию ведут к более простым и предсказуемым решениям, а не к случайной сложности.
Важный эффект: умолчания влияют на тысячи мелких решений в коде. Даже сильные инженеры устают «держать в голове правильный путь», если среда постоянно предлагает более простой, но рискованный вариант.
В Clojure (и в целом в функциональном подходе) многие короткие пути одновременно безопаснее:
Это не про «запрет мутабельности навсегда», а про то, что безопасный путь чаще оказывается проще и короче.
Когда умолчания хорошие, команда меньше спорит о базовых вещах: «копировать или менять?», «хранить ли состояние внутри объекта?», «как передать поведение?». Снижается вариативность — а значит, снижается стоимость чтения чужого кода и ревью.
Ещё один плюс: новые участники быстрее понимают “как принято”, потому что язык и стандартная библиотека подталкивают к одному и тому же стилю.
Плохая сторона любого «пути по умолчанию» — превращение в догму. Иногда мутабельность оправдана (например, узкие горячие участки), иногда проще написать прямолинейный код без сложной композиции.
Правило безопасности простое: умолчания должны уменьшать число исключений, но не запрещать их. Если вы отклоняетесь от нормы, делайте это осознанно и фиксируйте причину — в комментарии, ADR или в соглашениях команды.
Один из самых практичных способов «срезать» сложность — перестать строить систему вокруг иерархий объектов и начать собирать поведение из небольших функций. Рич Хики постоянно возвращается к этой мысли: композиция лучше наследования, потому что вы соединяете уже понятные блоки, а не усложняете модель цепочками переопределений.
Хорошая функция — это маленький контракт: понятные входы, понятный выход, минимальное количество скрытых допущений. Когда таких функций много, их можно комбинировать, как детали конструктора.
Например, вместо «умного» объекта, который сам решает всё подряд, вы строите последовательность шагов:
(-\u003e input
normalize
validate
enrich
calculate)
Даже если вы не пишете на Clojure, сама идея переносима: каждый шаг делает одну вещь, а весь процесс читается сверху вниз.
Чистые функции (без побочных эффектов) дают предсказуемость: одинаковый ввод — одинаковый результат. Это снижает «интуитивные» догадки при отладке и делает поведение системы менее зависимым от порядка вызовов.
Побочные эффекты (запись в базу, сеть, логирование) лучше выносить на границы: пусть середина системы будет про преобразование данных, а не про то, «куда оно ушло и что там изменило».
Подход Clojure поощряет мыслить потоком данных: данные проходят через набор преобразований, а не «живут» внутри сущностей. Это повышает ясность: чтобы понять модуль, достаточно увидеть, какие данные он принимает и какие возвращает.
На ревью проще обсуждать конкретные вещи:
В результате код становится менее «персональным» (держится не на авторских трюках), а более командным: его легче читать, тестировать и менять без страха сломать систему в неожиданном месте.
REPL (Read–Eval–Print Loop) — это режим работы, где вы отправляете в работающую программу маленькие фрагменты кода, тут же получаете результат и продолжаете с уточнением идеи. В отличие от привычного «написал → собрал → запустил», здесь вы не перезапускаете всё приложение ради одной проверки: вы разговариваете с системой, пока она живёт.
При сборке и запуске обратная связь приходит порциями: вы меняете несколько файлов, ждёте сборку, потом тестируете — и только тогда понимаете, где ошиблись. REPL делает этот цикл значительно короче: можно проверить функцию на реальных данных, посмотреть промежуточные значения, быстро нащупать форму данных, а уже затем оформить решение «как положено».
Это особенно полезно в задачах, где много неизвестных: интеграции, трансформации данных, тонкие условия, «почему это значение стало таким». Вместо догадок вы ставите маленькие эксперименты.
Практичный паттерн: держать приложение поднятым локально и подключаться к нему REPL-сессией. Тогда вы можете:
Чтобы это не превратилось в хаос, полезно фиксировать удачные эксперименты: переносить команды из REPL в тесты, в документацию, в небольшие утилиты для повторного прогона.
Интерактивность не отменяет инженерную гигиену. REPL легко создаёт «невидимое состояние» в сессии: вы переопределили функцию, обновили переменную — и всё работает, но только у вас. Поэтому важно стремиться к воспроизводимости: минимизировать побочные эффекты, чаще перезапускать сессию, а ключевые сценарии закреплять тестами.
Отдельный практический момент — окружения: то, что вы исследуете локально, должно корректно переноситься в CI и прод, без «магии» в настройках.
Сложность в больших продуктах редко берётся «из кода как такового». Чаще её создают свойства самой системы: много компонентов, много команд, много внешних зависимостей — и всё это постоянно меняется.
Несколько повторяющихся причин, почему системы начинают «расползаться»:
Простые модели данных и неизменяемость не «делают распределённость лёгкой». Но они уменьшают число мест, где нужно держать в голове скрытые правила.
Важно: эти принципы не отменяют компромиссы. Ретрай, идемпотентность, наблюдаемость, схемы данных и миграции всё равно придётся проектировать. Разница в том, что у вас меньше «магии», а значит проще искать причины проблем и точнее оценивать изменения.
Clojure обычно хорошо ложится на задачи, где много преобразований данных, событий, потоков обработки, правил и отчётности. Он может быть полезен, когда цена ошибок состояния высока, а изменения требований постоянны.
С другой стороны, сложнее может быть онбординг (непривычный стиль), местами экосистема (не всегда «как в мейнстриме»), и найм (меньше кандидатов). Это не стоп-факторы, но их лучше учитывать заранее и закладывать в план.
Идеи Рича Хики полезны даже если вы не планируете переходить на Clojure. Смысл не в конкретном синтаксисе, а в привычках: отделять данные от действий, минимизировать изменяемое состояние и выбирать умолчания, которые уменьшают шанс «случайной сложности».
Начните с двух простых ограничений:
Практический приём: заведите слой transform (или domain) с функциями, которые получают простые структуры (словари/JSON/DTO) и возвращают такие же.
Разделяйте код не по «сущностям» или «контроллерам», а по трём типам ответственности:
Так вы заранее видите, где находится состояние и почему оно меняется.
«Умолчания» — это то, что происходит без обсуждений. Примеры:
Перед одобрением PR спросите:
Если вам близка «хикиевская» идея про ясные модели и быстрый цикл обратной связи, её можно использовать не только в коде, но и в самом процессе разработки. В TakProsto.AI (vibe-coding платформа для российского рынка) удобно начинать с явной модели данных и шагов трансформации прямо в чате: вы формулируете структуру сущностей, сценарии изменений и границы I/O, а затем платформа помогает собрать приложение (веб на React, бэкенд на Go с PostgreSQL, мобильное на Flutter) без лишней «магии» в архитектуре.
Отдельно полезны режим планирования, снапшоты и откат: они дисциплинируют изменения, оставляя историю решений — по сути, «неизменяемость» на уровне процесса. А если нужно — можно экспортировать исходники и продолжить развитие в привычном пайплайне.
Выбор Clojure (или отказ от него) лучше делать так же «по-хикиевски»: снижать лишнюю сложность решения, а не доказывать правоту парадигмы. Хороший ориентир — считать успехом не «переписали на новом языке», а «стало проще поддерживать и менять систему».
Смотрите на контекст, а не на тренды.
Начните с малого: пилотный сервис или внутренний инструмент с понятными границами. Параллельно:
Фиксируйте не идеологию, а метрики: время на изменения, количество дефектов из‑за состояния, скорость разбора инцидентов. Обсуждайте решения через вопросы: «что станет проще? что станет сложнее? где риски?» — и оставляйте право на гибридный подход.
Если хотите углубиться — соберите «маршрут чтения»: выступления Рича Хики, основы Clojure, практики моделирования данных и работы с событиями.
Из связанных тем хорошо дополняют картину: event sourcing, CQRS, управление схемами данных и приемы миграций (см. также /blog).
У Хики простота — это про структуру: части системы слабо связаны и не «переплетены» друг с другом.
Легкость — это про ощущение «сделать быстро сейчас». Часто лёгкие решения добавляют скрытые зависимости (магия, глобальный контекст, порядок вызовов), и позже изменения становятся дороже.
Он использует идею complecting: сложность появляется, когда мы связываем то, что можно держать отдельно.
Частые причины:
Спросите себя: могу ли я объяснить модель системы за 2–3 минуты без длинных оговорок?
Проверьте, можете ли вы чётко назвать:
Если объяснение требует множества исключений, вероятно, модель «переплетена».
Потому что данные обычно живут дольше кода, API и команды. Если смысл «зашит» в методах и классах, интеграции и миграции становятся болезненными.
Практика: описывайте домен простыми структурами (карты/списки/множества, JSON-подобные формы) и делайте поведение набором функций, которые их преобразуют.
Антипаттерн — когда формат сущности становится неявным «протоколом»: чтобы понять, что внутри, нужно знать методы, побочные эффекты и порядок вызовов.
Альтернатива:
Неизменяемость уменьшает количество ошибок «кто и когда это поменял». Вместо мутирующего объекта появляется цепочка версий/снимков.
Особенно полезно для:
Страх «копирование дорого» часто не срабатывает на практике, потому что persistent-коллекции используют структурное разделение: новая версия переиспользует большую часть старой.
Компромиссы остаются:
Правило: оптимизируйте там, где есть измерения, а не «на всякий случай».
В многопоточности ключевые проблемы — гонки и блокировки. Неизменяемые данные можно безопасно шарить между потоками: чтения не конфликтуют, потому что никто не пишет.
Хорошая тактика: если изменяемость неизбежна, делайте её в одном понятном узле (минимальная точка синхронизации), а вокруг держите чистые преобразования.
Выносите I/O на границу и держите «середину» системы чистой.
Удобное разделение:
Так тесты становятся проще, а поведение — предсказуемее (тот же ввод → тот же вывод).
Пилотируйте подход без крайностей:
Если нужно углубиться, соберите маршрут чтения и примеры в базе знаний (см. также /blog).