Архитектурные решения
Результатом работы прикладного инженера (результатом выполнения методов концептуального и детального проектирования) будет концепция использования, а также концепция системы, а затем набор описаний системы с достаточной детальностью для изготовления (чертежи, программный код, какие-то физические модели и т.д.), плюс инженерные обоснования, для которых готовятся описания испытаний и аргументация. В итоге ожидается, что система будет успешна в части выполнения своих функций: компьютер будет считать, телефон звонить, организация выпускать продукт, сообщество развивать какую-то субкультуру.
Результатом работы архитектора (не функционального) в обеспечение приемлемых значений архитектурных характеристик (выбор которых — тоже его решение) будет набор архитектурных решений по разбивке системы на модули/конструктивные объекты и определение способов связи между модулями. Главная цель архитектора при этом — провести границу между модулями так, чтобы внутри границы связи между подмодулями были плотными (про связи внутри модуля говорят как про cohesiveness**/сплочённость, а между модулями — как coupling/связанность/зацепление**). Если система вся получилась несвязной/незацепленной, невзаимодействующей, то беда: эмерджентные свойства (функция системы в том числе) появляется как раз из взаимодействия частей. Нет взаимодействия — нет системы. Если вся система чрезвычайно связана, то
- ошибка в одном модуле во время эксплуатации приводит к быстрому распространению по всей системе. Это динамическая связность, которую нужно уменьшать — но до оптимума, а не ниже.
- изменения в одном модуле, сделанные разработчиком (время создания), приводят к необходимости изменений в другом модуле. Это статическая связность, которую нужно уменьшать — но до оптимума, а не ниже.
Например, если вы хотите повторно использовать какой-то фрагмент кода программы (в 90е годы была ценность reuse, «повторного использования», выноса за скобки любой функциональности и реализации её один раз, «отсутствие дублирования»), то неожиданно оказывается, что ошибка в этом фрагменте влияет на выполнение всей программы во всех местах его использования — скажем, в 1000 мест. А если вы занимаетесь улучшением этого фрагмента, то улучшение его исполнения для 3 случаев из этого задействования может привести к ухудшению для остальных 997 случаев изо всех 1000 (динамическая связность). А если меняете интерфейс (в архитектуре часто говорят «контракт», упирая на то, что интерфейс должен быть стабилен), так это требует изменения во всей тысяче модулей (статическая связность). Так что мы имеем тут U-образную «экономическую кривую» полезности уровня зацепления: связность в системе должна быть не слишком маленькой, но и не слишком большой. Архитектурные решения ищут оптимум, для этого проходят самые разные развилки в группировке функций по модулям.
Архитектура (хотя, конечно, речь идёт об архитектурном описании**—** но слово «описание» часто опускают) тем самым не сводится к каким-то структурным (то есть «разбиения на части») диаграммам, а сводится к набору архитектурных решений по прохождению архитектурны****х развилок (trade-offs)****, прежде всего:
- какие архитектурные стили (шаблоны структуры системы, им будет в нашем руководстве посвящён отдельный подраздел) использовать («монолит», «слоёный монолит», «микроядро» и так далее), в том числе конкретная нарезка на модули верхнего уровня, соответствующих стилю для принятого решения.
- какие принципы взаимодействия между модулями (синхронная коммуникация, асинхронная коммуникация, есть ли квитирование/подтверждения приёма запросов и т.д.), в том числе выбор архитектурного инструментария для реализации этих принципов взаимодействия (фреймворки, платформы).
Архитектурные решения предположительно ведут к успешности системы в части архитектурных интересов/характеристик (всяческих -ilities/-остей). То, что их архитектор принял не «чуйкой», а подумав, надо подтверждать обоснованиями, как и любые другие инженерные решения. В нашем руководстве есть целый раздел по инженерным обоснованиям: в момент принятия это rationale, а обоснование измерением по факту реализации (assurance) — оно делается задействованием fitness functions как «архитектурных тестов».
Лучший метод по оформлению архитектурных решений на сегодня — это форматирование их в виде записи архитектурного решения (ADR, architectural decision record, предложено Michael Nygard в 2011 году[1]). ADR — это документ на пару страниц текста (или экранов), имеющий примерно следующий формат (текст пишется в виде рассказа для разработчиков):
- **Заголовок/**title: короткое имя для принятого решения.
- Ситуация: описывает различные соображения в части влияния на архитектурные характеристики, в том числе именно тут описываются конфликты разных системных уровней, которые требуют оптимизации.
- Решение: что делать, чтобы достичь предположительного квазиоптимума (например, использовать какой-то архитектурный стиль или стандарт для межмодульного интерфейса)
- Статус: это предложение, это обязательно для выполнения, это решение уже отменено каким-то другим предложением (всё меняется, в том числе архитектурные решения)
- Последствия: что ожидается после реализации решения, в том числе «позитивные» (скажем, «производительность вырастет впятеро») и отрицательные (скажем, «вносить изменения придётся не в один, а в три разных модуля»).
Конечно, есть множество вариантов оформления ADR[2], равно как и других (чаще всего более простых) форм документирования архитектурных решений. Интересное развитие тут дало распространение fitness functions в качестве формы документирования архитектурных решений по выбору важных архитектурных характеристик. Тут прямое соответствие тому, что происходило с требованиями: сначала были use cases, по которым разрабатывались требования, по которым разрабатывались тесты, а закончилось тем, что сценарии использования начали сразу писать на языке тестирования. Так и тут: решение по выбору характеристики записывается сразу в виде текста процедуры замера этой характеристики. Но вот инженерное обоснование (почему принято такое решение) будет записано всё-таки в ADR.
Про принятие архитектурных решений путём прохождения развилок (trade-offs) на примере корпоративного программного обеспечения в крупных компаниях, где требуется обеспечить высокую скорость разработки и высокую производительность (распараллеливание работы между вычислителями) говорится в книге «Software Architecture: The Hard Parts».
Вот пример архитектурного решения из этой книги, оно относится к разбивке на модули:
- ADR: Общий сервис для назначения и маршрутизации заявок.
- Ситуация: когда заявка создана и принята системой, она должна быть назначена эксперту и затем маршрутизирована на мобильное устройство этого эксперта. Это может быть сделано одним общим сервисом назначения заявок или отдельными сервисами назначения и маршрутизации.
- Решение: Мы создадим один общий сервис для функций назначения и маршрутизации заявки. Заявки немедленно маршрутизируются эксплуатационному эксперту, как только они назначены, так что эти две операции тесно связаны и зависят друг от друга. Обе функции должны масштабироваться одинаково, поэтому нет никакой разницы по производительности между этими двумя сервисами, и нет никаких обратных зависимостей между этими функциями. Так как обе функции полностью зависят одна от другой, устойчивость к отказам не является поводом реализовывать их отдельно. Если эти две функции разделить, то потребуется организовывать между ними поток работ, и это даст дополнительные проблемы с производительностью, устойчивостью к отказам, а также возможные проблемы с надёжностью.
- Следствия: Изменения алгоритма назначения (эти изменения происходят на регулярной основе) и изменения механизма маршрутизации (нечастые изменения) могут потребовать тестирования и разворачивания обеих функций, результат будет в возрастающем объёме тестирования и рисках разворачивания.
В этом примере не приведён статус, ибо это «проект из книжки» (в реальном проекте нужно указывать статус, ставить архитектурные решения под контроль конфигурации).
Все эти архитектурные решения — строго результат прохождения развилок, то есть явного выбора из нескольких альтернатив. Часть этих решений описана в архитектурных паттернах. И это решения, относящиеся главным образом к связям между модулями. Эти связи и сами решения оказываются вовсе неочевидны.
Вот пример: кто должен проверять поступающее на вход модуля сырьё? Скажем, кто должен проверять, что Вася не засунул в мясорубку гвозди? Варианты:
- Проверок нет вообще
- Вася ответственный за то, чтобы в мясорубку гвозди не засовывать (и даже мясо чтобы было не гнилое)
- Мясорубка имеет входной контроль (и на гвозди, и на мясо, и на попытки работы без сырья вообще)
- И Вася, и мясорубка проверяют: один что кладёт, вторая --- что ей дали
Оказывается, в общем случае выгодно, чтобы проверок не было: это быстрее и дешевле всего, при этом такое можно сделать, если Вася и мясорубка срощены между собой, и это один модуль. Очевидно, что в данном случае так сделать нельзя.
Наиболее быстрое выполнение происходит в том случае, когда входного контроля нет, но есть жёсткий выходной контроль у Васи (ибо на вход мясорубки Вася подаёт в конечном итоге свою работу, и хорошо бы её проверять).
Если заранее непонятно, кто когда что подаёт на вход мясорубки и где её будут эксплуатировать, то выполнение контракта (контрактом/contract называют правила взаимодействия на интерфейсе с модулем, иногда это же называют протоколом) должна проверять мясорубка. В тех случаях, когда Вася тоже проверяет то, что он даёт мясорубке, возникает дублирование. Но в архитектуре есть принцип, что в большинстве случаев развязывание**/****расцепление/**decoupling модулей важнее отсутствия дублирования.
Так как же надо? Если делаете публичный сервис, то лучше бы мониторить то, что подаётся на вход, вести логи для ловли ошибок (кто когда почему подал на вход мясорубки гвозди: это не должно повториться!). Если модуль выполняет чётко определённую функцию, должен работать сверхбыстро, то проверки входных объектов для обработки (сырья, информации) в нём не должно быть, это дорого. Но выходные проверки лучше бы делать. Ну, или добиваться такой работы, чтобы выходные проверки были не нужны «по определению», но такое придётся доказывать (писать обоснования).
Пример: в Tesla на заводе agile производство устроено так, что каждый автомобиль собирается в уникальной конфигурации, ибо отдельные производственные участки часто дублируются, чтобы опробовать какой-нибудь новый способ производства — новый станок, новый приём работы. Старый участок работает по-старому, новый — по-новому. Через новый участок отправлятся часть деталей или подсборок, чтобы «попробовать». В случае успеха старый участок закрывается и все полуфабрикаты дальше идут через новый участок. В случае неуспеха — экспериментальный участок закрывается, продолжает работать старый участок.
Конечно, все эти производственные участки имеют выходной контроль — и старые, и новые экспериментальные. Но поскольку экспериментальный участок новый и не слишком отлажен, то этот его выходной контроль тоже новый, может не суметь проверить что-то важное. Поэтому на Tesla принято решение по связи между производственными участками/модулями: все участки (и старые, и новые) обязаны иметь и входной контроль (чтобы отлавливать возможные проблемы с деталями, пришедшими с новых плохо отлаженных участков) и выходной контроль (чтобы у следующих за ними участков не было проблем с негодными деталями).
Однозначного ответа «как надо» в архитектурных выборах нет, всё зависит от вашего проекта, и даже конкретной ситуации в вашем проекте (сегодня оптимальный ответ будет один, а через год в этой же системе ответ будет другим — и придётся всё переделывать. Это нормально, это и есть развитие/evolving системы и её архитектуры).
Решение о том, в каком модуле делать проверки — архитектурное****решение, оно по факту не относится к целевой функциональности, оно относится к модульному синтезу для функциональности «проверка». Рассуждения примерно одинаковы, идёт ли речь о Васе и мясорубке, или программе искусственного интеллекта, вызывающей модуль определения кошки и дающей не ожидаемую этим модулем фотографию с потенциальной на ней кошкой, а текст со словом «кошка», в надежде, что этот модуль эту «кошку» найдёт (как Вася надеется, что мясорубка сделает из гвоздей гвоздяной фарш, ибо ему сказали, что «желудки этих людей даже гвозди переварят!», и он просто хотел проверить, так ли это). Архитекторов тренируют на подобного сорта рассуждения: развязывание/расцепление против дублирования (и предупреждают, что дублирование часто лучше!).
Понятие межмодульной связи довольно сложное, и это один из основных объектов, с которым работает архитектор. У вас радиотехническое устройство. Правильно ли будет, если вы все резисторы поместите на одну плату, конденсаторы на другую плату, катушки::модуль индуктивности::функция на третью плату? Заметим, что в предприятии более чем часто делают «отдел со всеми программистами», «отдел со всеми юристами», и хорошо, что не делают «отдел со всеми менеджерами» (рассуждение по той же линии: «они все там одного типа, с одним образованием, пусть сидят вместе»). Это ведь ровно такой же архитектурный вопрос, архитектура-то универсальное понятие для всех типов систем — архитектура радиотехнического устройства и архитектура предприятия по факту требуют одинакового архитектурного мышления.
Или на одной плате должен быть «усилитель высокой частоты», а на другой плате «усилитель низкой частоты» и все резисторы-конденсаторы-катушки индуктивности должны быть там? Или нужно преодолеть огромные трудности и сделать всё на одной гигантской плате, но это «объединение в один модуль» потребует перестройки всего производства? Такой путь, например, был выбран в попытке сделать один чип на 4 триллиона транзисторов (третье поколение чипов Cerebras[3]): целью было получить максимальную производительность, и отчасти это даже удалось, но пришлось решать множество нестандартных проблем, типа отвести 3.5КВт тепла с площади чипа размером примерно с листок А4[4].
https://www.cognitect.com/blog/2011/11/15/documenting-architecture-decisions ↩︎
https://github.com/joelparkerhenderson/architecture-decision-record, https://adr.github.io/ ↩︎
https://cerebras.ai/press-release/cerebras-announces-third-generation-wafer-scale-engine ↩︎
https://www.cerebras.net/blog/cerebras-wafer-scale-engine-inducted-into-the-computer-history-museum/ ↩︎