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

Эти статьи предназначены для краткого чтения с практической направленностью. Таким образом, я хочу пропустить довольно теоретические вопросы, такие как возможное определение термина «архитектура программного обеспечения» или различие между архитектурой программного обеспечения и проектированием программного обеспечения. Есть много хороших статей и статей, которые уже затрагивают эти вопросы ». В рамках данной серии статей мы рассматриваем любые проектные соображения и решения, которые позволяют вашей программной системе развиваться с точки зрения определенного набора критериев качества.

Если вы не читали первую часть этой серии, вы можете найти ее здесь:
Практические советы по проектированию архитектуры программного обеспечения, часть первая

Подробнее о требованиях…

# 7 Определите важные функциональные требования

В предыдущей статье мы уже говорили о важности функциональных и нефункциональных требований. В то время как функциональные требования определяют поведение и варианты использования системы, нефункциональные требования определяют характеристики системы, такие как ее атрибуты качества (производительность, переносимость, масштабируемость, …) Или технические ограничения.

Как архитекторы, наша миссия - найти системный дизайн, который

  1. позволяет и поддерживает реализацию заданных функциональных требований
  2. соответствует указанным нефункциональным требованиям

Хотя есть и другие факторы, которые влияют на наши дизайнерские решения, такие как организационные ограничения или соображения риска, вышеупомянутая миссия является нашей основной игрой: требования будут движущими силами наших дизайнерских соображений. По сути, они формируют проблему, решение которой мы пытаемся найти.

В совете №5 я уже упоминал, что нефункциональные требования оказывают значительное влияние на архитектурный дизайн системы. А как насчет функциональных требований?

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

Во многих случаях функциональные требования предполагают или даже требуют определенного выбора дизайна и технологий. Если наша архитектура не принимает их во внимание, функции становятся трудными или даже невозможными для реализации, или мы вносим случайную сложность, предоставляя неоптимальную техническую среду для реализации.

В качестве тривиального примера приложение, управляемое формами данных и отчетами, может предложить использование (реляционной) базы данных. Конечно, мы могли бы использовать другой подход и вместо этого использовать простой файловый ввод-вывод. Но как только для отчетов потребуются какие-то агрегированные данные или сложная проекция, мы начнем реализовывать части функций управления данными, которые база данных будет предоставлять из коробки. Это простой пример, демонстрирующий случайную сложность - термин, введенный Фредом Бруксом в его знаменитой статье «Никакой серебряной пули». Это техническая сложность, которая не имеет отношения к проблеме, часто возникающая из-за неправильного выбора языка и инструментов.

Обычно мы можем идентифицировать кластеры функциональных требований, которые выполняются или поддерживаются одним и тем же соображением дизайна:

Пример
Рассмотрим приложение CRM, которое имеет следующие функциональные требования:

1) Формы данных, такие как создание нового клиента, могут быть визуально адаптированы к корпоративному дизайну организации путем указания пользовательских цветов, пользовательского шрифта, а также пользовательского изображения бренда.
2) Обязательные поля в формах данных окрашены в мягкий цвет, чтобы подчеркнуть их важность.

Как только мы нашли проект системы, который позволяет и поддерживает реализацию требования 1, маловероятно, что требование 2 окажет значительное влияние на наши соображения по проектированию системы. Оба требования требуют богатого пользовательского интерфейса и возможности его визуальной настройки на основе правил или какой-либо конфигурации.

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

Обычно это итеративный подход: как требования, так и ваши дизайнерские идеи со временем будут меняться и развиваться. Тем не менее, на любом этапе вы должны хорошо понимать текущее состояние требований, а также их влияние на архитектурный дизайн.

Начните с рассмотрения индивидуальных требований и пометьте их ключевыми словами, указывающими на возможные идеи решения или конструктивные особенности, от которых может выиграть требование. Как указано в совете №1, я рекомендую начинать с абстрактных концепций и не вводить конкретные варианты технологий слишком рано. Вначале вы можете работать с концептуальными ключевыми словами, такими как база данных графиков, push-коммуникация в ion, механизм отчетов, многофункциональный интерфейс, ОС реального времени, gp u accelerat io n и т. д.

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

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

# 8 Определите сценарии атрибутов качества

Как упоминалось ранее, нефункциональные требования обычно относятся к одной из следующих категорий:

  • Технические ограничения
  • Атрибуты качества, такие как доступность, расширяемость или переносимость

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

Гораздо более интересным (и сложным) является анализ атрибутов качества.

Первая задача - правильно их записать. По моему опыту, атрибуты качества - это обычно требования с самой неоднозначной формулировкой. Во многих проектах они даже не записываются, а упоминаются устно. Если они записаны, вы наверняка найдете довольно расплывчатое перечисление, например:

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

Хотя значения атрибутов качества, таких как расширяемость или надежность, стандартизированы2, они, безусловно, оставляют много места для различных интерпретаций. В совете № 6 я уже упоминал, что неплохо обсудить объем атрибутов качества. Это может быть первым полезным шагом к лучшему пониманию идей и мотивов, стоящих за ними.

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

Сценарий атрибута качества в основном описывает измеримую реакцию системы на определенное взаимодействие (стимул). Он состоит из шести частей:

  • Источник стимула: объект (человек или система), который генерирует стимул.
  • Стимул: происходящее взаимодействие.
  • Окружающая среда: состояние, при котором возникает раздражитель. Например, система может быть в состоянии перегрузки.
  • Артефакт: получатель стимула. Это либо система, либо ее часть.
  • Ответ: действие, которое происходит после появления стимула.
  • Мера отклика: определяет измеримый результат сценария.

Приведенный выше шаблон позволяет легко описать конкретные ожидания по каждому атрибуту качества, сохраняя при этом простое и краткое описание. Давайте посмотрим на два примера сценария производительности и расширяемости атрибутов качества:

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

О конструкции компонентов…

# 10 Дизайн с разделением мыслей

Разделение строительных блоков системы друг от друга - один из самых ценных инструментов разработчиков программного обеспечения. Реализуя различные части системы как отдельные библиотеки классов («модули») или храня их в отдельных пространствах имен или пакетах, мы делаем их управляемыми. На уровне реализации мы получаем четкое представление о каждом строительном блоке и его интерфейсах. С организационной точки зрения разные люди или группы могут сосредоточиться на своих строительных блоках, не отвлекаясь от деталей реализации других частей системы.

Однако до тех пор, пока эти строительные блоки не спроектированы как отдельные процессы, они никогда не будут по-настоящему независимыми друг от друга. Как же так?

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

  • правильная конфигурация всех частей системы
  • выполнение зависимостей всех частей системы, будь то библиотеки или сторонние системы, такие как системы баз данных или брокеры сообщений
  • и фаза инициализации всех частей системы во время запуска

В противном случае в системе может произойти сбой в случайный момент еще до выполнения кода нашего разработчика. В небольших системах это может показаться тривиальной проблемой. Однако в любой системе среднего или большего размера поддержание требований к конфигурации и зависимости различных компонентов, как правило, является довольно сложной задачей.

Единственный способ полностью изолировать части системы друг от друга - спроектировать их для работы в разных процессах. В наши дни наиболее известным архитектурным стилем, использующим разделение процессов, может быть стиль микросервисной архитектуры. При использовании микросервисов мы разбиваем приложение на небольшие, слабосвязанные, независимо развертываемые процессы.

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

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

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

Пример.
Предположим, мы хотим создать настольное приложение для редактирования векторной графики.
При проектировании архитектуры для редактора векторной графики высока вероятность, что один из компонентов нашей системы будет называться «Растеризатор». Растеризатор превращает векторную графику, описываемую геометрическими фигурами, в растровое изображение, описываемое пикселями. По сути, он превращает файл векторной графики в файл растрового изображения. Растеризатор может быть хорошим кандидатом для развертывания в качестве процесса помимо самого графического редактора:

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

- Алгоритм сложен и может использовать некоторые очень специфические зависимости (например, библиотеки для работы с вводом-выводом растровых изображений), которые могут не иметь значения для ядра нашего редактора.

- Алгоритм растеризации может быть довольно сложным и наверняка ориентирован на решение одной конкретной задачи. Изоляция процесса приводит к менее загроможденной кодовой базе, а также дает возможность выбора реализации компонента с учетом конкретных проблем. Например, мы могли бы выбрать другой язык программирования, чтобы воспользоваться преимуществами производительности или лучшими библиотеками.

В первой части я уже упоминал о возможной начальной фазе проектирования, когда части системы идентифицируются как абстрактные компоненты (совет №1). После определения частей системы верхнего уровня, а также их приблизительной внутренней структуры, неплохо было бы подумать о возможном разделении процессов и оценить, где это может быть полезно. Как и в случае с любыми архитектурными соображениями, такая оценка, конечно, является итеративным процессом на этапе проектирования.

Также стоит отметить, что такое разложение системы может быть выполнено и позже. Если мы применим его к существующей системе, объем работы может быть больше, поскольку нам обычно приходится реорганизовывать отдельные части системы, чтобы они могли работать как отдельные процессы. С другой стороны, наши представления о зрелой существующей системе наверняка будут лучше, чем о любой другой системе на каком-либо раннем этапе проектирования. С этой точки зрения, разложение на более поздней стадии, скорее всего, будет казаться естественным и основано на реальном опыте работы с системой.

Заключительные слова

Это все, что касается второй части. Если у вас есть какие-либо вопросы или отзывы относительно приведенных выше советов, оставьте комментарий. Я хотел бы прочитать о вашем мнении или вашем собственном опыте. 😊

Если вам понадобится дополнительная консультация или личная помощь по вашему текущему проекту, не стесняйтесь связаться со мной. Вместе со своей командой в axio concept GmbH я помогаю компаниям разрабатывать и создавать программные архитектуры и программные продукты.

❤️ Подпишитесь на меня в Medium, Twitter или LinkedIn, чтобы получать уведомления о новых статьях.

- Марко

[1] Хороший обзор различных определений можно найти здесь: https://resources.sei.cmu.edu/library/asset-view.cfm?assetid=513807

[2] ISO 25010
Системная и программная инженерия - Требования и оценка качества систем и программного обеспечения (SQuaRE) - Модели качества систем и программного обеспечения
https://www.iso.org/standard/35733 .html