| |

Хорошо спроектированное программное обеспечение должно быть «слишком простым»

Оригинал: Great software design looks underwhelming by Sean Goedecke

Несколько лет назад я потратил много времени на изучение проблем программирования. Сама задача была очень простой — создать CLI, который работал бы с API и позволял пользователю просматривать страницы и проверять данные. Мы допускали использование любых языков, и я видел самые разные варианты решений (если откровенно, сейчас я понимаю, что это было не совсем справедливое условие. Очевидно, что проверяющим было проще работать с языками, которые используются в компании. Поэтому кандидаты, которые выбрали другие языки (например, Java), оказывались в менее выгодном положении. Мы тогда пытались это как-то учесть, но лучше было бы просто сказать людям, чтобы они писали на одном из нескольких распространённых языков).

И вот среди всех решений я нашёл одно, которое показалось мне практически идеальным. Это был один файл на Python, всего около тридцати строк кода, написанный в очень практичном и прямолинейном стиле. Он делал именно то, что требовалось, самым простым и понятным способом.

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

Спустя годы я еще больше убедился, что был прав, а рецензент ошибался. Хорошо спроектированное программное обеспечение должно быть «слишком простым». И сейчас я, наконец, готов объяснить, почему это так.

Устранение риска

В любой программе есть множество мест, которые могут привести к сбоям. Иногда это называют “режимами отказа” системы. Вот пример:

  • Срок действия сертификатов SSL истекает, и они не обновляются
  • База данных заполняется слишком медленно или в ней не хватает памяти
  • Пользовательские данные перезаписываются или повреждаются
  • Пользователи видят сломанный интерфейс

  • Основные пользовательские процессы (например, сохранение записей) не работают

Есть два способа проектировать систему с учётом потенциального режима отказа. Первый — быть реактивным: добавлять блоки кода для обработки сбоев вокруг рискованных участков, обеспечивать повторение неудачных API-запросов, настраивать graceful degradation, чтобы ошибки не «взрывали» весь опыт использования, добавлять логирование и метрики, чтобы баги можно было легко идентифицировать, и так далее. Это стоит делать. На самом деле, я считаю, что такой (откровенно параноидальный) подход — признак опытного инженера-программиста. Но работа таким образом — не признак хорошего дизайна. Часто это сигнал, что вы замазываете недостатки плохого дизайна.

Второй способ справиться с потенциальными режимами отказа — спроектировать систему так, чтобы их не существовало. Что это означает на практике?

Второй способ справиться с возможными отказами заключается в том, чтобы исключить их существование. Что это означает на практике?

Защита критических путей

Иногда это означает вынос компонентов из критического пути. Я однажды работал над каталогом, который (из-за других архитектурных решений) был крайне неэффективным — порядка ~200 мс на запись. Это создавало для нас несколько неприятных режимов отказа: истощение ресурсов для остального приложения, таймауты прокси на запросах к индексу, и то, что пользователи просто сдавались после десяти секунд ожидания ответа. В итоге мы переместили код формирования каталога в cron-задачу, сохранили результаты в blob-хранилище и сделали так, чтобы endpoint каталога отдавал этот blob. У нас всё ещё остался тот ужасный код с 200 мс на запись, но теперь он был под нашим контролем: его нельзя было запустить действиями пользователя, и если он падал, в худшем случае мы просто отдавали устаревший blob.

Удаление компонентов

Иногда это означает использование меньшего количества компонентов. Ещё один сервис, над которым я работал, был CRM для документации, в котором была очень специфическая система для вытягивания различных частей документации из разных репозиториев и сшивания их вместе в записи базы данных (иногда вытягивая документацию прямо из комментариев в коде). Изначально это было хорошее решение — в то время было трудно заставить команды писать какую-либо документацию вообще, поэтому система должна была быть максимально гибкой. Но по мере роста компании её возраст стал сильно заметен. Задача синхронизации хранила часть состояния в базе данных, а часть — на диске, и часто вызывала странные ошибки git, когда состояние на диске рассинхронизировалось или на базовой машине заканчивалась память. В итоге мы полностью удалили базу данных, переместили всю документацию в центральный репозиторий и переделали страницу документации в обычный статический сайт (еще одна забавная история из этого проекта : мы использовали сессии, поддерживаемые базой данных, для хранения логинов пользователей , но у нас не было возможности их очистить . Мы обнаружили это , когда попытались перенести исходное приложение на новый уровень платформы , и дамп базы данных содержал десять миллионов строк с сессиями ). Всевозможные возможные runtime- и операционные баги были устранены, просто вот так.

Централизация состояния

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

Использование надёжных систем

Иногда это означает опору на проверенные в боях системы. Мой любимый пример — Ruby-вебсервер Unicorn. Это самый простой, незамысловатый способ, который только можно придумать, чтобы построить вебсервер поверх Linux. Сначала вы берёте серверный процесс, который слушает сокет и обрабатывает по одному запросу за раз. Обработка одного запроса за раз не будет масштабироваться: входящие запросы будут скапливаться в сокете быстрее, чем сервер успевает их обработать. Так что вы делаете? Вы делаете fork этого серверного процесса несколько раз. Из-за того, как работает fork, каждый дочерний процесс уже слушает исходный сокет, поэтому стандартная логика сокетов в Linux сама распределяет запросы равномерно между вашими серверными процессами. Если что-то пойдёт не так, вы можете убить дочерний процесс и мгновенно сделать fork нового.

Некоторые считают, что любить Unicorn немного глупо, потому что он очевидно менее масштабируем, чем сервер с тредами. Но я люблю его по двум причинам. Во-первых, потому что он перекладывает так много работы на примитивы процессов и сокетов в Linux. Это умно, потому что они ультранадёжны. Во-вторых, потому что рабочему процессу Unicorn очень и очень сложно сделать что-то плохое другому рабочему процессу Unicorn. Изоляция процессов гораздо надёжнее, чем изоляция тредов. Вот почему Unicorn — выбранный вебсервер для большинства крупных Rails-компаний: Shopify, GitHub, Zendesk и так далее. Отличный дизайн программного обеспечения не означает, что ваше программное обеспечение ультрапроизводительное. Это означает, что оно хорошо подходит для задачи (в большинстве случаев зарабатывание денег ).

Итог

Отличный дизайн программного обеспечения выглядит простым, потому что он устраняет как можно больше режимов отказа на этапе проектирования. Лучший способ устранить режим отказа — не делать что-то сложное (или, если возможно, не делать вообще ничего).

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

Это всё относительно скучные, непривлекательные идеи. Но отличный дизайн программного обеспечения — скучный и непривлекательный. Легко восхищаться большими идеями, такими как CQRS, микросервисы или service mesh. Отличный дизайн программного обеспечения не выглядит как большие захватывающие идеи. Большую часть времени он вообще никак не выглядит.

Похожие записи

  • | |

    Происходит нечто грандиозное

    Перевод оригинального эссе от 09 февраля 2026 года от Matt Shumer Вспомните февраль 2020 года. Если вы внимательно следили за новостями, вы могли заметить, что несколько человек говорили о распространяющемся за границей вирусе. Но большинство из нас не обращало пристального внимания. Фондовый рынок шёл в гору, ваши дети были в школе, вы ходили в рестораны,…

  • SRE Skills — ваш личный SRE :)

    Если вы работаете с высоконагруженными системами или развиваете практику Site Reliability Engineering (SRE), то наверняка сталкивались с необходимостью быстро получить квалифицированный совет, провести аудит или спроектировать систему observability. Репозиторий philyuchkoff/sre-skills предлагает элегантное решение: превратить LLM-агента (вроде Claude Code) в опытного SRE-консультанта. Давайте разберем, что внутри и как этим пользоваться. 🎯 Основная идея Проект предоставляет набор…

  • |

    Правило 10x/9

    Оригинал Каждая дополнительная «девятка» в SLO увеличивает надежность системы в 10 раз, но и одновременно увеличивает ее стоимость тоже в 10 раз (про реальность добавления «девяток» к аптайму). Я называю это правилом «10x/9» (читается как «десять иксов на девятку»). Когда я впервые услышал об этом, мне показалось это подозрительным, но, взглянув на математику и вспомнив…

  • |

    Grafana + Prometheus: обнаружение аномалий

    В предыдущей статье мы рассмотрели обнаружение аномалий с помощью правила 3 сигм в Influx. Теперь сделаем то же самое в Grafana + Prometheus. Краткое напоминание: правило 3 сигм Правило трех сигм утверждает, что приблизительно все наши «нормальные» данные должны находиться в пределах трех стандартных отклонений (σ) от среднего значения (μ) ваших данных. В этой статье исследуется, как мы можем измерять…

  • Как читать книги про SRE, чтобы они приносили пользу

    Каждый SRE знает это чувство: открываешь «Site Reliability Engineering» от Google, читаешь про SLI/SLO, Error Budgets, и думаешь: «ого, круто, надо внедрить у себя в конторе». Проходит полгода. Книга пылится на полке, инциденты по-прежнему разбираются как получится, алерты пишутся вручную, мониторится все, что можно, метрики собираются все, что существуют и ничего не изменилось. Проблема не…

  • Resilience Engineering

    Resilience Engineering (инженерия устойчивости) — это подход к проектированию и управлению системами, который фокусируется на их способности предвидеть, адаптироваться и восстанавливаться после сбоев, а не просто избегать их. В отличие от традиционных методов, которые стремятся к «идеальной» надежности (SRE), Resilience Engineering (RE) признает, что сбои неизбежны, и делает упор на устойчивость системы к неожиданным событиям и к…