Модульное программирование
До сих пор наши примеры были простыми автономными подпрограммами. Возможность расположить произвольные объявления в зоне описания способствует этому. Благодоря этой возможности мы смогли объявить наши типы и переменные в телах основных процедур.
Однако легко увидеть, что такой подход не вписывается в масштаб реальных приложений. Нам нужен лучший способ структурировать наши программы в модульные и отдельные блоки.
Ада поощряет разделение программ на несколько пакетов и подпакетов, предоставляя программисту множество инструментов в поисках идеальной организации кода.
Пакеты
Вот пример объявления пакета в Аде:
А вот как вы его используете:
Пакеты позволяют вам сделать код модульным, разбивая программы на семантически значимые единицы. Кроме того, отделение спецификации пакета от его тела (которое мы увидим ниже) может сократить время компиляции.
Хотя спецификатор контекста with
указывает зависимость,
в приведенном выше примере видно, что нам всё еще нужно использовать
префикс с менем пакета, чтобы сослаться на имя в этом пакете.
(Если бы мы также указали спецификатор использования
use Week
, то такой префикс уже не был бы необходим.)
При доступе к объектам из пакета используется точечная нотация A.B
,
аналогичная той, что используется для доступа к полям записей.
Спецификатор контекста with
может появляться только в начале
модуля компиляции (т. е. перед зарезервированным словом, таким как
procedure
, которое отмечает начало модуля).
В других местах он запрещен. Это правило необходимо
только по методологическим соображениям: человек, читающий ваш код,
должен сразу видеть, от каких модулей он зависит.
На других языках
Пакеты похожи на файлы заголовков в C / C ++, но семантически сильно отличаются от них.
Первое и самое важное отличие состоит в том, что пакеты представляют собой механизм уровня языка. В то время, как заголовочный файл из
#include
обрабатывается препроцессором C.Непосредственным следствием этого является то, что конструкция
with
работает на семантическом уровне, а не с помощью подстановки текста. Следовательно, когда вы работаете с пакетом указываяwith
, вы говорите компилятору: «Я зависим от этой семантической единицы», а не «включите сюда эту кучу текста».Таким образом, действие пакета не зависит от того, откуда на него ссылается
with
. Сравните это с C/C++, где смысл включенного текста может меняться в зависимости от контекста, в котором появляется#include
.Это позволяет повысить эффективность компиляции/перекомпиляции. Это также облегчает инструментам, таким как IDE, получать правильную информацию о семантике программы. Что, в свою очередь, позволяет иметь лучший инструментарий в целом и код, который легче поддается анализу даже людьми.
Важным преимуществом with
в Аде по сравнению с #include
является то, что он не имеет состояния.
Порядок спецификаторов with
и use
не имеет значения и может
быть изменен без побочных эффектов.
В наборе инструментов GNAT
Стандарт языка Ада не предусматривает каких-либо особых отношений
между исходными файлами и пакетами; например, теоретически вы можете
поместить весь свой код в один файл или использовать свои собственные
соглашения об именовании файлов. На практике, однако, каждая реализация
имеет свои правила. Для GNAT каждый модуль компиляции
верхнего уровня должен быть помещен в отдельный файл. В приведенном
выше примере пакет Week
будет находиться в файле .ads
(для Ада спецификации), а основная процедура Main
- в файле
.adb
(для Ада тела).
Использование пакета
Как мы видели выше, спецификатор контекста with
указывает на зависимость
от другого пакета. Тем не менее, каждая ссылка на сущъность, находящуюся
в пакете Week
, должна иметь префикс в виде полного имени пакета.
Можно сделать все сущъности пакета непосредственно видимым в текущей области
с помощью спецификатора использования use
.
Фактически, мы использовали спецификатор use
почти с самого начала этого
руководства.
Как вы можете видеть в приведенном выше примере:
Put_Line
- это подпрограмма из пакетаAda.Text_IO
. Мы можем ссылаться на нее непосредственно, потому что мы указали пакет вuse
в верхней части основного модуляMain
.В отличие от спецификатора контекста
with
, спецификатор использованияuse
может быть помещен либо в заголовок, либо в любую область описания. В последнем случае эффектuse
будет распространяться только на соответствующую область видимости.
Тело пакета
В приведенном выше простом примере пакет Week
содержит только объявления и
не содержит реализаций. Это не ошибка: в спецификации пакета,
которая проиллюстрирована выше, нельзя объявлять реализации.
Реализации должны находиться в теле пакета.
Здесь мы видим, что тело функции Increment_By
должно быть объявлено
в теле. Пользуясь появлением тела можно поместить переменную
Last_Increment
в тело, и сделать ее недоступной для пользователя пакета
Operations
, обеспечив первую форму инкапсуляции.
Это работает, поскольку объекты, объявленные в теле, видимы только в теле.
В этом примере показано, как непосредственно использовать Increment_By
:
Дочерние пакеты
Пакеты можно использовать для создания иерархий. Мы достигаем этого с
помощью дочерних пакетов, которые расширяют функциональность
родительского пакета. Одним из примеров дочернего пакета, который мы
использовали до сих пор, является пакет Ada.Text_IO
. Здесь родительский пакет
называется Ada
, а дочерний пакет называется Text_IO
. В предыдущих примерах мы
использовали процедуру Put_Line
из дочернего пакета Text_IO
.
Важное замечание
Ада также поддерживает вложенные пакеты. Однако, поскольку их использование может быть более сложным, рекомендуется использовать дочерние пакеты. Вложенные пакеты будут рассмотрены в расширенном курсе.
Давайте начнем обсуждение дочерних пакетов с нашего предыдущего пакета
Week
:
Если мы хотим создать дочерний пакет для Week
, мы можем написать:
Здесь Week
- это родительский пакет, а Child
- дочерний. Это соответствующее
тело пакета Week.Child
:
В реализации функции Get_First_Of_Week
мы можем использовать строку
Mon
непосредственно, хотя она объявлена в родительском пакете
Week
. Мы не пишем здесь with Week
, потому что все элементы из
спецификации пакета Week
, такие как Mon
, Tue
и т. д.,
видны в дочернем пакете Week.Child
.
Теперь, когда мы завершили реализацию пакета Week.Child
, мы можем использовать
элементы из этого дочернего пакета в подпрограмме, просто написав with Week.Child
.
Точно так же, если мы хотим использовать эти элементы непосредственно, мы
дополнительно напишем use Week.Child
. Например:
Дочерний пакет от дочернего пакета
До сих пор мы видели двухуровневую иерархию пакетов. Но иерархия,
которую мы потенциально можем создать, этим не ограничивается.
Например, мы могли бы расширить иерархию предыдущего примера исходного
кода, объявив пакет Week.Child.Grandchild
. В этом случае
Week.Child
будет родительским для пакета Grandchild
.
Рассмотрим эту реализацию:
Мы можем использовать этот новый пакет Grandchild
в нашем тестовом приложении
так же, как и раньше: мы можем повторно использовать предыдущее
тестовое приложение адаптировав with
, use
и вызов функции.
Вот обновленный код:
Опять же, это не предел иерархии пакетов. Мы могли бы продолжить
расширение иерархии предыдущего примера, реализовав пакет
Week.Child.Grandchild.Grand_grandchild
.
Множественные потомки
До сих пор мы видели лишь один дочерний пакет родительского пакета. Однако
родительский пакет также может иметь несколько дочерних. Мы
могли бы расширить приведенный выше пример и создать пакет Week.Child_2
.
Например:
Здесь Week
по-прежнему является родительским пакетом для пакета Child
, но
также родительским пакетом и для пакета Child_2
. Таким же образом, Child_2
,
очевидно, является одним из дочерних пакетов Week
.
Это соответствующее тело пакета Week.Child_2
:
Теперь мы можем сослаться на оба потомка в нашем тестовом приложении:
Видимость
В предыдущем разделе мы видели, что элементы, объявленные в спецификации родительского пакета, видны в дочернем пакете. Однако это не относится к элементам, объявленным в теле родительского пакета.
Рассмотрим пакет Book
и его дочерний элемент Additional_Operations
:
Это тела обоих пакетов:
В реализации Get_Extended_Title
мы используем константу Title
из родительского пакета Book
.
Однако, как указано в комментариях к функции Get_Extended_Author
, строка Author
, которую мы
объявили в теле пакета Book
, не отображается в пакете Book.Additional_Operations
. Следовательно, мы
не можем использовать его для реализации функции Get_Extended_Author
.
Однако мы можем использовать функцию Get_Author
из Book
в реализации функции Get_Extended_Author
для
получения этой строки. Точно так же мы можем использовать эту
стратегию для реализации функции Get_Extended_Title
. Это адаптированный код:
Вот простое тестовое приложение для указанных выше пакетов:
Объявляя элементы в теле пакета, мы можем реализовать инкапсуляцию в языке Ада. Эти элементы будут видимы только в теле пакета, но нигде больше. Но это не единственный способ добиться инкапсуляции в Аде: мы обсудим другие подходы в главе Изоляция.
Переименование
Ранее мы упоминали, что
подпрограммы можно переименовывать.
Мы также можем переименовывать пакеты. Опять же, для этого мы используем
ключевое слово renames
. В следующем примере пакет Ada.Text_IO
переименовывается как TIO
:
Мы можем использовать переименование, чтобы улучшить читаемость нашего
кода, используя более короткие имена пакетов. В приведенном выше
примере мы пишем TIO.Put_Line
вместо более длинного имени
(Ada.Text_IO.Put_Line
). Этот подход особенно
полезен, когда мы не используем спецификатор использования use
,
но хотим, чтобы код не становился слишком многословным.
Обратите внимание, что мы также можем переименовывать подпрограммы и
объекты внутри пакетов. Например, мы могли бы просто переименовать
процедуру Put_Line
в приведенном выше примере исходного кода: