Что такое программное обеспечение модуля

Обновлено: 17.05.2024

Термин "модуль" (module) взят из статьи Modules vs. microservices. Так же для описания чего-то среднего между микросервисами и монолитами иногда используют термины "микролит" (microlith) или "моносервис" (monoservice). Но, не смотря на то, что термин "модуль" и так уже нагружен общеизвестным смыслом, на мой взгляд он подходит лучше других вариантов. Update: В комментарии lega использовал термин "встроенный микросервис" — он лучше описывает суть подхода, чем "модуль".

Монолит и микросервисы это очень разные подходы, поэтому в любой попытке взять лучшее от обоих критически важен баланс — что взять, а что нет. Иначе получится монстр вроде OSGi.

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

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

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

В отличие от микросервисов, модуль:

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

В отличие от обычных библиотек, модуль:

  • Не должен разделять с использующим его API кодом никаких общих данных — все данные должны передаваться через API либо в виде копии, либо, если данных очень много и к ним нужен доступ только на чтение, в виде неизменяемой (immutable) структуры данных (требует поддержки на уровне языка).
  • Не предоставляет функций, которые могут вызывать другие модули — т.е. не выполняет никакого своего кода в чужом потоке выполнения (нити, горутине, etc.), таким образом изолируя от вызывающего кода даже исключения, которые могут возникать в коде модуля.
    • У модуля есть публичная функция инициализации и запуска модуля, которая вызывается при запуске приложения, но она не предназначена для (повторного) вызова из других модулей.
    • Конфигурацию (передаваемую ему при запуске приложения).
      • Она может включать общие для всех модулей настройки логирования.
    • Хранилище данных (если оно ему нужно).
      • Поддержка версионирования схемы этих данных и миграций при обновлениях — так же ответственность модуля, хотя запуск миграций данных каждого модуля может быть частью общего для всех модулей процесса деплоя приложения.

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

    Есть константная добавленная сложность (accidental complexity), одинаковая в каждом микросервисе (регистрация/обнаружение сервисов, подключение и переподключение к ним, авторизация между сервисами, (де)маршалинг и шифрование трафика, использование прерывателей зацикленных запросов, реализация трассировки запросов, etc.). Есть аналогичная константная добавленная операционная сложность (необходимость автоматизации тестирования и выката, реализация детального мониторинга, агрегация логов, использование служебных сервисов для регистрации и поиска сервисов, для хранения конфигурации сервисов, для аудита, etc.). С этим можно смириться, потому что реализовать всё это можно один раз, по мере роста количества микросервисов эта сложность не растёт, а преимущества микросервисов с лихвой компенсируют эти затраты.

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

    • Необходимость разделять идемпотентные и не идемпотентные запросы.
    • Для всех запросов на клиенте:
      • Необходимость использовать таймауты (которые могут быть разными для разных запросов).
      • Необходимость использовать больше асинхронности для ускорения работы.
      • Больше ситуаций eventual consistency и вызванных этим сложностей.
        • В т.ч. необходимость иногда кешировать данные других сервисов.
      • Необходимость повторять некоторые запросы:
        • Используя ограничение по количеству или времени повторов.
        • Используя задержки между повторами.
      • Необходимость реализовать возможность определения дубликатов при повторе запросов.
      • Необходимость определять и обрабатывать дубликаты запросов (по-разному, в зависимости от бизнес-логики каждого запроса).

      Правильный модульный подход позволяет сохранить многие преимущества микросервисов (при наличии необходимой поддержки на уровне языка и/или инструментов разработки), но помимо потери ненужных в данном приложении возможностей масштабирования и высокой доступности есть и другие:

      • Падение модуля приводит к падению всего приложения.
      • Уменьшается скорость сборки и тестирования.
        • Не уверен, что скорость сборки это реальная проблема — есть много способов её ускорения.
        • В принципе возможно при тестировании ветки/PR запускать только тесты изменившегося модуля, а тесты всего проекта выполнять только перед деплоем — это в достаточной степени должно нивелировать эту проблему.
        • Здесь проблема не столько в реальном замедлении, сколько в том, что когда сервисов много время запуска "размазано" по нескольким сервисам, которые (пере)запускаются независимо друг от друга. Тем не менее, видимый эффект более медленного запуска этот факт не отменяет.

        Так же у модульного подхода появляются новые достоинства:

        • Увеличивается скорость взаимодействия между модулями-сервисами, что и позволяет значительно уменьшить необходимость в асинхронности, eventual consistency и кешировании.
        • Пока у модуля нет внешнего (сетевого) API его API намного проще изменять и деплоить эти изменения атомарно с деплоем использующих этот модуль клиентов (других модулей этого же приложения).
        • Совместную работу модулей проще тестировать, чем группу микросервисов.
        • Проще использовать монорепо (если хочется), хоть это и не обязательно.
          • Монорепо упрощает рефакторинг и изменение API, по крайней мере пока есть гарантия что у модуля нет сетевого интерфейса и внешних клиентов.

          Необходимая поддержка этого подхода в данный момент есть далеко не во всех языках, но в некоторых есть: автор статьи "Modules vs. microservices" писал о поддержке модульности в Java 9, в Go уже пару лет есть поддержка internal-пакетов, в Erlang судя по статье на эту же тему Dawn of the Microlith — Monoservices with Elixir всё хорошо, …. Я не уверен, насколько на скриптовых языках возможно обеспечить реальную изоляцию модулей, но попытки есть: micromono на NodeJS, в комментарии lega ссылка на подход для Python, …

          Если у вас есть соображения по теме (а ещё лучше — опыт реального проекта на похожих принципах) или дополнительные ссылки на статьи/проекты по теме — пишите в комментариях, я постараюсь дополнять ими статью.








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

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

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

          2. Основные характеристики программного модуля.

          Критерии приемлемости выделенного модуля (по Хольту):

          * хороший модуль снаружи проще, чем внутри;

          * хороший модуль проще использовать, чем построить.

          Критерии приемлемости выделенного модуля (по Майерсу

          * сцепление с другими модулями,

          * рутинность модуля (независимость от предыстории обращений к нему).

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

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

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

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

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

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

          * всегда следует использовать рутинный модуль, если это не приводит к плохим (не рекомендуемым) сцеплениям модулей;

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

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

          3. Методы разработки структуры программы.

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

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

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

          Функциональная спецификация модуля строится так же, как и функциональная спецификация ПС.

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

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

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





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

          Рис. 2.1. Шаг 1 формирования модульной структуры программы при конструктивном подходе.

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




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

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

          Читайте также: