Разбор идей John Ousterhout о практическом дизайне ПО, наследии Tcl, споре с Бруксом и том, как сложность убивает продукты.

John Ousterhout — учёный и инженер, чья работа охватывает как исследования, так и реальные системы. Он создал язык программирования Tcl, помог сформировать современные файловые системы и затем свёл десятилетия опыта к простой, немного неудобной идеи: сложность — главный враг софта.
Это послание остаётся актуальным, потому что большинство команд терпят не из‑за недостатка фич или усилий, а потому что их системы (и организации) становятся трудными для понимания, изменения и надёжного изменения. Сложность замедляет не только инженеров. Она просачивается в продуктовые решения, уверенность в роадмапе, доверие клиентов, частоту инцидентов и даже найм — потому что адаптация новых сотрудников превращается в месячную эпопею.
Формулировка Оустерхаута практична: когда система накапливает особые случаи, исключения, скрытые зависимости и «только на этот раз» исправления, стоимость выходит за пределы кода. Весь продукт становится дороже в развитии. Фичи отнимают больше времени, QA усложняется, релизы становятся рискованнее, а команды начинают избегать улучшений, потому что любое изменение кажется опасным.
Речь не про академическую чистоту. Это напоминание о том, что у каждого упрощения есть процентные платежи — а сложность это долг с самым высоким процентом.
Чтобы идея стала конкретной, мы посмотрим на послание Оустерхаута через три угла:
Это не только для фанатов языков. Если вы строите продукты, руководите командами или принимаете решения по роадмапу, вы найдёте действенные способы замечать сложность на ранних стадиях, не дать ей укорениться и сделать простоту первоочередным ограничением — а не милой опцией после запуска.
Сложность — это не «много кода» или «сложная математика». Это разрыв между тем, что вы думаете система сделает при изменении, и тем, что она фактически делает. Система сложна, когда мелкие правки кажутся рискованными — потому что вы не можете предсказать радиус поражения.
В здоровом коде вы можете ответить: «Если мы это поменяем, что ещё может сломаться?» Сложность делает этот вопрос дорогим.
Она часто скрывается в:
Команды ощущают сложность как медленную поставку (больше времени на расследование), больше багов (поведение удивляет) и хрупкие системы (изменения требуют координации множества людей и сервисов). Это также бьёт по онбордингу: новички не могут построить ментальную модель и избегают трогать критические потоки.
Часть сложности существенна: бизнес‑правила, требования соответствия, пограничные сценарии реального мира. Их нельзя удалить.
Но много чего — случайно: запутанные API, дублирующаяся логика, «временные» флаги, которые становятся постоянными, и модули, протекающие внутренними деталями. Это та сложность, которую создают решения по дизайну — и единственная, которую вы регулярно можете убрать.
Tcl родился с практической целью: упростить автоматизацию ПО и расширение приложений без переписывания. John Ousterhout спроектировал его так, чтобы команды могли добавить «ровно столько программируемости», сколько нужно — и дать эту мощь пользователям, операторам, QA или всем, кто должен скриптовать рабочие процессы.
Tcl популяризировал понятие «язык‑клей»: небольшой гибкий скриптовый слой, который связывает компоненты, написанные более быстрыми, низкоуровневыми языками. Вместо того чтобы встраивать каждую фичу в монолит, вы могли экспортировать набор команд и комбинировать их в новые поведения.
Эта модель оказалась влиятельной, потому что соответствовала тому, как реально делается работа. Люди не только строят продукты; они создают билд‑системы, тест‑хорнесс, админ‑инструменты, конвертеры данных и одноразовые автоматизации. Лёгкий скриптовый слой превращает эти задачи из «заведи задачу» в «напиши скрипт».
Tcl сделал встраивание первостепенным: интерпретатор можно было интегрировать в приложение, экспортировать чистый командный интерфейс и мгновенно получить конфигурируемость и быструю итерацию.
Та же идея проявляется сегодня в системах плагинов, языках конфигурации, API расширений и встроенных рантаймах — вне зависимости от синтаксиса скриптов.
Она также укрепила полезную привычку дизайна: отделять стабильные примитивы (ядро хост‑приложения) от меняемой композиции (скрипты). Когда это работает, инструменты эволюционируют быстрее, не дестабилизируя ядро.
Синтаксис Tcl и модель «всё — строка» могли быть неинтуитивными, и большие Tcl‑базы порой тяжело понимать без строгих соглашений. По мере появления более богатых стандартных библиотек, лучшего инструментария и больших сообществ многие команды естественно мигрировали.
Это не стирает наследие Tcl: он помог нормализовать идею, что расширяемость и автоматизация — это не добавка, а фичи продукта, которые могут резко уменьшить сложность для людей, использующих и поддерживающих систему.
Tcl строился вокруг кажущейся строгости: держать ядро маленьким, делать композицию мощной и держать скрипты читаемыми, чтобы люди могли работать вместе без постоянного перевода.
Вместо большого набора специализированных фич Tcl опирался на компактный набор примитивов (строки, команды, простые правила вычисления) и ожидал, что пользователи комбинируют их.
Такая философия подталкивает дизайнеров к меньшему числу концепций, которые переиспользуются в разных контекстах. Урок для продукт‑ и API‑дизайна прост: если вы можете решить десять задач двумя‑тремя согласованными строительными блоками, вы сужаете поверхность изучения.
Ключевая ловушка — оптимизация под удобство билдера. Фича может быть лёгкой в реализации (скопировать опцию, добавить флаг, залатать крайний случай), но усложнить продукт для пользователей.
Tcl акцентировал обратное: держать ментальную модель компактной, даже если реализации приходится делать больше работы за кулисами.
При ревью предложения спрашивайте: уменьшает ли это число концепций, которые пользователь должен помнить, или добавляет ещё одно исключение?
Минимализм помогает только если примитивы согласованы. Если две похожие команды ведут себя по‑разному в пограничных случаях, пользователи начинают зубрить трюки. Набор маленьких инструментов превращается в «острые края», когда правила варьируются тонко.
Подумайте о кухне: хороший нож, сковорода и духовка позволяют готовить множество блюд, комбинируя техники. Гаджет, который только режет авокадо — одноразовая фича: удобно продать, но он загромождает ящик.
Философия Tcl за нож и сковороду: общие инструменты, которые чисто компонуются, чтобы не требовался новый гаджет для каждого рецепта.
В 1986 году Фред Брукс написал эссе с провокационным выводом: нет единственного прорыва — «серебряной пули», которая в один прыжок сделает разработку софта в разы быстрее, дешевле и надёжнее.
Его мысль не в том, что прогресс невозможен. Он в том, что программирование — среда, где мы можем делать почти всё, и эта свобода несёт уникальную ношу: мы постоянно определяем вещь по ходу её создания. Лучшие инструменты помогают, но они не стирают самую трудную часть работы.
Брукс разделил сложность на две корзины:
Инструменты могут раздавить случайную сложность. Подумайте о том, что мы получили с уровнями выше: высокоуровневые языки, контроль версий, CI, контейнеры, управляемые БД и хорошие IDE. Но по Бруксу сущностная сложность доминирует, и она не исчезнет просто потому, что инструменты стали лучше.
Даже с современными платформами команды всё ещё тратят основную энергию на согласование требований, интеграцию систем, обработку исключений и поддержание консистентности поведения во времени. Поверхность может поменяться (API облака вместо драйверов устройств), но основная задача остаётся: перевод человеческих потребностей в точное, сопровождаемое поведение.
Это задаёт напряжение, на которое делает упор Оустерхаута: если сущностную сложность нельзя удалить, может ли дисциплинированный дизайн существенно уменьшить утечку этой сложности в код — и, соответственно, в головы разработчиков каждый день?
Люди часто рисуют «Ousterhout vs Brooks» как битву оптимизма и реализма. Полезнее читать это как двух опытных инженеров, описывающих разные аспекты одной и той же проблемы.
Брукс говорит, что нет единого лекарства для самых больших трудностей. Оустерхаута это не опровергает.
Его практический контраргумент уже уже: команды часто считают сложность неизбежной, тогда как большая её часть самопорождена.
По Оустерхауту хороший дизайн может существенно уменьшить сложность — не сделав софт «простым», но сделав его менее запутанным для изменения. Это большое утверждение, потому что именно путаница превращает повседневную работу в медленную работу.
Брукс фокусируется на сущностной сложности: софт должен моделировать запутанную реальность, изменяющиеся требования и пограничные случаи вне кода. Даже с отличными инструментами это не исчезнет. Можно только управлять этим.
Они куда ближе, чем кажется:
Вместо вопроса «Кто прав?», спрашивайте: Какая сложность под нашим контролем в этом квартале?
Команды не могут контролировать рыночные изменения или фундаментальную сложность домена. Но они могут контролировать, добавляют ли новые фичи особые случаи, заставляют ли API запоминать скрытые правила, и протекают ли модули внутрь вызывающих.
Это действие посередине: принимать сущностную сложность и бескомпромиссно сокращать случайную.
Глубокий модуль — это компонент, который делает много, но предоставляет маленький, понятный интерфейс. «Глубина» — это насколько много сложности модуль снимает с вас: вызывающим не нужно знать грязных деталей, а интерфейс не заставляет их это делать.
Поверхностный модуль — наоборот: он оборачивает маленькую логику, но выталкивает сложность наружу — через множество параметров, флагов, обязательный порядок вызовов или правила «вы должны помнить…».
Подумайте о ресторане. Глубокий модуль — это кухня: вы заказываете «пасту» по простому меню и не заботитесь о поставщиках, времени варки или сервировке.
Поверхностный модуль — это «кухня», которая выдаёт вам сырые ингредиенты с 12‑шаговой инструкцией и просит принести свою сковороду. Работа всё ещё выполняется — но её переложили на клиента.
Дополнительные слои полезны, если они сводят множество решений в один очевидный выбор.
Например, слой хранилища, который предоставляет save(order) и внутри обрабатывает повторы, сериализацию и индексирование, — это глубокий модуль.
Слои вредят, когда они в основном переименовывают вещи или добавляют опции. Если новая абстракция вводит больше конфигурации, чем убирает — например save(order, format, retries, timeout, mode, legacyMode) — вероятно, она поверхностна. Код может выглядеть «организованным», но когнитивная нагрузка проявится во всех местах вызова.
useCache, skipValidation, force, legacy.Глубокие модули инкапсулируют не просто код — они инкапсулируют решения.
«Хороший» API — это не только тот, который многое может сделать. Это тот, который человек может удержать в голове, пока работает.
Линза Оустерхаута заставляет судить API по требуемому умственному усилию: сколько правил нужно запомнить, сколько исключений предвидеть и как легко случайно сделать неверно.
Человеко‑дружелюбные API обычно малые, согласованные и трудные для неправильного использования.
Малый не значит беспомощный — это означает, что поверхность сосредоточена в нескольких понятиях, которые хорошо компонуются. Согласованность значит, что один и тот же паттерн работает по всей системе (параметры, обработка ошибок, нейминг, типы возвращаемых значений). Трудность неправильного использования означает, что API направляет в безопасные пути: ясные инварианты, валидация на границах и типы или проверки времени выполнения, которые падают рано.
Каждый дополнительный флаг, режим или «на всякий случай» конфигурация становится налогом для всех пользователей. Даже если только 5% вызывающих нуждаются в нём, 100% теперь должны знать о его существовании, задумываться, нужен ли он им, и интерпретировать поведение при взаимодействии с другими опциями.
Так API накапливают скрытую сложность: не в одном вызове, а в комбинаторике.
Дефолты — это доброта: они позволяют большинству пропускать решения и получать здравое поведение. Соглашения (один очевидный способ) уменьшают ветвление в мыслях пользователя. Названия тоже выполняют реальную работу: выбирайте глаголы и имена, соответствующие намерению пользователя, и держите похожие операции с похожими именами.
Ещё одно напоминание: внутренние API так же важны, как публичные. Большая часть сложности продукта живёт за кулисами — на границах сервисов, в общих библиотеках и «хелпер» модулях. Обращайтесь к этим интерфейсам как к продуктам: ревью и дисциплина версионирования (см. также /blog/deep-modules).
Сложность редко приходит как одно «плохое решение». Она накапливается через маленькие, разумно выглядящие патчи — особенно когда команды под дедлайном и цель на данном этапе — выпустить.
Одна ловушка — флаги функций везде. Флаги полезны для безопасных выкатываний, но когда они затягиваются, каждый флаг умножает число возможных поведений. Инженеры перестают думать о «системе» и начинают думать о «системе, кроме когда флаг A включён и пользователь в сегменте B».
Другая — логика особых случаев: «Enterprise‑клиенты нуждаются в X», «Кроме в регионе Y», «Если аккаунт старше 90 дней». Эти исключения часто расползаются по коду, и через несколько месяцев никто не знает, какие ещё нужны.
Третья — протекающие абстракции. API, который заставляет вызывающих разбираться во внутренних деталях (тайминги, формат хранения, правила кэширования), выталкивает сложность наружу. Вместо одного модуля, несущего нагрузку, каждый вызывающий узнаёт причуды.
Тактическое программирование оптимизирует на эту неделю: быстрые фиксы, минимальные изменения, «залатать».
Стратегическое программирование оптимизирует на следующий год: небольшие редизайны, которые предотвращают те же классы багов и уменьшают будущую работу.
Опасность — «процент на обслуживание». Быстрый обход кажется дешёвым сейчас, но вы платите с процентами: медленный онбординг, хрупкие релизы и страх трогать старый код.
Добавьте лёгкие подсказки в код‑ревью: «Добавляет ли это новый особый случай?», «Можно ли спрятать это в API?», «Какую сложность мы оставляем позади?».
Ведите короткие записи по решениям для нетривиальных компромиссов (несколько буллетов достаточно). И оставляйте небольшой бюджет на рефакторинг в каждом спринте, чтобы стратегические исправления не считались внеплановой работой.
Сложность не остаётся в инженерии. Она просачивается в графики, надёжность и опыт клиентов.
Когда система тяжела для понимания, любое изменение занимает дольше. Time‑to‑market соскальзывает, потому что каждый релиз требует больше координации, больше регрессионного тестирования и более «на всякий случай» этапов проверки.
Надёжность тоже страдает. Сложные системы создают взаимодействия, которые никто не может полностью предсказать, поэтому баги появляются в виде крайних случаев: оформление заказа падает только если купон, сохранённая корзина и региональный налог совпали в особой комбинации. Такие инциденты тяжело воспроизвести и медленно фиксить.
Онбординг — скрытый тормоз. Новички не могут сложить полезную модель, избегают трогать рискованные области, копируют паттерны, которые не понимают, и непреднамеренно добавляют ещё сложность.
Клиентам всё равно, что поведение вызвано «особым случаем» в коде. Они видят непоследовательность: настройки, которые не действуют везде; потоки, которые меняются в зависимости от того, как вы попали; фичи, которые работают «большую часть времени».
Доверие падает, отток растёт, принятие замедляется.
Саппорт платит через длинные тикеты и много переписок, чтобы собрать контекст. Операции платят через больше оповещений, больше ранбуков и более осторожные выкаты. Каждое исключение — это то, что нужно мониторить, документировать и объяснять.
Представьте запрос на «ещё одно правило уведомления». Добавить его кажется быстрым, но он вводит ещё одну ветвь поведения, больше копии в UI, больше тесткейсов и больше способов, как пользователи могут неправильно настроить систему.
Сравните с упрощением существующего потока уведомлений: меньше типов правил, понятные дефолты и согласованное поведение на вебе и в мобильном. Вы можете выпустить меньше ручек, но уменьшите сюрпризы — сделав продукт проще в использовании, поддержке и эволюции.
Рассматривайте сложность как производительность или безопасность: планируйте, измеряйте и защищайте её. Если вы замечаете сложность только тогда, когда поставки замедляются, вы уже платите проценты.
Параллельно с объёмом фич определяйте, сколько новой сложности релиз может ввести. Бюджет может быть простым: «нет новых концепций, если мы не удалим одну» или «каждая новая интеграция должна заменить старый путь».
Явно делайте компромиссы при планировании: если фича требует три новых режима конфигурации и два исключения, она должна «стоить» больше, чем фича, которая укладывается в существующие концепции.
Не нужны идеальные числа — достаточно сигналов, которые показывают тренд:
Отслеживайте это по релизам и связывайте с решениями: «Мы добавили две публичные опции; что мы удалили или упростили в компенсацию?».
Прототипы часто оценивают по вопросу «можем ли мы это собрать?» Вместо этого используйте их, чтобы ответить: «Кажется ли это простым в использовании и трудным для неправильного применения?»
Попросите кого‑то незнакомого с фичей выполнить реалистичную задачу с прототипом. Измерьте время до успеха, вопросы и места неправильных предположений. Это «горячие точки» сложности.
Здесь современные рабочие процессы могут снизить случайную сложность — если они поддерживают быструю итерацию и лёгкий откат. Например, когда команды используют платформу вроде Koder.ai чтобы набросать внутренний инструмент или новый флоу через чат, функции как planning mode (чтобы прояснить намерение до генерации) и snapshots/rollback (чтобы быстро отменять рискованные изменения) делают ранние эксперименты безопаснее — без накопления полуброшенных абстракций. Если прототип проходит, вы всё ещё можете экспортировать исходники и применить те же принципы «глубоких модулей» и дисциплины API, описанные выше.
Задайте работу «очистки сложности» периодически (ежеквартально или после большого релиза) и определите, что значит «готово»:
Цель — не «красивый код», а меньше концепций, меньше исключений и более безопасные изменения.
Ниже — несколько шагов, которые переводят идею Оустерхаута «сложность — враг» в привычки команды неделя за неделей.
Выберите подсистему, которая регулярно вызывает путаницу (проблемы онбординга, повторяющиеся баги, много вопросов «как это работает?»).
Внутренние продолжения, которые вы можете запустить: «review сложности» при планировании (/blog/complexity-review) и быстрая проверка, уменьшает ли ваш тулкит случайную сложность или добавляет новые слои (/pricing).
Что бы вы удалили первым, если могли убрать только один особый случай на этой неделе?
Сложность — это разрыв между тем, чего вы ожидаете при изменении системы, и тем, что происходит на самом деле.
Вы ощущаете её, когда небольшие правки кажутся рискованными, потому что вы не можете предсказать радиус поражения (тесты, сервисы, конфиги, клиенты или крайние случаи, которые вы можете сломать).
Ищите признаки, что рассуждать о системе дорого:
Сущностная сложность исходит из предметной области (регулирования, реальных пограничных случаев, бизнес-правил). Её нельзя удалить — можно лишь корректно смоделировать.
Случайная (accidental) сложность — это самоналоженная: протекающие абстракции, дублированная логика, слишком много режимов/флагов, неясные API. Эту часть команды могут последовательно уменьшать через дизайн и упрощение.
A глубокий модуль делает много и при этом предоставляет маленький, стабильный интерфейс. Он «поглощает» грязные детали (повторы, форматы, порядок операций, инварианты), чтобы вызывающим модулям не приходилось об этом думать.
Практический тест: если большинство вызывающих могут правильно пользоваться модулем, не зная внутренних правил, он глубок; если вызывающим нужно запоминать правила и последовательности, модуль поверхностен.
Типичные симптомы:
legacy, skipValidation, force, mode).Предпочитайте API, которые:
Когда вас тянет добавить «ещё одну опцию», сначала спросите, можно ли переработать интерфейс так, чтобы большинство вызывающих вообще не думали об этом выборе.
Используйте флаги функций для контролируемых выкатываний, но рассматривайте их как долг с датой погашения:
Долговечные флаги умножают количество «систем», о которых инженерам нужно думать.
Сделайте сложность явной при планировании, а не только в код-ревью:
Цель — вынести компромиссы на обсуждение до того, как сложность станет институциональной.
Тактическое программирование оптимизирует эту неделю: быстрые патчи, минимальные изменения, «выпустить».
Стратегическое программирование оптимизирует следующий год: небольшие переработки, которые удаляют повторяющиеся классы багов и уменьшают будущую работу.
Полезная эвристика: если фикс требует знаний вызывающего («помни вызвать X первым» или «включай этот флаг в проде только»), вероятно, нужна более стратегическая правка, чтобы спрятать эту сложность внутри модуля.
Урок Tcl: сила маленького набора примитивов плюс сильная композиция — часто как встроенный «клей».
Современные эквиваленты:
Цель проектирования та же: держать ядро простым и стабильным, а изменение — через чистые интерфейсы.
Поверхностные модули часто выглядят организованно, но переносят сложность на каждого вызывающего.