В октябре 1984 года два идеолога опубликовали радикальный манифест… ну, или что-то вроде того.
Легенды computer science Брайан Керниган и Роб Пайк сформулировали в Program Design in the UNIX Environment паттерн архитектуры ПО, за сохранение которого оба боролись долгие годы.
Как и следовало ожидать от манифеста, в нём два этих канадских инженера максимально решительны. Самый резкий удар в статье — это запомнившаяся многим строчка из аннотации:
Старые программы покрываются коркой сомнительных фич.
Суть статьи часто сводят к аббревиатуре DOTADIW, или «Do One Thing And Do It Well» («Делайте что-то одно и делайте это хорошо»). В Unix и его потомках есть множество программ, в которых воплощена эта мантра: ls
просто создаёт список файлов, cat
просто выводит содержимое файлов, grep
просто фильтрует данные, wc
просто подсчитывает слова и так далее. У каждой программы есть несколько опций, меняющих её поведение, но не слишком сильно. Например: wc
можно сконфигурировать для подсчёта строк или слов, но не для подсчёта количества абзацев или вхождений какой-то фразы.
Мощь Unix, защищаемая Керниганом и Пайком, заключалась в возможности соединения этих простых программ в цепочку для создания сложных поведений. Зачем добавлять сопоставление регулярных выражений в wc
, если с этим уже способна справиться grep
? Для подсчёта количества функций в файле Rust можно выполнить такую команду:
cat main.rs | grep "^s*fns" | wc -l
В переводе с Bash на русский это означает: прочитай файл, отфильтруй в нём только строки, содержащие функции (строки, в которых первый текст без пробелов равен fn
), а затем подсчитай эти строки. Оператор pipe (|
) просто передаёт вывод одной программы на вход следующей.
Это отличная идея! Простые программы, составляющие эту команду, легко разрабатывать и поддерживать. На самом деле, они настолько просты, что в них даже могут полностью отсутствовать баги, а это свойство практически недоступно для любого более сложного ПО.
К сожалению, как и в случае с большинством манифестов, этот идеал не выдержал столкновения с реальностью. Программы для Unix могут общаться только в одном направлении и только при помощи отправки потоков текста. Модель имела определённый смысл в терминальной среде, но так и не совершила успешного перехода в десктопные операционные системы. Поэтому популярные современные программы наподобие Photoshop и Word максимально «покрыты коркой сомнительных фич». Прекрасная идея Кернигана и Пайка так никогда и не дала плодов.
▍ Гниение платформ
Допустим, Керниган и Пайк были правы хотя бы в одном: что раздувание ПО является проблемой. Огромные приложения сложно изучать, трудно использовать и в них куча багов. Частично это проблема UX, однако по большей мере это симптом проблемы разработчика.
Крупные монолитные приложения имеют большие кодовые базы, замедляющие скорость разработки. Они медленнее компилируются, их сложнее тестировать, внутри них куча тёмных уголков, где могут таиться и размножаться баги. Плохие изменения в одной части кодовой базы могут вызвать проблемы у всего офиса разработчиков, снижая продуктивность на часы или дни.
Но, разумеется, пользователей не волнует скорость разработки и размер кодовой базы. К сожалению, эти качества неразрывно связаны с тем, что для пользователей важно: со скоростью, стоимостью, фичами и, что самое важное, с надёжностью.
В общем случае скорость внесения багов пропорциональна скорости разработки. Каждая строка кода с некой вероятностью (обозначим её Pbug) может вызвать баг, поэтому чем меньше добавляется строк, тем меньше шансы появления новых багов. С уменьшением скорости разработки должна падать и скорость появления новых багов.
К сожалению, это соотношение не совсем постоянно; существуют две силы, сдвигающие равновесие к худшему с увеличением кодовой базы.
Во-первых, снижение скорости разработки заставляет команды идти на сложные компромиссы, чтобы сдать работу вовремя, что часто приводит к снижению качества. В конце концов, руководство будет разгневано запоздалым выпуском фичи, но, возможно, не заметит отложенное устранение бага. Поэтому с падением скорости разработки неизбежно ещё сильнее падает скорость устранения багов.
Во-вторых, Pbug увеличивается с ростом кодовой базы. Ранее я писал о том, что самые пагубные баги являются структурными, а не алгоритмическими, то есть вызванными неожиданным способом взаимодействия двух подсистем, а не ошибками смещения на единицу или математическими ошибками. Поэтому вместе с увеличением количества подсистем и модулей растёт и Pbug. При достаточно большом масштабе систем очень вероятно, что ни автор коммита, ни ревьюеры не понимают полный контекст достаточно хорошо, чтобы устранять баги до их внедрения.
И, разумеется, снижение скорости и повышение Pbug приводит к тому, что устранение новых добавленных багов требует много времени и чревато внедрением ещё большего количества багов.
Добавляемые баги примерно пропорциональны скорости, умноженной на процент времени, который тратят на новые фичи, умноженный на Pbug
Постепенно крупные кодовые базы достигают точки гниения (enshittification), после которой баги добавляются быстрее, чем их можно устранить. Конкретный размер, при котором это происходит, зависит от команды и архитектуры, но даже самые талантливые инженеры сталкиваются с гниением, если их кодовой базе позволяют неконтролируемо расти. Подведём итог: крупные монолитные кодовые базы приводят к отстойному ПО.
Но по-прежнему остаётся открытым такой вопрос: могут ли на практике кодовые базы меньшего размера приводить к созданию чуть менее отстойного ПО? Или у них есть собственные проблемы?
▍ Микросервисы: DOTADIW в больших масштабах
1990-е: спагетти-ориентированная архитектура (копипейст); 2000-е: лазанья-ориентированная архитектура (многослойный монолит); 2010-е: равиоли-ориентированная архитектура (микросервисы). Что дальше? Возможно, пицца-ориентированная архитектура. Источник: The Evolution of Software Architecture (Бенуа Хедиар)
Вокруг преимуществ (или недостатков) архитектуры микросервисов ведутся жаркие дебаты. Основная её идея заключается в том, что крупные серверы можно разбить на дискретные части, каждая из которых отвечает за один аспект системы. Один сервер может обрабатывать аутентификацию, другой отслеживать предпочтения пользователей, третий — заниматься выявлением спама. Иными словами, микросервисы должны «делать что-то одно и делать это хорошо».
Было описано множество случаев, когда архитектура микросервисов работает хорошо, а когда не очень. Вот несколько ресурсов, которые показались мне полезными:
- Amazon Prime Video’s Microservices Move Doesn’t Lead to a Monolith after All [The New Stack]
- When Microservices Are a Bad Idea [Tomas Fernandez]
- Breaking Up a Monolith: Kong Case Study [Marco Palladino]
Я заметил следующий общий паттерн:
Писать микросервисы трудно. Для создания архитектуры сети микросервисов требуется тратить много дополнительных ресурсов и необходимо много бойлерплейт-кода для конфигурирования и ориентирования элементов. Микросервисы оправдывают себя только после того, как проект превысит определённый размер. В этот момент их относительно малое время компиляции и хорошо кодифицированные взаимодействия подкомпонентов повышают скорость разработки и снижают Pbug, позволяя кодовым базам разрастаться до чрезвычайно больших размеров без гниения (но это не всегда гарантировано).
С другой стороны, некоторые проекты откровенно лучше работают в виде монолитов. При использовании микросервисов приходится расплачиваться производительностью. Важность этого фактора зависит от характеристик конкретного проекта. Архитекторам ПО не зря платят так много: для принятия решений им приходится учитывать бесчисленное количество факторов и компромиссов, которые в долговременной перспективе запросто могут привести к экономии или тратам миллионов долларов.
Как минимум, микросервисы — отличный инструмент в арсенале разработчика серверов. Чем больше инструментов, тем лучше. В основном я фронтенд-разработчик, поэтому мне завидно! Какое же оружие я могу использовать против сил гниения?
▍ Апплеты?
При разбиении сервисов на мелкие части мы получаем микросервисы, а при разбиении фронтендов — очевидно, микрофронтенды. Но на самом деле микрофронтенды на самом деле являются ребрендингом старой идеи: апплетов.
Типичный Java-апплет из начала 2000-х
«Апплет» (applet) — это уменьшительная форма слова App («приложение»). Это лаконичное обозначение маленькой программы с узкой функциональностью, которая не покроется коркой сомнительных фич.
К сожалению, похоже, большинство создателей апплетов не дочитали мантру Кернигана и Пайка до конца: хотя апплеты «делали что-то одно», им редко удавалось «делать это хорошо». Слово «апплет» обычно синонимично Java, хотя можно включить в этот зонтичный термин и приложения на ActiveX и Adobe Flash. Если вы слишком молоды, чтобы помнить, когда произвольные части веб-сайтов ломались из-за того, что какой-то плагин был «слишком старым» или «слишком новым», или «не поддерживался вашей ОС», то, наверно, мне стоит вас поздравить? Вы не пропустили ничего хорошего.
Ещё важнее то, что апплеты были практически полностью автономными. Не было никакой возможности объединить несколько апплетов в цепочку процесса аналогично командам Unix. Даже их взаимодействие с ОС и файловой системой было ограничено из соображений безопасности. Они были в «маленькими приложениями» в том же смысле, в каком детские электромобили являются «маленькими автомобилями»: забавные, но бесполезные.
Так что, возможно, настало время избавиться от принципа «делайте что-то одно и делайте это хорошо» для фронтендов, выбросив её в кучу других красивых, но неработающих идей наподобие цеппелинов и коммунизма. Терминал Unix не захватил в одночасье весь мир, не удалось этого сделать и апплетам. Возможно, гниение — просто неотъемлемая часть жизни фронтенд-разработчиков, фундаментальное следствие энтропии, постепенное угасание, свойственное и нашим стареющим телам.
А может быть, апплеты просто реализовали идею неправильно.
▍ Плагины!
Недавно я писал о старом стандарте десктопного ПО: нативных приложениях для Mac. В частности, я писал об утере их актуальности. Основная причина этого в том, что смартфоны совершенно поглотили принцип «персональности» персональных компьютеров. Воспользовавшись аналогией Стива Джобса, можно сказать, что ноутбуки и десктопные компьютеры превратились из прикольных семейных седанов в скучные рабочие грузовики.
Ещё более точная аналогия — трактор. У грузовика одна задача: брать грузы в одном месте и перевозить их в другое. У современных тракторов может быть куча предназначений: они могут возделывать землю, сеять, убирать снег, рыть ямы и так далее. То есть они представляют собой общую платформу, к которой подключаются («плагинятся») другие специализированные инструменты.
Это привычный шаблон проектирования промышленного оборудования. Цифровые кинокамеры — это сборные конструкторы для съёмки фильмов, скелетом которых является видеосенсор. Промышленные роботы можно сконфигурировать со множеством различных производственных инструментов. Некоторые здания сегодня строятся сборкой заранее изготовленных частей поверх стандартного фундамента.
Чтобы превратиться в «синие воротнички», устройства, которые раньше назывались персональными компьютерами, должны адаптироваться к этому промышленному образу
Наилучшим актуальным примером этого является приложение Obsidian. При поверхностном взгляде оно может и не выглядеть тяжёлым оборудованием. На самом деле, оно даже довольно милое.
Obsidian 1.4 в macOS и iOS
По своей сути Obsidian — это редактор markdown. И он справляется с этой задачей хорошо. Но его настоящая магия заключается в экосистеме плагинов. Плагины Obsidian не ограничиваются включением боковых панелей и добавлением новых элементов меню, они могут добавлять новую функциональность «холсту», на котором редактируется и потребляется контент. Уже написаны плагины, позволяющие встраивать код, генерировать индексы и даже интегрировать большие языковые модели напрямую в обычный процесс написания текстов. Сабреддит Obsidian похож на сообщество водителей грузовиков, хвастающихся своими машинами. Это программное обеспечение для изобретателей и экспериментаторов.
Хорошее расширяемое десктопное приложение должно уметь становиться своего рода «операционной системой», но построенной на основе некого вида «холста». VS Code — это холст для программных проектов, Blender — холст для 3D-сред, Figma — холст для дизайна UI. У каждого из этих приложений есть качественные встроенные функции редактирования, но каждое из них обогащается благодаря обширным экосистемам плагинов, добавляющих новую функциональность напрямую на холст.
Но это проще сказать, чем сделать. Ограничивать плагины от того, чтобы они не мешали работе друг друга на одном общем холсте — это как пасти кошек. Это очень сложная проблема дизайна API, но, по крайней мере, она одна. В отличие от микросервисов, плагинам не нужны сложные сети компонентов, каждый из которых имеет собственные связи. Всё упорядочено вокруг центрального хаба, что обеспечивает некую степень порядка.
В идеале этот центральный хаб должен быть максимально небольшим, как ядро операционной системы. В Obsidian даже базовые функции приложения наподобие диспетчера файлов и строки поиска реализованы как плагины. Пользователи получают от гибкости как прямую (не нравится диспетчер файлов? Просто замени его другим!), так и косвенную пользу (высокую скорость разработки). Obsidian — полностью функциональное ПО, созданное семью людьми и кошкой. По моим впечатлениям, оно очень надёжно и быстро совершенствуется.
Obsidian не «делает что-то одно и хорошо». Он чудесным образом «делает многое и делает это хорошо». Но в чём-то это мираж: на самом деле, Obsidian — не одно приложение, а что-то вроде многоклеточного организма. Каждая его часть — это отдельная и специализированная единица, как и задумывалось Керниганом и Пайком. Вместе они создают своего рода эмерджентное поведение, которое предполагалось как возможное благодаря шеллу Unix. В этом смысле Obsidian и его ilk (как и VS Code) нашёл Святой грааль разработки ПО.
Так в чём же секрет? И почему сейчас? Что такого в его модели плагинов, которая работает там, где другие модели потерпели поражение?
▍ Соединяем части
Крошечные части ПО с единственной функцией сами по себе не так уж полезны. Они становятся полезными, только когда объединяются способами, обеспечивающими появление более сложных поведений. Апплеты как концепция провалились, потому что их совершенно нельзя было комбинировать. Различия между другими моделями в основном сводятся к конкретному способу реализации соединения частей.
Хорошая аналогия — это игрушечные конструкторы. Один кирпичик Lego не особо полезен, но его можно скреплять с другими кирпичами, образуя красивые здания и конструкции. Программные архитектуры работают схожим образом.
Деревянные кубики никак друг с другом не соединяются. Кирпичики Lego соединяются, но только в одном измерении — из них можно создавать только линии и плоскости. K’nex могут прикрепляться под углами вокруг центрального хаба. А лабиринты для хомяков — это просто хаотичное соединение элементов произвольным образом.
Если отбросить сложные аналогии, то получим следующее:
Модель хаба с ответвлениями, используемая для плагинов, работает, потому что соблюдает нужный баланс — она достаточно гибка, чтобы создавать сложные поведения, но не настолько гибка, чтобы приложения оказалось невозможно отлаживать или поддерживать. Модель позволяет плагинам пользоваться единым холстом и даже косвенно взаимодействовать друг с другом, в то же время заставляя их следовать согласованным правилам.
Но для микросервисов тоже есть своё место. Модель хаба с ответвлениями можно назвать и атомарной моделью — маленькие независимые электроны (плагины) вращаются вокруг твёрдого ядра. Приложения, как и атомы, могут содержать ограниченное количество элементов, иначе станут нестабильными. Проблема урана не в том, что он «плохо спроектирован», а в том, что он просто «раздут».
Микросервисы напоминают другую микроскопическую единицу вещества — молекулу. Молекулы могут оставаться стабильными даже с миллионами частей, но ценой сложности: в атомной физике есть лишь несколько конкретных законов, а молекулярная физика полна эмпирических правил и догадок.
Как ни грандиозно бы звучало сравнение разработки ПО с квантовой физикой, мне кажется, эти две области имеют много общего. Всё в природе, от ярчайшей звезды до простой картошки, состоит из единиц простой материи. Всё разнообразие аспектов реальности возникает благодаря конкретной структуре выстраивания этих единиц. Аналогично, успешной программной архитектурой становится та, в которой однофункциональные блоки кода выстроены в синергетическое целое.
Учёный может изучать сложный объект, исследуя его фундаментальную структуру через микроскоп. У инженера почти такая же задача, только у него нет микроскопа. Вместо этого инженеры должны изобретать каждый атом и молекулу, каждую систему и подсистему, пока в конечном итоге не получат нужный результат. Стать мастером в этой работе невозможно. Все мы стремимся к лучшему, но часто терпим неудачу.
Простых ответов не существует, но повсюду можно найти подсказки. Чтобы подобрать идеальную структуру для сложного ПО, обратите внимание на структуры, пережившие миллиарды лет жесточайшей эволюции: атомы и молекулы, солнечные системы и галактики, наши собственные внутренние органы. Тракторы и камеры. Obsidian и VS Code. Все они созданы из маленьких однофункциональных блоков, как и задумывалось Керниганом и Пайком.
Автор:
ru_vds