Дочерние пакеты и инициализация
Дочерние пакеты расширяют родителя, не трогая его исходник, и получают законный доступ к его приватной части — это масштабируемая модульность.
Дочерний пакет (child package) — пакет с составным именем вида
Parent.Child, логически принадлежащий родителю: он расширяет родительский интерфейс новыми возможностями и (в отличие от посторонних модулей) имеет доступ к приватной части родителя.
Большой модуль рано или поздно разрастается. Хочется добавить к нему новые операции, утилиты ввода-вывода, отладочные средства — но не раздувать исходный файл и не нарушать уже существующих клиентов. Дочерние пакеты, появившиеся в Ada 95, решают именно это: они позволяют наращивать функциональность «снаружи», добавляя новые единицы компиляции, при этом не редактируя и не перекомпилируя ядро. Это иерархическая декомпозиция, встроенная в систему имён.
Зачем нужны дочерние пакеты
Рассмотрим, что было бы без них. Чтобы добавить к пакету Stacks функцию печати содержимого, пришлось бы либо вписать её прямо в Stacks (раздувая интерфейс и перекомпилируя всех клиентов), либо вынести в посторонний пакет — но тогда нет доступа к приватному представлению Stack, и печать полей невозможна без публичных «геттеров». Дочерний пакет снимает дилемму: Stacks.Debug — это формально новый модуль (отдельные .ads/.adb, отдельная компиляция), но он видит приватную часть родителя Stacks, как если бы был его частью.
-- stacks-debug.ads — дочерний пакет (имя файла с дефисом в GNAT)
with Ada.Text_IO;
package Stacks.Debug is
procedure Dump (S : Stacks.Stack); -- печатает ВНУТРЕННОСТИ стека
end Stacks.Debug;
-- stacks-debug.adb
package body Stacks.Debug is
procedure Dump (S : Stacks.Stack) is
begin
-- Здесь доступны S.Top и S.Data — приватная часть родителя видна!
Ada.Text_IO.Put_Line ("Top =" & S.Top'Image);
for I in 1 .. Integer (S.Top) loop
Ada.Text_IO.Put_Line (" [" & I'Image & " ] =" & S.Data (I)'Image);
end loop;
end Dump;
end Stacks.Debug;
Ключевой момент: S.Top и S.Data доступны внутри тела дочернего пакета, хотя для любого постороннего модуля они скрыты. Дочерний пакет — «свой» по отношению к приватной части родителя. Это сознательное послабление: расширять абстракцию должны те, кто имеет на это право, и иерархия имён задаёт это право явно.
Иерархии и их применение
Дочерние пакеты образуют деревья произвольной глубины: Parent.Child.Grandchild. Так строятся целые библиотеки. Сама стандартная библиотека Ada организована именно так: Ada — корень, Ada.Text_IO — ввод-вывод, Ada.Strings.Unbounded — строки переменной длины, Ada.Containers.Vectors — контейнеры. Иерархия имён отражает иерархию понятий, и подключить можно ровно нужную ветку: with Ada.Containers.Vectors; не тянет за собой все контейнеры.
Отдельный приём — приватные дочерние пакеты (private package Parent.Internal). Такой пакет виден только другим потомкам того же родителя, но не внешнему миру: идеально для внутренней «кухни» библиотеки, которую не хочется показывать пользователям, но которую нужно разделять между несколькими дочерними модулями. Это даёт ещё один уровень управления видимостью поверх private-типов.
with Stacks;
with Stacks.Debug;
procedure Demo is
S : Stacks.Stack;
begin
Stacks.Push (S, 7);
Stacks.Push (S, 8);
Stacks.Debug.Dump (S); -- использует и родителя, и потомка
end Demo;
Порядок инициализации (elaboration)
У пакетов есть жизненный цикл до main: их объявления нужно «оживить» (англ. elaboration) — выполнить инициализаторы переменных, отработать необязательную секцию инициализации тела пакета (begin ... end в конце package body). Порядок этого оживления критичен: если пакет A в своей инициализации вызывает подпрограмму пакета B, то B обязан быть проинициализирован раньше A, иначе вызов попадёт в ещё не готовый код.
package body Config is
Settings : Integer;
begin
-- Секция инициализации тела: выполняется при elaboration пакета,
-- ДО первого обращения клиента и до main.
Settings := Load_From_Defaults;
Ada.Text_IO.Put_Line ("Config elaborated");
end Config;
Ada относится к этому максимально серьёзно. Язык предоставляет директивы-прагмы (pragma Elaborate_Body, pragma Elaborate_All), которыми разработчик объявляет требования к порядку, а компилятор и компоновщик выстраивают корректную последовательность или сообщают о неразрешимой циклической зависимости. Более того, существует статический режим контроля elaboration, в котором потенциально опасные «преждевременные» вызовы отлавливаются ещё на этапе сборки, а не превращаются в Program_Error во время выполнения. Для систем, где сбой недопустим, это бесценно: проблема порядка инициализации, печально знаменитая в C++ как «static initialization order fiasco», в Ada переведена из разряда «отладочных кошмаров» в разряд «ошибок компоновки».
Как работает под капотом
Компоновщик строит граф зависимостей elaboration: ребро A → B означает «A зависит от готовности B». Если граф ацикличен, выбирается топологический порядок, и сгенерированный код вызывает процедуры elaboration пакетов строго в нём перед входом в main. Дочерний пакет в этом графе всегда оживляется после своего родителя — это естественно, ведь он видит приватную часть родителя и может на неё опираться. Если же требования противоречивы (цикл, который нельзя удовлетворить), сборка завершится ошибкой, и вы исправите архитектуру до запуска, а не после инцидента.
Дочерние пакеты как инструмент эволюции системы
Отдельного разговора заслуживает то, как дочерние пакеты помогают системам стареть достойно. Программное обеспечение, особенно в долгоживущих областях (авионика эксплуатируется десятилетиями), постоянно дополняется: новые требования, новые режимы, новая диагностика. Самый разрушительный способ добавлять функциональность — править существующие, уже проверенные и, возможно, сертифицированные модули: каждая такая правка ставит под сомнение всё, что было верифицировано ранее. Дочерние пакеты дают альтернативу: новую возможность добавляют снаружи, отдельным потомком, не трогая исходный текст родителя. Сертифицированное ядро остаётся неизменным и не требует повторной верификации, а расширение проходит проверку как новый, изолированный модуль.
Эта модель «расширяй, не изменяя» — практическое воплощение принципа открытости/закрытости (открыт для расширения, закрыт для изменения) задолго до того, как его сформулировали в объектно-ориентированном мире. Причём в Ada она работает не только для типов (через наследование, о котором речь в финальном разделе), но и для целых модулей. Иерархия дочерних пакетов позволяет выстраивать «слои» функциональности: базовый слой — стабильное ядро, над ним — дополнительные потомки для конкретных платформ, конфигураций, заказчиков. Один и тот же родитель служит основой нескольких ветвей, каждая из которых добавляет своё, не мешая остальным.
Видимость как проектное решение
Наконец, сочетание дочерних пакетов с приватными типами и приватными потомками даёт инженеру тонкую палитру управления видимостью — куда богаче, чем простое «public/private» большинства языков. Можно сделать тип приватным для внешнего мира, но раскрытым для семейства потомков (через приватную часть, которую потомки видят). Можно создать приватный дочерний пакет, доступный только другим потомкам того же родителя, для общей внутренней «кухни» библиотеки. Эти средства позволяют выразить намерение архитектора с большой точностью: что является публичным контрактом, что — внутренним соглашением между частями одной подсистемы, а что — скрыто полностью. В крупных системах такая точность управления видимостью прямо влияет на сопровождаемость: чем уже и осмысленнее интерфейсы, тем меньше неожиданных зависимостей и тем безопаснее эволюция.
Управление инициализацией в больших системах
В крупных программах граф зависимостей elaboration становится нетривиальным, и Ada даёт средства управлять им осознанно. Прагма pragma Elaborate_All (Some_Package); требует, чтобы перед оживлением текущего модуля были полностью оживлены указанный пакет и всё, от чего он зависит, — это гарантирует готовность всей цепочки. Прагма pragma Preelaborate и более строгая pragma Pure идут с другого конца: они декларируют, что пакет не выполняет нетривиальной инициализации (не имеет состояния, требующего оживления в рантайме), и компилятор это проверяет. Пакеты Pure и Preelaborate можно безопасно использовать из любого контекста, не опасаясь проблем порядка, — поэтому их активно применяют для базовых, фундаментальных модулей.
Практический совет, выработанный сообществом: проектировать пакеты так, чтобы избегать сложной инициализации с взаимными зависимостями вообще. Чем меньше пакет делает в момент оживления (в идеале — ничего, кроме простых присваиваний значений по умолчанию), тем проще граф elaboration и тем меньше шансов на циклическую зависимость. Состояние, требующее настоящей настройки, часто лучше инициализировать явно вызовом процедуры Initialize из main, а не в секции begin тела пакета — это делает порядок видимым и подконтрольным программисту. Сочетание статического контроля elaboration и дисциплины «минимальной инициализации» окончательно изгоняет из программ на Ada тот класс ошибок порядка запуска, что в C++ известен как «фиаско порядка статической инициализации».
Завершая раздел о пакетах, окинем взглядом его сквозную идею. Пакеты, приватные типы, ограниченные типы, дочерние пакеты и контроль elaboration — это не пять разрозненных средств, а единая система управления сложностью и зависимостями. Пакет проводит границу между интерфейсом и реализацией; приватные типы делают эту границу непроницаемой для представления; ограниченные типы добавляют контроль над копированием; дочерние пакеты позволяют расширять абстракции, не ломая их; контроль elaboration упорядочивает запуск. Вместе они дают инженеру точные инструменты, чтобы строить большие системы из независимо понятных, проверяемых на стыках, безопасно эволюционирующих компонентов. Именно эта продуманность модульности — а не отдельные синтаксические находки — сделала Ada образцом инженерного языка и причиной, по которой её механизмы десятилетиями переоткрываются в новых языках. Усвоив их, вы получаете не набор приёмов, а целостный способ мыслить архитектуру надёжного ПО.
Частые ошибки
- Путать дочерний пакет с обычным
with. Посторонний пакет, даже подключённый черезwith, НЕ видит приватной части. Доступ к приватному представлению родителя даёт именно отношение «родитель–потомок». - Имя файла без дефиса. В GNAT дочерний пакет
Stacks.Debugлежит в файлахstacks-debug.ads/.adb(точка имени → дефис в имени файла). Неверное имя — и сборщик не найдёт модуль. - Игнорировать порядок elaboration. Вызов подпрограммы другого пакета прямо в инициализации без гарантии его готовности может дать
Program_Error; используйтеpragma Elaborate_Allили статический режим контроля. - Складывать всё в один гигантский пакет. Когда модуль растёт, выносите расширения в дочерние пакеты: клиенты подключают только нужные ветки, а ядро остаётся стабильным.
Итоги
- Дочерний пакет
Parent.Childрасширяет родителя новыми единицами компиляции, не редактируя его исходник. - В отличие от посторонних модулей, потомок видит приватную часть родителя — это законный, иерархически заданный доступ.
- Иерархии имён строят целые библиотеки (так устроена и стандартная библиотека
Ada.*); приватные дочерние пакеты прячут внутреннюю «кухню». - Elaboration — оживление пакетов до
main; порядок критичен и управляется прагмами. - Статический контроль elaboration переводит «фиаско порядка инициализации» из runtime-кошмара в ошибку компоновки.