Модульное программирование

До сих пор наши примеры были простыми автономными подпрограммами. Возможность расположить произвольные объявления в зоне описания способствует этому. Благодоря этой возможности мы смогли объявить наши типы и переменные в телах основных процедур.

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

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

Пакеты

Вот пример объявления пакета в Аде:

package Week is Mon : constant String := "Monday"; Tue : constant String := "Tuesday"; Wed : constant String := "Wednesday"; Thu : constant String := "Thursday"; Fri : constant String := "Friday"; Sat : constant String := "Saturday"; Sun : constant String := "Sunday"; end Week;

А вот как вы его используете:

with Ada.Text_IO; use Ada.Text_IO; with Week; -- References the Week package, and -- adds a dependency from Main to Week procedure Main is begin Put_Line ("First day of the week is " & Week.Mon); end Main;

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

Хотя спецификатор контекста 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 почти с самого начала этого руководства.

with Ada.Text_IO; use Ada.Text_IO; -- ^ Make every entity of the -- Ada.Text_IO package -- directly visible. with Week; procedure Main is use Week; -- Make every entity of the Week -- package directly visible. begin Put_Line ("First day of the week is " & Mon); end Main;

Как вы можете видеть в приведенном выше примере:

  • Put_Line - это подпрограмма из пакета Ada.Text_IO. Мы можем ссылаться на нее непосредственно, потому что мы указали пакет в use в верхней части основного модуля Main.

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

Тело пакета

В приведенном выше простом примере пакет Week содержит только объявления и не содержит реализаций. Это не ошибка: в спецификации пакета, которая проиллюстрирована выше, нельзя объявлять реализации. Реализации должны находиться в теле пакета.

package Operations is -- Declaration function Increment_By (I : Integer; Incr : Integer := 0) return Integer; function Get_Increment_Value return Integer; end Operations;
package body Operations is Last_Increment : Integer := 1; function Increment_By (I : Integer; Incr : Integer := 0) return Integer is begin if Incr /= 0 then Last_Increment := Incr; end if; return I + Last_Increment; end Increment_By; function Get_Increment_Value return Integer is begin return Last_Increment; end Get_Increment_Value; end Operations;

Здесь мы видим, что тело функции Increment_By должно быть объявлено в теле. Пользуясь появлением тела можно поместить переменную Last_Increment в тело, и сделать ее недоступной для пользователя пакета Operations, обеспечив первую форму инкапсуляции.

Это работает, поскольку объекты, объявленные в теле, видимы только в теле.

В этом примере показано, как непосредственно использовать Increment_By:

with Ada.Text_IO; use Ada.Text_IO; with Operations; procedure Main is use Operations; I : Integer := 0; R : Integer; procedure Display_Update_Values is Incr : constant Integer := Get_Increment_Value; begin Put_Line (Integer'Image (I) & " incremented by " & Integer'Image (Incr) & " is " & Integer'Image (R)); I := R; end Display_Update_Values; begin R := Increment_By (I); Display_Update_Values; R := Increment_By (I); Display_Update_Values; R := Increment_By (I, 5); Display_Update_Values; R := Increment_By (I); Display_Update_Values; R := Increment_By (I, 10); Display_Update_Values; R := Increment_By (I); Display_Update_Values; end Main;

Дочерние пакеты

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

Важное замечание

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

Давайте начнем обсуждение дочерних пакетов с нашего предыдущего пакета Week:

package Week is Mon : constant String := "Monday"; Tue : constant String := "Tuesday"; Wed : constant String := "Wednesday"; Thu : constant String := "Thursday"; Fri : constant String := "Friday"; Sat : constant String := "Saturday"; Sun : constant String := "Sunday"; end Week;

Если мы хотим создать дочерний пакет для Week, мы можем написать:

package Week.Child is function Get_First_Of_Week return String; end Week.Child;

Здесь Week - это родительский пакет, а Child - дочерний. Это соответствующее тело пакета Week.Child:

package body Week.Child is function Get_First_Of_Week return String is begin return Mon; end Get_First_Of_Week; end Week.Child;

В реализации функции Get_First_Of_Week мы можем использовать строку Mon непосредственно, хотя она объявлена в родительском пакете Week. Мы не пишем здесь with Week, потому что все элементы из спецификации пакета Week, такие как Mon, Tue и т. д., видны в дочернем пакете Week.Child.

Теперь, когда мы завершили реализацию пакета Week.Child, мы можем использовать элементы из этого дочернего пакета в подпрограмме, просто написав with Week.Child. Точно так же, если мы хотим использовать эти элементы непосредственно, мы дополнительно напишем use Week.Child. Например:

with Ada.Text_IO; use Ada.Text_IO; with Week.Child; use Week.Child; procedure Main is begin Put_Line ("First day of the week is " & Get_First_Of_Week); end Main;

Дочерний пакет от дочернего пакета

До сих пор мы видели двухуровневую иерархию пакетов. Но иерархия, которую мы потенциально можем создать, этим не ограничивается. Например, мы могли бы расширить иерархию предыдущего примера исходного кода, объявив пакет Week.Child.Grandchild. В этом случае Week.Child будет родительским для пакета Grandchild. Рассмотрим эту реализацию:

package Week.Child.Grandchild is function Get_Second_Of_Week return String; end Week.Child.Grandchild;
package body Week.Child.Grandchild is function Get_Second_Of_Week return String is begin return Tue; end Get_Second_Of_Week; end Week.Child.Grandchild;

Мы можем использовать этот новый пакет Grandchild в нашем тестовом приложении так же, как и раньше: мы можем повторно использовать предыдущее тестовое приложение адаптировав with, use и вызов функции. Вот обновленный код:

with Ada.Text_IO; use Ada.Text_IO; with Week.Child.Grandchild; use Week.Child.Grandchild; procedure Main is begin Put_Line ("Second day of the week is " & Get_Second_Of_Week); end Main;

Опять же, это не предел иерархии пакетов. Мы могли бы продолжить расширение иерархии предыдущего примера, реализовав пакет Week.Child.Grandchild.Grand_grandchild.

Множественные потомки

До сих пор мы видели лишь один дочерний пакет родительского пакета. Однако родительский пакет также может иметь несколько дочерних. Мы могли бы расширить приведенный выше пример и создать пакет Week.Child_2. Например:

package Week.Child_2 is function Get_Last_Of_Week return String; end Week.Child_2;

Здесь Week по-прежнему является родительским пакетом для пакета Child, но также родительским пакетом и для пакета Child_2. Таким же образом, Child_2, очевидно, является одним из дочерних пакетов Week.

Это соответствующее тело пакета Week.Child_2:

package body Week.Child_2 is function Get_Last_Of_Week return String is begin return Sun; end Get_Last_Of_Week; end Week.Child_2;

Теперь мы можем сослаться на оба потомка в нашем тестовом приложении:

with Ada.Text_IO; use Ada.Text_IO; with Week.Child; use Week.Child; with Week.Child_2; use Week.Child_2; procedure Main is begin Put_Line ("First day of the week is " & Get_First_Of_Week); Put_Line ("Last day of the week is " & Get_Last_Of_Week); end Main;

Видимость

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

Рассмотрим пакет Book и его дочерний элемент Additional_Operations:

package Book is Title : constant String := "Visible for my children"; function Get_Title return String; function Get_Author return String; end Book;
package Book.Additional_Operations is function Get_Extended_Title return String; function Get_Extended_Author return String; end Book.Additional_Operations;

Это тела обоих пакетов:

package body Book is Author : constant String := "Author not visible for my children"; function Get_Title return String is begin return Title; end Get_Title; function Get_Author return String is begin return Author; end Get_Author; end Book;
package body Book.Additional_Operations is function Get_Extended_Title return String is begin return "Book Title: " & Title; end Get_Extended_Title; function Get_Extended_Author return String is begin -- "Author" string declared in the body -- of the Book package is not visible -- here. Therefore, we cannot write: -- -- return "Book Author: " & Author; return "Book Author: Unknown"; end Get_Extended_Author; end 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. Это адаптированный код:

package body Book.Additional_Operations is function Get_Extended_Title return String is begin return "Book Title: " & Get_Title; end Get_Extended_Title; function Get_Extended_Author return String is begin return "Book Author: " & Get_Author; end Get_Extended_Author; end Book.Additional_Operations;

Вот простое тестовое приложение для указанных выше пакетов:

with Ada.Text_IO; use Ada.Text_IO; with Book.Additional_Operations; use Book.Additional_Operations; procedure Main is begin Put_Line (Get_Extended_Title); Put_Line (Get_Extended_Author); end Main;

Объявляя элементы в теле пакета, мы можем реализовать инкапсуляцию в языке Ада. Эти элементы будут видимы только в теле пакета, но нигде больше. Но это не единственный способ добиться инкапсуляции в Аде: мы обсудим другие подходы в главе Изоляция.

Переименование

Ранее мы упоминали, что подпрограммы можно переименовывать. Мы также можем переименовывать пакеты. Опять же, для этого мы используем ключевое слово renames. В следующем примере пакет Ada.Text_IO переименовывается как TIO:

with Ada.Text_IO; procedure Main is package TIO renames Ada.Text_IO; begin TIO.Put_Line ("Hello"); end Main;

Мы можем использовать переименование, чтобы улучшить читаемость нашего кода, используя более короткие имена пакетов. В приведенном выше примере мы пишем TIO.Put_Line вместо более длинного имени (Ada.Text_IO.Put_Line). Этот подход особенно полезен, когда мы не используем спецификатор использования use, но хотим, чтобы код не становился слишком многословным.

Обратите внимание, что мы также можем переименовывать подпрограммы и объекты внутри пакетов. Например, мы могли бы просто переименовать процедуру Put_Line в приведенном выше примере исходного кода:

with Ada.Text_IO; procedure Main is procedure Say (Something : String) renames Ada.Text_IO.Put_Line; begin Say ("Hello"); end Main;