Вы знаете из чего и как строятся программы? Странно что ни в одной из статей о программной архитектуре вы не найдете упоминаний о том из чего эти программы строятся.

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

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

Как вы наверно знаете, под сборкой (построением) софтовых проектов в первую очередь подразумевается компиляция и линковка, хотя, если вы спросите в интернете что такое архитектура программного обеспечения (Software architecture, например https://en.wikipedia.org/wiki/Software_architecture) то вы найдете там очень много умных слов, которые, в общем то, сводятся к тому что архитектура это структура или, еще хуже, что архитектура это метафора и аналогия к архитектуре в реальном (каменном) строительстве:

The architecture of a software system is a metaphor, analogous to the architecture of a building. И все!

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

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

Что значит в порядке?

Перевод:

Сравнение между разработкой программного обеспечения и (гражданской) архитектурой впервые было проведено в конце 1960-х годов[23], но термин "архитектура программного обеспечения" не получил широкого распространения до 1990-х годов.[24] Область компьютерных наук с момента своего образования сталкивалась с проблемами, связанными со сложностью.[25] Более ранние проблемы сложности решались разработчиками путем выбора правильных структур данных, разработки алгоритмов и применения концепции разделения задач. Хотя термин "архитектура программного обеспечения" является относительно новым для отрасли, фундаментальные принципы этой области время от времени применялись пионерами разработки программного обеспечения с середины 1980-х годов.

Оригинал:

The comparison between software design and (civil) architecture was first drawn in the late 1960s,[23] but the term "software architecture" did not see widespread usage until the 1990s.[24] The field of computer science had encountered problems associated with complexity since its formation.[25] Earlier problems of complexity were solved by developers by choosing the right data structures, developing algorithms, and by applying the concept of separation of concerns. Although the term "software architecture" is relatively new to the industry, the fundamental principles of the field have been applied sporadically by software engineering pioneers since the mid-1980s. Early attempts to capture and explain software architecture of a system were imprecise and disorganized, often characterized by a set of box-and-line diagrams.[26]

Software architecture as a concept has its origins in the research of Edsger Dijkstra in 1968 and David Parnas in the early 1970s. These scientists emphasized that the structure of a software system matters and getting the structure right is critical. During the 1990s there was a concerted effort to define and codify fundamental aspects of the discipline, with research work concentrating on architectural styles (patterns), architecture description languages, architecture documentation, and formal methods.[27]

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

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

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

Архитектура является не единственным термином который настраивает нас на аналогию с каменным строительством. Наверно еще более широко используемый термин применительно к большим софтовым системам — это термин "The build" или просто "build" — построение, и как наименование для результата, и как наименование для действия (для выполнения построения).

"The build" can contain many things:

  • Compilation of source files (for languages/environments that support a separate/explicit compilation step)

  • Linking of object code (for languages/environments that support a separate/explicit linking step)

  • Production of distribution packages, also called "installers"

  • Generation of documentation that is embedded within the source code files, e.g. Doxygen, Javadoc

  • Execution of automated tests like unit tests, static analysis tests, and performance tests

  • Generation of reports that tell the development team how many warnings and errors occurred during the build

  • Deployment of distribution packages. For example, the build could automatically deploy/publish a new version of a web application (assuming that the build is successful).

На самом деле для современных больших проектов первые два пункта заметно разветвились, теперь это никогда не обходится просто:

  • компиляцией исходных файлов и

  • линковкой объектных файлов.

Для примера я могу перечислить вам как это происходит во многих известных мне проектах:

  1. Загрузка упакованных файлов из сетевых (онлайн) репозиториев на целевой сервер (локально)

  2. Распаковка пакетов в целевую файловую систему

  3. Применение Патчей

  4. Компиляция инструментальных приложений

  5. Линковка инструментальных приложений (+ развертывание в нужной локации)

  6. Вспомогательная генерация кода и специальных файлов с данными

  7. Основная компиляция

  8. Линковка для каждой библиотеки и исполняемого файла в проекте.

  9. разнообразные проверки (вплоть до пробной компиляции) на каждом из этапов

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

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

Проблема в том что компиляции всегда подлежат множество файлов. В терминах языка Си, с которого все началось (по крайней мере у меня, и в смысле компиляции), отдельные файлы с исходным кодом назывались модулями и процесс построения результирующей программы из таких модулей, в некотором смысле, вполне подходит под аналогию с процессом строительства из физических блоков (кирпичиков, панелей, ...). Собственно, необходимость разделения исходного кода больших программ разработчики осознали уже при зарождении языков программирования и естественным выбором, в качестве единицы локализации таких кусков исходного кода, оказались файлы, на которые делится софтовый проект. С современными софтовыми проектами все гораздо сложнее. Зачастую, современные проекты содержат файлы разного типа, плюс разные инструментальные программы, которые участвуют, например, в генерации файлов с исходным кодом, который в конечном итоге компилируются и линкуются. Таким образом процесс постройки современного проекта стал в среднем значительно сложнее, чем это было скажем в прошлом веке (каких-то 25 лет назад, 25 лет назад прошивки для смартфонов строились на серверах иногда несколько дней), не говоря о том, что сами по себе проекты разрослись до гигабайтных размеров, по крайней мере гигабайтные гит-репозитории уже давно ни у кого не вызывают изумления.

Но давайте для начала разберемся с самыми простыми единицами строительства программ.

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

Таким образом можно придти к заключению что архитектура ПО ставит перед нами нетривиальные вопросы:

  1. определения архитектурных единиц, на которое мы будем делить наше полное решение

  2. способ и критерии по которым исходный код относится к той или иной архитектурной единице

Здесь, кстати, будет вполне уместным перечислить общеизвестные архитектурные единицы которые применяются используются в разных языках. На вскидку это:

файлы, классы, функции (методы, процедуры), namespace, библиотеки динамические и статические, подпроекты клиентов, подпроекты серверов …

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

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

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

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

В самом лучшем случае приложения клиента и сервера будут компилироваться не зависимо, то есть изменения, например, в клиенте не должны влиять на код сервера и соответственно сервер не должен пере компилироваться, а значит не должен по новой развертываться и ни должен проходить никакие процедуры CI/CD! Многие сразу начнут возмущенно возражать что это невозможно, и отчасти будут правы так как, если клиент и сервер взаимодействуют друг с другом, они зависят друг от друга и при компиляции, но только в той части в которой они принимают и получают вызовы-запросы и данные друг от друга. Соответственно, и с той и с другой стороны есть код который, для примера, визуализирует эти данные на стороне клиента и, скажем, обеспечивает доступ к данным по запросам от клиента в некое хранилище этих данных на стороне сервера и который можно рассматривать в какой-то степени отдельно от кода который обеспечивает, собственно, взаимодействие сервера и клиентов.

Как может строиться архитектура такой системы? Мы должны отправлять и принимать данные одних и тех же типов и со стороны клиента и со стороны сервера. Сервер будет эти данные или принимать и сохранять или читать и отправлять, а клиент принимать и отображать или читать с пользовательского ввода и отправлять серверу.

Тут есть одна проблема которая не раскрыта: например «принимать и отображать» данные обычно не получается напрямую, принятые данные обычно приходится преобразовать в другой тип данных, который поддерживает отображение (просто более удобный для отображения), но представляет исходные сырые данные, этакий тип данных для представления. То есть очевидно что код, например клиента, который просто занимается визуализацией данных будет иметь в своем составе довольно существенную часть, которая или вообще не будет зависеть от кода взаимодействия с сервером либо будет слабо зависеть, например, будет зависеть только от типов данных, которые мы получаем с сервера. Мы должны идентифицировать такие слабые зависимости и следить чтобы они не спутались и не стали сложными!

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

Простая идея состоит в том чтобы сделать модуль принимающий и отправляющий данные универсальным, чтобы один и тот же модуль мог использоваться и на стороне клиента и на стороне сервера, но с этим есть проблема, это очень не просто сделать с точки зрения компиляции и линковки, которые обычно применяются при сборке приложений! Но сделать это все таки можно, вопрос в том: как, все таки, это сделать? Для этого существуют библиотеки, в этом случае особо не важно будет наша библиотека динамически или статически линкуемой, это влияет только на некоторую специфику с кодом обращения к библиотеке и некоторых возможностей по конфигурации и по работе с такой библиотекой в составе проекта. У динамической линковки больше возможностей, но при этом она (понятно) сложнее, знать про нее надо больше, чтобы даже просто скомпилировать проект, не говоря уже о том чтобы он работал в ран-тайме. Поэтому лучше начинать со статической библиотеки, там по крайней мере есть максимальная вероятность, что если проект у вас скомпилировался, он и в ран-тайме будет работать.

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

Рассмотрим самую простую архитектурную задачу когда у нас есть сервер, обеспечивающий доступ к хранилищу данных и клиент (один из многих) который занимается визуализацией-редактированием этих данных. Я привел некоторое описание функциональности, которую наш абсолютно гипотетический проект должен обеспечить, исключительно для того чтобы задача не казалась совсем абстрактной. Если у нас ПО клиента (клиентов) разворачиваются скажем на десктопах, а ПО серверной части запускается на специальной-особенной машине с навороченной серверной архитектурой и специальной операционной системой, и, соответственно, компиляция ПО сервера и клиентов осуществляется разными компиляторами (тулзами компиляции) можно нарисовать, например, такую схему построения и отношений между действующими единицами Программного Обеспечения и соответствующими исходниками:

Компиляция для разных аппаратных платформ
Компиляция для разных аппаратных платформ

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

Компиляция для подобных аппаратных платформ или внутри одной аппаратной платформы
Компиляция для подобных аппаратных платформ или внутри одной аппаратной платформы

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

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

Комментарии (7)


  1. igorb789
    27.07.2025 04:27

    не в плане критики, но и не в качестве рекламы -- несколько, на мой взг., весьма неплохих книг по теме: Марк Ричардс и Нил Форд "Фундаментальный подход к программной архитектуре", их же (и ещё 2-ва соавтора) "Современный подход к программной архитектуре" и "Эволюционная архитектура", Нил Форд, Ребекка Парсонс и др.


  1. olku
    27.07.2025 04:27

    Так и не понял из чего всё таки строить. Будущим архитекторам на заметку https://www.isaqb.org/download/curriculum-foundation-level/?wpdmdl=3892


    1. BobovorTheCommentBeast
      27.07.2025 04:27

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

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

      Думал статья будет об этом.


      1. olku
        27.07.2025 04:27

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


        1. BobovorTheCommentBeast
          27.07.2025 04:27

          Да, только под этим обычно идут все те же паттерны или абстрактные принципы и методики.

          Нет мммм базовых строительных блоков. Не знаю как конкретно это описать.


    1. rukhi7 Автор
      27.07.2025 04:27

      я вроде как, частично, угадал с перечислением:

      Software architects know and understand ...

      • components/building blocks with interfaces and relationships

      • ...

      Some examples of alternative (concrete) names for building blocks:Component, module, package, namespace, class, file, program, subsystem, function, configuration, data-definition.

      но мне особенно приятно, что я угадал что надо именно перечислить, этого видимо достаточно :) !


      1. olku
        27.07.2025 04:27

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