Copyright (C) А.Гавва V-0.4w май 2004

7. Пакеты

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

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

7.1 Общие сведения о пакетах Ады

7.1.1 Идеология концепции пакетов

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

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

Спецификация определяет интерфейс к вычислительным ресурсам (сервисам) пакета доступным для использования во внешней, по отношению к пакету, среде. Другими словами - спецификация показывает "что" доступно при использовании этого пакета.

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

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

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

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

Примечание:
В системе компилятора GNAT существует соглашение согласно которому файлы спецификаций имеют расширение ads (ADa Specification), а файлы тел имеют расширение adb (ADa Body).

7.1.2 Спецификация пакета

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


package Odd_Demo is
        
    type A_String is array (Positive range <>) of Character;

    Pi  : constant Float := 3.14;

    X   : Integer;

    type A_Record is 
        record
          Left  : Boolean;
          Right : Boolean;
        end record;

    -- примечательно, что дальше, для двух подпрограмм представлены только
    -- их спецификации, тела этих подпрограмм будут находиться в теле пакета

    procedure Insert(Item : in Integer; Success : out Boolean);
    function Is_Present(Item : in Integer) return Boolean;

end Odd_Demo;

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


имя_пакета.имя_используемого_ресурса

где имя_используемого_ресурса - это имя типа, подпрограммы, переменной и т.д.

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


with Odd_Demo;

procedure Odder_Demo is

    My_Name : Odd_Demo.A_String;
    Radius  : Float;
    Success : Boolean;

begin
    Radius := 3.0 * Odd_Demo.Pi;
    Odd_Demo.Insert(4, Success);
    if Odd_Demo.Is_Present(34) then ...
        . . .
end Odder_Demo;

Не трудно заметить, что доступ к ресурсам пакета, текстуально подобен доступу к полям записи.

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


with Odd_Demo;        use Odd_Demo;

procedure Odder_Demo is

    My_Name : A_String;
    Radius  : Float;
    Success : Boolean;

begin
    Radius := 3.0 * Pi;
    Insert(4, Success);
    if Is_Present(34) then ...
        . . .
end Odder_Demo;

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


-------------------------------
package No1 is
    A, B, C : Integer;
end No1;

-------------------------------
package No2 is
    C, D, E : Integer;
end No2;

-------------------------------
with No1;        use No1;
with No2;        use No2;

procedure Clash_Demo is
begin
    A := 1;
    B := 2;

    C := 3;        -- двусмысленность, мы ссылаемся
                   -- на No1.c или на No2.c?

    No1.C := 3;    -- избавление от двусмысленности путем возврата
    No2.C := 3;    -- к полной точечной нотации
end Clash_Demo;

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


package No1 is
    A : Integer;
end No1;

with No1;        use No1;
procedure P is
    A : Integer;
begin
    A := 4;       -- это - двусмысленно
    P.A := 4;     -- удаление двусмысленности путем указания
                  -- имени процедуры в точечной нотации
    No1.A := 5;   -- точечная нотация для пакета
end P;

7.1.3 Тело пакета

Тело пакета содержит все детали реализации сервисов, указаных в спецификации пакета. Схематическим примером тела пакета, для показанной выше спецификации, может служить:


package body Odd_Demo is

    type List is array (1..10) of Integer;
    Storage_List : List;
    Upto         : Integer;

    procedure Insert(Item     : in   Integer;
                     Success  : out  Boolean) is
    begin
        . . .
    end Insert;

    function Is_Present(Item : in Integer) return Boolean is

    begin
        . . .
    end Is_Present;


begin                -- действия по инициализации пакета
                     -- это выполняется до запуска основной программы!
    for I in Storage_List'Range loop
        Storage_List(I) := 0;
    end loop;
    Upto := 0;
end Odd_Demo;

Все ресурсы, указанные в спецификации пакета, будут непосредственно доступны в теле пакета без использования дополнительных инструкций спецификации контекста with и/или use.

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

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

Раздел "begin ... end", в конце тела пакета, содержит перечень инструкций инициализации для этого пакета. Инициализация пакета выполняется до запуска на выполнение главной подпрограммы. Это справедливо для всех пакетов. Следует заметить, что стандарт не определяет порядок выполнения инициализации различных пакетов.

7.2 Средства сокрытия деталей реализации внутреннего представления данных

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

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

Ада позволяет "закрыть" детали внутреннего представления данных и, таким образом, избавиться от проблемы массовых переделок. Такие возможности обеспечивает использование приватных типов (private types) и лимитированных приватных типов (limited private types).

7.2.1 Приватные типы (private types)

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

- изъятие средств (Withdraw)
- размещение средств (Deposit)
- создание счета (Create)

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

Для того чтобы выполнить поставленную задачу, мы описываем объект бухгалтерского счета Account как приватный тип:


package Accounts is
    type Account is private;        -- описание будет представлено позже

    procedure Withdraw(An_Account : in out Account;
                       Amount     : in     Money);

    procedure Deposit( An_Account : in out Account;
                       Amount     : in     Money);
    function Create( Initial_Balance : Money) return Account;
    function Balance( An_Account : in    Account) return Integer;

private                -- эта часть спецификации пакета
                       -- содержит полные описания
    type Account is
        record
            Account_No : Positive;
            Balance    : Integer;
        end record;
end Accounts;

В результате такого описания, тип Account будет приватным. Следут заметить, что Ада разрешает использовать следующие предопределенные операции над объектами приватного типа вне этого пакета:

- присваивание
- проверка на равенство (не равенство)
- проверки принадлежности ("in", "not in")

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

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

В данном примере необходимо обратить внимание на то, что спецификация пакета разделена на две части. Все что находится до зарезервированного слова private - это общедоступная часть описаний, которая будет "видна" всем пользователям пакета. Все что находится после зарезервированного слова private - это приватная часть описаний, которая будет "видна" только внутри пакета (и в его дочерних модулях; см. "Дочерние модули").

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

Хотя читатель спецификации пакета видит как устроено реальное внутреннее представление реализации приватного типа, это не обеспечивает его возможностью явным и допустимым способом использовать эту информацию. При этом, примечательным является то, что экземпляры объектов приватного типа могут быть созданы вне пакета. Например:


with Accounts;        use Accounts;

procedure Demo_Accounts is

    Home_Account : Account;
    Mortgage     : Account;
    This_Account : Account;

begin
    Mortgage := Accounts.Create(Initial_Balance => 500.00);
    Withdraw(Home_Account, 50);

        . . . 

    This_Account := Mortgage;        -- присваивание приватного типа - разрешено

    -- сравнение приватных типов
    if This_Account = Home_Account then

        . . .

end Demo_Accounts;

7.2.2 Лимитированные приватные типы (limited private types)

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

В качестве демонстрации сказанного, рассмотрим следующий пример:


package Compare_Demo is

    type Our_Text is private;

    . . .

private
    type Our_Text (Maximum_Length : Positive := 20) is
        record
            Length : Index := 0;
            Value  : String(1..Maximum_Length);
        end record;

    . . .

end Compare_Demo;

Здесь, тип Our_Text описан как приватный и представляет из себя запись. В данной записи, поле длины Length определяет число символов которое содержит поле Value (другими словами - число символов которые имеют смысл). Любые символы, находящиеся в позиции от Length + 1 до Maximum_Length будут нами игнорироваться при использовании этой записи. Однако, если мы попросим компьютер сравнить две записи этого типа, то он, в отличие от нас, не знает предназначения поля Length. В результате, он будет последовательно сравнивать значения поля Length и значения всех остальных полей записи. Очевидно, что алгоритм предопределенной операции сравнения в данной ситуации не приемлем, и нам необходимо написать собственную функцию сравнения.

Для подобных случаев Ада предусматривает лимитированные приватные типы. Изменим рассмотренный выше пример следующим образом:


package Compare_Demo is

    type Our_Text is limited private;

    . . .

    function "=" (Left, Right : in Our_Text) return Boolean;

    . . .

private
    type Our_Text (Maximum_Length : Positive := 20) is
        record
            Length : Index := 0;
            Value  : String(1..Maximum_Length);
        end record;

    . . .

end Compare_Demo;

Теперь, тип Our_Text описан как лимитированный приватный тип. Также, в спецификации пакета, описана функция знака операции сравнения на равенство "=". Реализация алгоритма этой функции должна быть помещена в тело пакета. Примечательно, что при совмещении знака операции равенства "=" автоматически производится неявное совмещение знака операции неравенства "/=". При этом следует учесть, что если функция реализующая действие знака операции равенства "=" возвращает значение тип которого отличается от предопределенного логического типа Boolean (полное имя - Standard.Boolean), то совмещение знака операции неравенства "/=" необходимо описать явно. Следует заметить, что Ада разрешает переопределять знак операции равенства для всех типов.

Для лимитированного приватного типа можно также создать процедуру для выполнения присваивания (или инициализации). Например, для показанного выше типа Our_Text, спецификация такой процедуры может иметь следующий вид:


    . . .
    procedure Init (T : in out Our_Text;
                           S : in     String);
    . . .

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

7.2.3 Отложенные константы (deferred constants)

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


package Coords is

    type Coord is private;
    Home: constant Coord;  -- отложенная константа!

private
    type Coord is record
        X : Integer;
        Y : Integer;
    end record;

    Home : constant Coord := (0, 0);
end Coords;

В результате такого описания, пользователи пакета Coords "видят" константу Home, которая имеет приватный тип Coord, и могут использовать эту константу в своем коде. При этом, детали внутреннего представления этой константы им не доступны и они могут о них не заботиться.

7.3 Дочерние модули (child units) (Ada95)

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

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

Дочерние модули непосредственно используются в стандартной библиотеке Ады. В частности, дочерними модулями являются такие пакеты как Ada.Text_IO, Ada.Integer_Text_IO. Следует также заметить, что концепция дочерних модулей была введена стандартом Ada95. В стандарте Ada83 эта идея отсутствует.

7.3.1 Расширение существующего пакета

Рассмотрим случай когда возникает необходимость расширения пакета который уже содержит некоторое множество описаний. Например, в пакете Stacks может понадобиться дополнительный сервис просмотра Peek. Если в текущий момент времени пакет Stacks используется многими модулями, то такая модификация, путем добавления нового сервиса, потребует значительных затрат на перекомпиляцию всех зависимых модулей, причем, включая и те модули которые не будут использовать новый сервис.

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


package Stacks is

    type Stack is private;
    procedure Push(Onto : in out Stack; Item : Integer);
    procedure Pop(From : in out Stack; Item : out Integer);
    function Full(Item : Stack) return Boolean;
    function Empty(Item : Stack) return Boolean;

private
    -- скрытая реализация стека
    ...
    -- точка  A 

end Stacks;

package Stacks.More_Stuff is

    function Peek(Item : Stack) return Integer;

end Stacks.More_Stuff;

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

В показанном выше примере, пакет Stacks.More_Stuff является дочерним пакетом для пакета Stacks. Значит, дочерний пакет Stacks.More_Stuff "видит" все описания пакета Stacks, вплоть до точки A.

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

Примечание:
Согласно правил именования файлов, принятым в системе компилятора GNAT, спецификация и тело пакета Stacks должны быть помещены в файлы:

stacks.ads и stacks.adb
соответственно, а спецификация и тело дочернего пакета Stacks.More_Stuff - в файлы:
stacks-more_stuff.ads и stacks-more_stuff.adb

Клиенту, которому необходимо использовать функцию Peek, просто необходимо включить дочерний пакет в инструкцию спецификатора совместности контекста with:


with Stacks.More_Stuff;

procedure Demo is

    X : Stacks.Stack;

begin
    Stacks.Push(X, 5);
    if Stacks.More_Stuff.Peek = 5 then
        . . .

end Demo;

Следует заметить, что включение дочернего пакета в инструкцию спецификатора совместности контекста with автоматически подразумевает включение в инструкцию with всех пакетов-родителей. Однако, инструкция спецификатора использования контекста use таким образом не работает. То есть, область видимости может быть получена пакетом только в базисе пакета.


with Stacks.More_Stuff; use Stacks; use More_Stuff;

procedure Demo is

    X : Stack;

begin
    Push(X, 5);
    if Peek(x) = 5 then
        . . .

end Demo;

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

7.3.2 Иерархия модулей как подсистема

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

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


package Root is

    -- корневой пакет может быть пустым

end Root;

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

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

7.3.3 Приватные дочерние модули (private child units)

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

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


private package Stacks.Statistics is

    procedure Increment_Push_Count;

end Stacks.Statistics;

Процедура Stacks.Statistics.Increment_Push_Count могла бы быть вызвана внутри реализации пакета Stacks. Такая процедура не будет доступна ни одному внешнему, по отношению к этой иерархии модулей, клиенту.


Copyright (C) А.Гавва V-0.4w май 2004