Абстрагирование общей функциональности
Абстрагирование общей функциональности
Показанные ранее реализации стека, предусматривали общее множество подпрограмм, таких как Push, Pop, и т.д.В Аде, мы можем установить общность используя общий тип.Общий тип описывает какие должны быть предусмотрены подпрограммы и какие профили должны иметь эти подпрограммы.При этом, он не указывает как реализовать эти подпрограммы.Тогда, тип, производный от такого общего типа, вынужден представить свою собственную реализацию каждой подпрограммы.Такой общий тип называют абстрактным типом, и он является абстракцией абстракции.Смысл этого заключается в том, чтобы указать клиентам абстракции, что выбор конкретной реализации абстракции не имеет значения.Клиенты всегда получат, как минимум, ту функциональность, которая описана абстрактным типом.Рассмотрим следующий пример:
package Stacks is type Stack is abstract tagged null record; -- абстрактный тип, который не имеет полей, и не имеет "реальных" операций. -- Ада требует "tagged"-описание для абстрактных типов. Underflow : exception; Overflow : exception; procedure Push(Item : in out Stack; Value : in Integer) is abstract; procedure Pop(Item : in out Stack; Value : out Integer) is abstract; function Full(Item : Stack) return Boolean is abstract; function Empty(Item : Stack) return Boolean is abstract; function Init return Stack is abstract;end Stacks; |
В данном случае, в пакете Stacks описан абстрактный тип стека Stack.Причем, в этом пакете приводится только перечень подпрограмм, которые будут реализованы каким-либо типом, производным от абстрактного типа Stack.Примечательно, что тип Stack может быть приватным:
package Stacks is type Stack is abstract tagged private; -- подпрограммы . . .private type Stack is abstract tagged null record;end Stacks; |
В этом же пакете или, что более типично, в другом пакете, можно описать расширение типа Stack и создать производный тип, который реализует указанную функциональность абстрактного типа:
with Lists; with Stacks;package Unbounded_Stacks is type Unbounded_Stack is new Stacks.Stack with private; -- тип Unbounded_Stack, производный от пустой записи стек, -- с небольшим расширением, добавленным как описано в приватной -- секции procedure Push(Item : in out Unbounded_Stack; Value : in Integer); procedure Pop(Item : in out Unbounded_Stack; Value : out Integer); function Full(Item : Unbounded_Stack) return Boolean; function Empty(Item : Unbounded_Stack) return Boolean; -- возвращает пустой инициализированный Unbounded_Stack function Init return Unbounded_Stack;private -- Расширяет пустую запись Stacks.Stack на одно поле. -- Все вызовы будут перенаправлены к внутреннему -- связанному списку. type Unbounded_Stack is new Stacks.Stack with record Values : Lists.List; end record;end Unbounded_Stacks; |
В данном случае, для реализации функциональности, которая задана абстрактным типом Stack, необходимо чтобы вызовы подпрограмм, обращенные к типу Unbounded_Stack, были соответствующим образом перенаправлены к связанному списку.Таким образом, тело пакета Unbounded_Stacks будет иметь следующий вид:
package body Unbounded_Stacks is procedure Push(Item : in out Unbounded_Stack; Value : in Integer) is begin Lists.Insert_At_Head(Item.Values, Value); end Push; procedure Pop(Item : in out Unbounded_Stack; Value : out Integer) is begin Lists.Remove_From_Head(Item.Values, Value); exception when Lists.Underflow => raise Stacks.Underflow; end Pop; . . .end Unbounded_Stacks; |
Абстракция данных
Абстракция данных
Абстракция данных является мощным инструментом современного программирования.Этот концептуальный подход позволяет объединить тип данных с множеством операций, которые допустимо выполнять над этим типом данных.Более того, эта философия позволяет использовать такой тип данных не задумываясь о деталях внутренней организации данных, которые могут быть продиктованы средствами используемого компьютерного оборудования.
Абстракция данных позволяет рассматривать необходимые объекты данных и операции, которые должны выполняться над такими объектами, без необходимости вникать в несущественные детали.Кроме того, абстракция данных является важнейшей составной частью объектно-ориентированного программирования.
Абстракция очереди
Абстракция очереди
Очень важно определить различие между тем какие сервисы предусматривает тип, и как этот тип их реализует.Например, абстракция списка, которая имеет соответствующие подпрограммы и сама может быть реализована как связанный список или как массив, может быть использована для реализации абстракции очереди.Абстракция очереди будет иметь только несколько операций:
Add_To_Tail Remove_From_Head Full Empty Init |
Необходимо убедиться, что в программе существует четкое различие между используемой абстракцией и реализацией.Кроме того, желательно возложить на компилятор проверку правильности осуществления вызова подпрограмм реализации, чтобы автоматически предотвратить непреднамеренные обращения к подпрограммам используемой абстракции.
Ниже приведен пример пакета использование которого может привести к проблемам:
package Lists is type List is private; procedure Add_To_Head(Item : in out List; Value : in Integer); procedure Remove_From_Head(Item : in out List; Value : out Integer); procedure Add_To_Tail(Item : in out List; Value : in Integer); procedure Remove_From_Tail(Item : in out List; Value : out Integer); function Full(Item : List) return Boolean; function Empty(Item : List) return Boolean; function Init return List;private type List is ... -- полное описание типаend Lists;with Lists; use Lists; -- абстракция списка Listspackage Queues is type Queue is new List; -- наследует операции типа List -- то есть, следующее описано "автоматически" (неявно) -- -- procedure Add_To_Head(Item : in out Queue; Value : in Integer); -- procedure Remove_From_Head( -- Item : in out Queue; -- Value : out Integer); -- и т.д.end Queues; |
Здесь, тип очереди (Queue) наследует все операции типа список (List), даже те, которые для него не предназначены (например Remove_From_Tail). При такой реализации типа Queue, клиенты абстракции могут легко нарушить очередь, которая должна разрешать только вставку в конец очереди и удаление из начала очереди.
Например, клиент пакета Queues может легко сделать следующее:
with Queues; use Queues;procedure Break_Abstraction is My_Q : Queue;begin Add_To_Head(My_Q, 5); Add_To_Tail(My_Q, 5); -- очередь должна разрешать вставку в конец очереди -- и удаление из начала очереди, -- или наоборотend Break_Abstraction; |
Для решения подобной проблемы при создании абстракции очереди необходимо использовать абстракцию списка приватно:
with Lists; -- это используется только приватной секциейpackage Queues is type Queue is private; procedure Remove_From_Head(Item : in out Queue; Value : out Integer); procedure Add_To_Tail(Item : in out Queue; Value : Integer); function Full(Item : Queue) return Boolean; function Empty(Item : Queue) return Boolean; function Init return Queue;private type Queue is new Lists.List;end Queues;package body Queues is -- выполняем конверсию типов (из Queue в List), а затем, -- выполняем вызов соответствующей подпрограммы List use Lists; -------------------------------------------------------------------- procedure Remove_From_Head(Item : in out Queue; Value : out Integer) is begin Lists.Remove_From_Head(List(Item), Value); end Remove_From_Head; -------------------------------------------------------------------- function Full(Item : Queue) return Boolean is begin return Lists.Full(List(Item)); end Full; . . . function Init return Queue is begin return Queue(Lists.Init); end Init;end Queues; |
Абстракция стека
Абстракция стека
В качестве простого примера, который демонстрирует использование пакета для реализации абстрактного типа данных, рассмотрим стек.Следует заметить, что клинтам этого пакета абсолютно безразлично каким образом выполнена внутренняя реализация стека, то есть клиенты не нуждаются в информации о том, что стек реализован с помошью массива или связанного списка.Шаблоном подобной абстракции может служить следующее:
package Stacks is Max : constant := 10; subtype Count is Integer range .Max; subtype Index is range .Max; type List is array(Index) of Integer; type Stack is record Values : List; Top : Count; end record; Overflow : exception; Underflow : exception; procedure Push(Item : in out Stack; Value : in Integer); procedure Pop(Item : in out Stack; Value : out Integer); function Full(Item : Stack) return Boolean; function Empty(Item : Stack) return Boolean; -- возвращает пустой проинициализированный стек function Init return Stack;end Stacks; |
Однако, в данном случае детали реализации достаточно очевидны, то есть кроме того, что клиенту пакета доступно то, что пакет предоставляет в качестве сервиса, клиенту пакета также доступно и то, как этот сервис реализован.Такую ситуацию можно достаточно просто изменить переместив все, в чем клиент не будет нуждаться, в приватную секцию спецификации пакета:
package Stacks is type Stack is private; -- неполное описание типа Stack. -- это делает информацию о деталях реализации недоступной. Overflow : exception; Underflow : exception; procedure Push(Item : in out Stack; Value : in Integer); procedure Pop(Item : in out Stack; Value : out Integer); function Full(Item : Stack) return Boolean; function Empty(Item : Stack) return Boolean; -- возвращает пустой проинициализированный стек function Init return Stack;private -- полное описание типа, вместе с другими приватными деталями. Max : constant := 10; subtype Count is Integer range .Max; subtype Index is range .Max; type List is array(Index) of Integer; type Stack is record Values : List; Top : Count; end record; end Stacks; |
Следует заметить, что хотя программист использующий такую абстракцию, имеет возможность увидеть все детали реализации (при наличии исходных текстов кода), он не может написать свою собственную программу, которая будет использовать эту информацию.Также очевидно, что такое разделение вынуждает лучше понять, что для абстракции важно, а что нет.
В качестве альтернативы, можно изменить приватную секцию в соответствии с реализацией стека на связанном списке:
package Stacks is type Stack is private; -- неполное описание типа Stack. -- это делает информацию о деталях реализации недоступной. Overflow : exception; Underflow : exception; procedure Push(Item : in out Stack; Value : in Integer); procedure Pop(Item : in out Stack; Value : out Integer); function Full(Item : Stack) return Boolean; function Empty(Item : Stack) return Boolean; -- возвращает пустой проинициализированный стек function Init return Stack;private type Stack; type Ptr is access Stack; type Stack is record Value : Integer; Next : Ptr; end record; end Stacks; |
Программа-клиент может использовать оба варианта пакета следующим образом:
with Stacks; use Stacks;procedure Demo is A : Stack := Init; B : Stack := Init; Temp : Integer;begin for I in 0 loop Push(A, I); end loop; while not Empty(A) loop Pop(A, Temp); Push(B, Temp); end loop; end Demo; |
Примечательно, что оба варианта реализации стека достаточно отличаются один от другого.Таким образом, показанные выше примеры наглядно демонстрируют использование идеи возможности подстановки различной реализации, или, более точно, использование программой-клиентом только того, что предоставляет публичный интерфейс.Напомним, что этот принцип очень важен при построении надежных систем.
Рассмотрим еще один пример использования рассмотренной абстракции стека:
with Stacks; use Stacks;procedure Not_Quite_Right is A, B : Stackbegin Push(A, 1); Push(A, 2); B := A; end Not_Quite_Right; |
Не трудно заметить, что при копировании реализация стека с использованием массива будет работать корректно, а реализация стека с использованием связанного списка скопирует только указатель на "голову" списка, после чего A и B будут указывать на один и тот же связанный список.Действительно, поскольку внимание текущего обсуждения больше посвящено теме абстракции данных, показанная выше реализация стека на связанном списке максимально упрощена.Для того чтобы избавиться от подобных "сюрпризов" в практической реализации стека на связанном списке необходимо использовать средства, которые предоставляют контролируемые типы Ады.
Элаборация
Элаборация
Процесс элаборации является процессом предварительной подготовки, который осуществляется непосредственно перед началом выполнения головной программы, тела инструкции блока или подпрограммы.Процесс элаборации также называют процессом выполнения описаний.
Следует заметить, что в других языках программирования подобные подготовительные действия, осуществляемые непосредственно перед запуском программы на выполнение, явно не определяются, а последовательность их выполнения неявно продиктована последовательностью компиляции и сборки приложения.
Еще один пример стека
Еще один пример стека
Предположим, что мы хотим создать пакет стека, но при этом, мы не хотим переписывать все подпрограммы. Мы уже имеем реализацию связанного списка, которая может служить основой для организации стека. Заметим, что пакет Lists сохраняет значения типа Integer.
Допустим, что существует пакет Lists, который описан следующим образом:
package Lists is type List is private; Underflow : exception; procedure Insert_At_Head(Item : in out List; Value : in Integer); procedure Remove_From_Head(Item : in out List; Value : out Integer); procedure Insert_At_Tail(Item : in out List; Value : in Integer); procedure Remove_From_Tail(Item : in out List; Value : out Integer); function Full(Item : List) return Boolean; function Empty(Item : List) return Boolean; -- возвращает пустой проинициализированный список function Init return List;private . . .end Lists; |
Позже можно будет использовать возможность Ады, которая позволяет скрыть полное описание типа в приватной секции.В данном случае, описывается абсолютно новый тип (поскольку в этом заинтересован клиент), который реально реализован как производный тип.В результате, клиент не может это "увидеть", и детали реализации остаются надежно спрятанными "под капотом".
Таким образом, реализация стека осуществляется следующим образом:
with Lists;package Stacks is type Stack is private; Underflow : exception; procedure Push(Item : in out Stack; Value : in Integer); procedure Pop(Item : in out Stack; Value : out Integer); function Full(Item : Stack) return Boolean; function Empty(Item : Stack) return Boolean; -- возвращает пустой проинициализированный стек function Init return Stack;private -- создаем новый тип, производя его от уже существующего -- это остается спрятанным от клиента, который будет -- использовать наш новый тип type Stack is new Lists.List; -- тип Stack наследует все операции типа List -- это неявно описывается как -- -- procedure Insert_At_Head(Item : in out Stack; Value : in Integer); -- procedure Remove_From_Head(Item : in out Stack; -- Value : out Integer); -- . . . -- function Full(Item : Stack) return Boolean; -- function Empty(Item : Stack) return Boolean; -- function Init return Stack;end Stacks; |
Теперь нам необходимо установить связь между неявно описанными подпрограммами (унаследованными от типа List), и подпрограммами, которые должны быть публично доступны.Это достаточно просто выполняется с помощью механизма "транзитного вызова".
package body Stacks is procedure Push(Item : in out Stack; Value : in Integer) is begin Insert_At_Head(Item, Value); end Push; -- эту процедуру можно сделать более эффективной, используя: pragma Inline(Push); procedure Pop(Item : in out Stack; Value : out Integer) is begin Remove_From_Head(Item, Value); exception => when Lists.Underflow => raise Stacks.Underflow; end Pop; . . .end Stacks; |
Следует заметить, что все это справедливо для публично описанных подпрограмм, имена которых отличаются от имен неявно описанных подпрограмм (тех, которые унаследованы от типа List).Однако, в спецификации пакета, присутствуют две функции Full, которые имеют профиль:
function Full(Item : Stack) return Boolean; |
В данном случае, одна функция описана в публично доступной секции, а вторая, неявно описана в приватной секции.Что же происходит на самом деле?Спецификация первой функции в публично доступной секции пакета - это только обещание предоставить эту функцию.Таким образом, это еще не реализованая функция, ее реализация будет выполнена где-то в теле пакета.Кроме этой функции существует другая, неявно описанная функция, которая, по-случаю, имеет такие же имя и профиль.Компилятор Ады может обнаружить, что обе функции имеют одинаковые имя и профиль, но он ничего не предполагает о том, что публично описанная функция должна быть реализована приватной функцией.
Например, предположим, что Lists.Full всегда возвращает "ложь" (False), для индикации того, что связанный список никогда не заполнен полностью и всегда может увеличиваться.Разработчику стекового пакета может понадобиться принудительное ограничение размера стека.Например, так чтобы стек мог содержать максимум 100 элементов.После чего разработчик стекового пакета соответствующим образом пишет функцию Stacks.Full.Это будет не корректно для реализации функции Full по-умолчанию, которая наследуется от реализации связанного списка.
Согласно правил видимости Ады, явное публичное описание полностью скрывает неявное приватное описание, и, таким образом, приватное описание становится вообще не доступным.Единственным способом вызвать функцию Full пакета Lists, будет явный вызов этой функции.
package body Stacks is function Full(Item : Stack) return Boolean is begin -- вызов оригинальной функции Full из другого пакета return Lists.Full(Lists.List(Item)); -- конверсия типа параметра end Full; . . .end Stacks; |
Все это выглядит ужасающе, но в чем заключается смысл таких требований Ады?Оказывается, обнаружение подобного рода проблем заключается в сущности конструирования программ с независимыми пространствами имен, или, выражаясь точнее, в разделении различных областей действия имен, где одинаковые имена не конфликтуют между собой.Таким образом, Ада просто предусматривает решение проблемы, которая является неизбежным последствием наличия различных пространств имен.Вариант языка в котором отсутствуют пространства имен - еще хуже чем такое решение.Поэтому, "не надо выключать пейджер, только потому, что не нравятся сообщения".Это очень существенное замечание, и не следует двигаться дальше, пока суть этого замечания не понятна.
Вид всего тела пакета Stacks может быть следующим:
use Lists;package body Stacks is procedure Push(Item : in out Stack; Value : in Integer) is begin Insert_At_Head(Item, Value); end Push; procedure Pop(Item : in out Stack; Value : out Integer) is begin Remove_From_Head(Item, Value); exception => when Lists.Underflow => raise Stacks.Underflow; end Pop; -- примечательно, что исключение объявленное в другом пакете -- "транслируется" в исключение описанное внутри этого пакета function Full(Item : Stack) return Boolean is begin return Lists.Full(List(Item)); end Full; function Empty(Item : Stack) return Boolean is begin return Lists.Empty(List(Item)); end Empty; -- возвращает пустой проинициализированный стек function Init return Stack is begin return Stack(Lists.Init); end Init;end Stacks; |
Именование методов
Именование методов
Для именования методов абстракций предлагаются следующие правила: Имена методов в комбинации с их аргументами должны читаться подобно фразам английского языка. функции, возвращающие логические результаты, должны использовать предикативные предложения. Именная ассоциация должна использоваться для методов, которые имеют более одного параметра. Аргумент, который обозначает обрабатываемый методом экземпляр объекта, не должен называться также как абстракция к которой он принадлежит.Вместо этого, он должен использовать более общее имя, такое как: Affecting, Self, This ...(примечание: это правило обязательно используется только при именном ассоцировании параметров).
Следующий пример демонстрирует использование этих правил:
Insert (The_Item => An_Apple, Before_Index => 3, Into => The_Fruit_List);Merge (Left => My_Fruit_List, Right => The_Citrus_Fruits);Is_Empty (The_Queue); -- используется позиционная ассоциация |
Следует учитывать, что при несоблюдении правила 4 может быть нарушена читабельность наследуемых операций.Рассмотрим следующий случай, где правило 4 нарушено:
package List is . . . procedure Pop (The_Item : out List.Item; From_List : in out List.Object); -- используется From_List -- вместо From end List;package Queue is . . . type Object is new List.Object; -- Queue.Object наследует -- операции List.Object end Queue;with List; with Queue; procedure Main is begin . . . Queue.Pop ( The_Item => My_Item From_List => The_Queue); -- не очень "чисто": -- имя ассоциации указывает список на (List), -- а аргументом является очередь (Queue) ?! end Main; |
Именование тэговых типов
Именование тэговых типов
При построении абстракций с применением тэговых типов, необходимо использовать согласованные наименования как для тэговых типов, так и для ассоциируемых с ними пакетов.Следует заметить, что соглашения по наименованию часто являются источником "религиозных войн".В следствие этого существует два различных подхода.
Первый подход, за исключением двух специальных случаев, позволяет установить единые соглашения по наименованию описаний, вне зависимости от использования объектно-ориентированных свойств, и при этом он интегрирует в себе использование объектно-ориентированных свойств: тэговые типы именуются традиционным образом для имен пакетов экспортирующих какие-либо абстракции, для которых предполагается множество реализаций, используется префикс Abstract_ для имен пакетов предусматривающих функциональные модули, которые могут быть подмешаны ("mixed in") в основную абстракцию используется суффикс _Mixin
Следующий пример, который состоит из двух частей, демонстрирует использование этого варианта соглашений по наименованию.Для первой части этого примера предположим, что тип Set_Element описывается где-то в другом месте:
package Abstract_Sets is type Set is abstract tagged private; -- пустое множество function Empty return Set is abstract; -- построение множества из одного элемента function Unit (Element: Set_Element) return Set is abstract; -- объединение двух множеств function Union (Left, Right: Set) return Set is abstract; -- пересечение двух множеств function Intersection (Left, Right: Set) return Set is abstract; -- удаление элемента из множества procedure Take (From : in out Set; Element : out Set_Element) is abstract; Element_Too_Large : exception;private type Set is abstract tagged null record; end Abstract_Sets;with Abstract_Sets; package Bit_Vector_Sets is -- одна реализация абстракции множества type Bit_Set is new Abstract_Sets.Set with private; . . . private Bit_Set_Size : constant := 64; type Bit_Vector is ... type Bit_Set is new Abstract_Sets.Set with record Data : Bit_Vector; end record; end Bit_Vector_Sets;with Abstract_Sets; package Sparse_Sets is -- альтернативная реализация абстракции множества type Sparse_Set is new Abstract_Sets.Set with private; . . . private . . . end Sparse_Sets; |
Вторая часть этого примера демонстрирует использование соглашений по наименованию для смешанных пакетов (mixin packages), которые обеспечивают поддержку оконной системы.
Предположим, что у нас есть тэговый лимитированный приватный тип Basic_Window, описание которого имеет следующий вид:
type Basic_Window is tagged limited private; |
Тогда:
generic type Some_Window is abstract new Basic_Window with private; package Label_Mixin is type Window_With_Label is abstract new Some_Window with private; . . . private . . . end Label_Mixin;generic type Some_Window is abstract new Basic_Window with private; package Border_Mixin is type Window_With_Label is abstract new Some_Window with private; . . . private . . . end Border_Mixin; |
Второй подход отображает использование объектно-ориентированных свойств с помощью специальных имен и суффиксов: пакет абстракции именуется без каких-либо суффиксов, в соответствии с объектом, который этот пакет представляет смешанные пакеты (mixin packages) именуются в соответствии с аспектами (facet) которые они представляют, используя суффикс _Facet для главного тэгового типа абстракции используется имя Instance (Object...) для ссылочного типа (если такой определен), значения которого ссылаются на значения главного тэгового типа, используется имя Reference (Handle, View...) для последующего описания типа, который является надклассовым типом соответствущим тэговому типу, используется имя Class
Следующий пример демонстрирует использование этого варианта соглашений по наименованию:
package Shape is subtype Side_Count is range 0 .. 100; type Instance (Sides: Side_Count) is tagged private; subtype Class is Instance'Class; . . . -- операции Shape.Instance private . . . end Shape;with Shape; use Shape; package Line is type Instance is new Shape.Instance with private; subtype Class is Instance'Class; . . . -- переопределенные или новые операции private . . . end Line;with Shape; use Shape; generic type Origin is new Shape.Instance; package With_Color_Facet is type Instance is new Origin with private; subtype Class is Instance'Class; -- операции для colored shapes private . . . end With_Color_Facet;with Line; use Line; with With_Color_Facet; package Colored_Line is new With_Color_Facet (Line.Instance); |
Простые описания, в этом случае, могут иметь следующий вид:
Red_Line : Colored_Line.Instance; procedure Draw (What : Shape.Instance); |
Показанная выше схема соглашений по наименованию корректно работает как в случае использования полных имен, так и при использовании спецификатора use.При использовании одного и того же имени для всех специфических типов (например type Instance) и надклассовых тпов, одни не полностью квалифицированные имена всегда будут "прятать" другие не полностью квалифицированные имена.Таким образом, компилятор будет требовать применение полной точечной нотации, чтобы избежать возникновение двусмысленности имен при использовании спецификатора use.
Необходимо использовать такую схему наименований, которая будет согласованной, читабельной и выразительной для представления абстракции.В идеале, схема наименований для различных способов использования тэговых типов, которые используются для построения различных абстракций, должна быть единой.Однако если используемая схема соглашений по наименованиям слишком жесткая, то использующие ее фрагменты кода будут выглядеть неестественно с точки зрения читабельности.При использовании подобных соглашений по наименованию для расширений типа, посредством наследования или подмешивания настраиваемых модулей (generic mixin), достигается читабельность описаний объектов.
Использование "is" и символа точки с запятой ';'
Использование "is"
и символа точки с запятой ';'
Бесконечное горе ждет тех пользователей Ады, которые путают назначение символа точки с запятой ';' с назначением "is".При использовании некоторых компиляторов, это приводит к длинной последовательности сообщений об ошибках.Наиболее вероятен случай использования точки с запятой вместо is - при описании подпрограмм, также как это делается в языке Паскаль:
procedure Do_Something(X : Integer); -- <---- это подразумевает проблему!!! -- описанияbegin -- инструкцииend Do_Something; |
Проблема заключается в том, что такое использование символа точки с запятой допустимо, но смысл его отличается от того, что вы ожидаете.Строка:
procedure Do_Something(X : Integer); |
является, в реальности, описанием спецификации процедуры, что в языке Паскаль больше похоже на опережающее описание процедуры с помощью директивы forward.Следовательно, перепутывание символа точки с запятой ';' с is почти гарантировано приводит к порождению большого количества сообщений об ошибках компилятора, поскольку синтаксический анализатор компилятора Ады будет трактовать такую инструкцию как спецификацию подпрограммы и будет сбит с толку появлением последующего блока begin-end, который ошибочно располагается вне контекста.Появление is точно указывает Ада-компилятору на то, что далее следует тело подпрограммы.
Спецификации подпрограмм,
являющиеся частью спецификации пакета,
могут рассматриваться в контексте опережающих описаний,
для чего в Паскале обычно используется директива forward.В этом случае, первая строка тела подпрограммы (в теле пакета) должна быть идентична спецификации,
за исключением того, что символ точки с запятой ';'
заменяется на is.Это отличается от Паскаля, где список параметров может не повторяться.
Код элаборации
Код элаборации
Стандарт Ada95 предусматривает общие правила выполнения кода в процессе элаборации (иначе, кода элаборации).Обработка кода элаборации осуществляется в трех контекстах:
Инициализация переменных - Переменные, описанные на уровне библиотеки в спецификациях или телах пакетов, могут требовать инициализацию, которая выполняется в процессе элаборации:
Sqrt_Half : Float := Sqrt (); |
Инициализация пакетов - Код в секции begin - end, на внешнем уровне тела пакета, является кодом инициализации пакета, и выполняется как часть кода элаборации тела пакета.
Аллокаторы задач уровня библиотеки - Задачи, которые описаны с помощью аллокаторов на уровне библиотеки, начинают выполняться немедленно и, следовательно, могут выполняться в процессе элаборации.
Любой из этих контекстов допускает вызов подпрограмм.Это подразумевает, что любая часть программы может быть выполнена как часть кода элаборации.Более того, существует возможность написания программы, вся работа которой осуществляется в процессе элаборации, при этом, головная прорамма - пуста.Следует однако заметить, что подобный способ структурирования программы считается стилистически неверным.
В отношении кода элаборации существует один важный аспект: необходима уверенность в том, что выполнение кода элаборации осуществляется в соответствующем порядке.Программа обладает множеством секций кода элаборации (потенциально, каждый модуль программы содержит одну секцию кода элаборации).Следовательно, важна правильность последовательности, в которой осуществляется выполнение этих секций.Для примера описания переменной Sqrt_Half, который показан выше, это подразумевает, что если другая секция кода элаборации использует Sqrt_Half, то она должна выполняться после секции кода элаборации, который содержит описание переменной Sqrt_Half.
Порядок элаборации не вызывает проблем, когда соблюдается следующее правило: элаборация спецификации и тела, каждого указанного в спецификаторе with модуля, осуществляется перед элаборацией модуля, который содержит такой спецификатор with.Например:
with Unit_1; package Unit_2 is ... |
В данном случае необходимо, чтобы элаборация спецификации и тела модуля Unit_1 осуществлялась перед спецификацией модуля Unit_2.Однако, такое правило накладывает достаточно жесткие ограничения.В частности, это делает невозможным наличие взаимно рекурсивных подпрограмм, которые распологаются в разных пакетах.
Можно предположить, что достаточно "умный" компилятор способен проанализировать код элаборации и определить правильный порядок элаборации, но в общем случае это не возможно.Рассмотрим следующий пример.
Тело модуля Unit_1 содержит подпрограмму Func_1, которая использует переменную Sqrt_1 описанную в коде элаборации тела Unit_1:
Sqrt_1 : Float := Sqrt (); |
Код элаборации тела модуля Unit_1 также содержит:
if expression_1 = 1 then Q := Unit_Func_2; end if; |
Существует также модуль Unit_2, структура которого аналогична: он содержит подпрограмму Func_2, которая использует переменную Sqrt_2 описанную в коде элаборации тела Unit_2:
Sqrt_2 : Float := Sqrt (); |
Код элаборации тела модуля Unit_2 также содержит:
if expression_2 = 2 then Q := Unit_Func_1; end if; |
Для показанных фрагментов кода суть вопроса заключена в выборе правильного порядка элаборации, то есть выбрать последовательность вида:
Spec of Unit_1 Spec of Unit_2 Body of Unit_1 Body of Unit_2 |
или последовательность вида:
Spec of Unit_2 Spec of Unit_1 Body of Unit_2 Body of Unit_1 |
При внимательном рассмотрении можно обнаружить, что ответить на этот вопрос во время компиляции не возможно.Если выражение expression_1 не равно 1, а выражение expression_2 не равно 2, то можно использовать любую из показанных выше последовательностей элаборации, поскольку отсутствуют вызовы функций.В случае, когда результаты обеих проверок истинны (true), не может быть использована ни одна из показанных последовательностей элаборации.
Если результат проверки одного условия истина (true), а другого - ложь (false), то одна из показанных последовательностей элаборации будет справедлива, а другая - нет.Например, если expression_1 = 1 и expression_2 /= 2, то присутствует вызов Func_2, но нет вызова Func_1.Это подразумевает, справедливость выполнения элаборации тела модуля Unit_1 перед элаборацией тела Unit_2, то есть первая последовательность элаборации правильна, а вторая - нет.
Если выражения expression_1 и expression_2 зависят от вводимых данных, то компилятор и/или редактор связей будут не в состоянии определить какое из условий будет истинным.Таким образом, для данного случая не возможно гарантировать правильность порядка элаборации во время выполнения программы.
Комбинирование абстракций, множественное наследование
Комбинирование абстракций, множественное наследование
Множественное наследование подразумевает построение новой абстракции путем наследования свойств двух и более абстракций.Хотя в основе замысла множественного наследования лежит концепция классификаци, его реализация и использование значительно различается в разных языках программирования.В следствие этого множественное наследование чаще используется не с целью создания сложных иерархий типов данных, а как кустарное приспособление, которое скрадывает слабости языка программирования.В действительности, подлинное практическое использование множественного наследования встречается крайне редко.
Следует также заметить, что поддержка множественного наследования значительно усложняет язык программирования.Она требует введения различных дополнительных правил для "разборок" с патологическими случаями.Примером такого случая может служить повторяющееся наследование, то есть, ситуация, когда наследование выполняется от различных абстракций, которые имеют одного общего предка.
Основываясь на этом, Ада не предусматривает встроенных механизмов поддержки множественного наследования.Следует заметить, что это не является недостатком, поскольку задачи, которые требуют использование множественного наследования в других языках программирования, как правило, могут быть решены другими средствами Ады.Кроме того, Ада предусматривает средства с помощью которых можно построить множественное наследование "вручную".
В отличие от языков программирования для которых единственным используемым понятием абстракции данных является класс, а наследование является главенствующим механизмом комбинирования абстракций и управления пространством имен, при программировании на языке Ада нет необходимости использовать множественное наследование вне задач классификации.Например, сборку новых объектов из других, уже существующих объектов, легче осуществить путем композиции, а не путем использования множественного наследования.Руководство по хорошо известному объектно-ориентированному языку программирования предлагает описать тип Apple_Pie (яблочный пирог) путем наследования от типов Apple (яблоко) и Cinnamon (корица).Это типичный случай ошибочного использования множественного наследования: производный тип наследует все свойства своих предков, хотя не все эти свойства применимы к производному типу.В данном примере, бессмысленно производить Apple_Pie (яблочный пирог) от Apple (например, Apple_Pie - не растет на дереве и не имеет кожуры).Ада не требует использования множественного наследования для управления пространством имен.Это выполняется не зависимо от классов и достигается путем использования соответствующих спецификаторов контекста (with и/или use).
Зависимость реализации, когда спецификация наследуется от одного типа, а реализация - от другого, может быть выражена без использования множественного наследования.Например, тип Bounded_Stack может быть построен путем наследования от абстрактного типа Stack, который описывает интерфейс (операции Push, Pop, ...) и конкретного типа Array, который будет предусматривать реализацию.Однако, такая форма множественного наследования будет правильной только в том случае, если все операции унаследованные от реализующего типа будут иметь смысл для производного типа.В этом случае, операции, которые предоставляют доступ к элементам массива, должны быть скрыты от пользователей типа Bounded_Stack, поскольку только тот элемент, который находится на вершине стека, должен быть доступен клиентам.Вместо всего этого, в Аде, новая абстракция может использовать простое наследование для описания спецификации и обычную композицию для реализации.
Контекст и видимость
Контекст и видимость
Ошибочное понимание разницы между контекстом (scope) и видимостью (visibility) может вызвать трудности у многих программистов, которые знакомятся с языком программирования Ада.Следует учесть, что понимание этого различия имеет важное значение при использовании Ады для разработки программного обеспечения.Спецификатор совместности контекста with помещает соответствующий библиотечный модуль в контекст, однако, ни один ресурс этого модуля не становится непосредственно видимым для клиента.Это является основным различием от директивы #include семейства языков C или директивы uses в реализации Паскаль-системы фирмы Borland.Ада предоставляет множество способов обеспечения непосредственной видимости различных ресурсов, после того как они помещены в контекст.Разделение контекста и видимости является важным концептом разработки программного обеспечения.Однако, следует заметить, что этот концепт редко реализовывается в различных языках программирования.Примечательно также, что идея пространства имен namespace, принятая в C++, подобна концепции контекста и видимости Ады.
Каждая инструкция и/или каждая конструкция языка имеет объемлющий контекст.Обычно, контекст легко увидеть в исходном тексте, поскольку контекст имеет какую-либо точку входа (описательная часть declare, идентификатор подпрограммы, идентификатор составного типа, идентификатор пакета...) и какую-либо явную точку завершения.Как правило, явная точка завершения контекста обозначается с помощью соответствующей инструкции end.Таким образом, как только мы обнаруживаем в исходном тексте какую-либо инструкцию end, мы понимаем, что она является точкой завершения какого-нибудь контекста.Контекст может быть вложенным.Например, одна процедура может быть описана внутри другой процедуры.
Несколько иначе выглядит ситуация, когда спецификатор совместности контекста with помещает в контекст какой-либо библиотечный модуль.В этом случае в контекст помещаются все ресурсы указанного библиотечного модуля, но при этом, спецификатор with не делает эти ресурсы непосредственно видимыми.
В Аде, какой-либо ресурс может находиться в контексте (scope) и при этом он не будет непосредственно видимым.Эта концепция больше характерна для Ады чем для других хорошо известных языков программирования.Для того чтобы соответствующие ресурсы были непосредственно видимыми, необходимо использовать соответствующие средства:
спецификатор использования use |
- | делает непосредственно видимыми все публично доступные ресурсы пакета |
спецификатор использования use type |
- | делает непосредственно видимыми все публично доступные знаки операций для указанного типа |
полная точечная нотация | - | ресурс указанный с помощью полной точечной нотации становится непосредственно видимым |
локальное переименование операций и знаков операций | - | обычно, является лучшим средством обеспечения непосредственной видимости для операций и знаков операций |
В процессе разработки, сообщения об ошибках, получаемые от Ада-компилятора, могут указывать, что некоторые ресурсы не видимы в том месте где они используются.Следует заметить, что подобные проблемы видимости наиболее часто возникают со знаками операций.Для того, чтобы сделать ресурс непосредственно видимым, можно использовать какой-либо из перечисленных выше способов.
Контролируемые или не контролируемые?
Контролируемые или не контролируемые?
Когда главный тип абстракции является производным от Ada.Finalization.Controlled, то вызовы Initialize, Adjust и Finalize автоматически выполняются компилятором (так указывается в руководстве по языку).Это полезно для предотвращения утечек памяти (компилятор никогда не забудет удалить объект, который больше не используется).Это полезно при дублировании объекта: две копии могут быть сделаны независимыми.
Однако эти свойства должны использоваться только в случаях, когда традиционное поведение компилятора, принятое по умолчанию, не удовлетворяет потребности разрабатываемой абстракции.Действительно, как только главный тип абстракции описан как контролируемый, то программист обязан заботиться о контролируемой обработке для каждого потомка.Таким образом, если потоммок добавляет какое-либо расширение, то унаследованные операции Initialize, Adjust и Finalize не имеют никакого представления о таком расширении.Следовательно, вы обязаны переопределять эти операции для потомков вашей абстракции.
Многоуровневые абстракции
Многоуровневые абстракции
При построении новой абстракции из другой абстракции, новая абстракция может нуждаться в доступе к приватным описаниям уже существующей абстракции.Например, повсеместные "widgets", использующиеся для программирования в X Window System, имеют спецификации (такие как "labels"), которые зависят от их реализации в приватном представлении "widgets".
Описание абстрактного типа данных Widget_Type для X Window System может иметь следующий вид:
with X_Defs; use X_Defs; package Xtk is type Widget_Type is tagged private; procedure Show (W : in Widget_Type); private type Widget_Ref is access all Widget_Type'Class; type Widget_Type is record Parent : Widget_Ref; Class_Name : X_String; X, Y : X_Position; Width, Height : X_Dimension; Content : X_Bitmap; end record; end Xtk; |
В данном случае, построение абстракции Label_Type поверх Widget_Type удобно осуществить с помощью создания дочернего модуля Xtk.Labels, который может выглядеть следующим образом:
with X_Defs; use X_Defs; package Xtk.Labels is type Label_Type is new Widget_Type with private; procedure Show (L : in Label_Type); -- необходим доступ к приватным описаниям Xtk (например, позиция label) private type Label_Type is new Widget_Type with record Label : X_String; Color : X_Color_Type; end record; end Xtk.Labels; |
Следует заметить, что в подобных случаях, иерархия модулей, как правило, параллельна иерархии абстракций представленных в этих модулях.
Настраиваемый модуль как параметр настройки
Настраиваемый модуль как параметр настройки
Настраиваемый модуль может быть конкретизирован с помощью использования настраиваемого модуля в качестве формального параметра настройки.В таком случае спецификация формального параметра может быть выполнена следующим образом:
generic with package P is new P_G (<>); -- фактический параметр для P_G определяется package Module_G is ... -- в P_G как обычно, а также (new) в Module_G |
Такой вид формального параметра настройки значительно упрощает импортирование целой абстракции.При этом нет необходимости указывать тип и делать полное перечисление необходимых подпрограмм, с соответственным указанием их профилей (параметр/результат).Следовательно, вся абстракция типа данных, которая описывается настраиваемым пакетом, может быть импортирована очень просто.
Такой механизм подразумевает построение одной настраиваемой абстракции из другой настраиваемой абстракции или расширение уже существующей абстракции.Например, так может быть построен пакет двунаправленного связанного списка (экспортируя Insert, Delete и Swap) при описании операции Sort, выполняющей сортировку при вставке.
В качестве иллюстрации данного подхода рассмотрим спецификации двух взаимосвязанных абстрактных типов данных: комплексные числа и матрицы комплексных чисел.Абстрактный тип данных комплексных чисел - это настраиваемый модуль с формальным параметром вещественного типа:
generic type Float_Type is digits <>; package Complex_Numbers_G is type Complex_Type is private; function Value (Re, Im : Float_Type) return Complex_Type; function "+" (Left, Right : Complex_Type) return Complex_Type; ... -- другие операцииprivate type Complex_Type is record Re, Im : Float_Type; end record; end Complex_Numbers_G; |
Абстрактный тип данных матрицы комплексных чисел - это настраиваемый модуль с формальным параметром абстрактного типа данных комплексных чисел (то есть с любой конкретизацией настраиваемого пакета показанного выше):
with Complex_Numbers_G; generic with package Complex_Numbers is new Complex_Numbers_G (<>); use Complex_Numbers; package Complex_Matrices_G is type Matrix_Type is array (Positive range <>, Positive range <>) of Complex_Type; function "*" (Left : Complex_Type; Right : Matrix_Type) return Matrix_Type; ... -- другие операции end Complex_Matrices_G; |
Следующий пример показывает две типичные конкретизации абстрактных типов данных для выполнения вычислений с комплексными числами и матрицами:
package Complex_Numbers is new Complex_Numbers_G (Float); package Complex_Matrices is new Complex_Matrices_G (Complex_Numbers);package Long_Complex_Numbers is new Complex_Numbers_G (Long_Float); package Long_Complex_Matrices is new Complex_Matrices_G (Long_Complex_Numbers); |
Никогда не используйте неинициализированные объекты
Никогда не используйте неинициализированные объекты
Для избежения непредсказуемого поведения при использовании неинициализированных объектов, описание главного типа абстракции должно устанавливать значения по умолчанию для каждого атрибута.Следовательно, каждая абстракция должна описывать публичный объект nil, который может быть использован как значение атрибута по умолчанию, в контексте описания абстракции более высокого уровня.
Для простых типов, таких как Integer_32 или Float_6, значение по умолчанию соответствует ограниченному выбору значений, которые могут быть использованы вместо Integer_32'Last или Float_6'Last.
Для перечислимых типов значение по умолчанию должны соответствовать выбору по умолчанию.
Объектно-ориентированное программирование
Объектно-ориентированное программирование
Объектно-ориентированное программирование в отличие от метода, основанного на управлении, характеризуется тем, что программа большей частью описывает создание, манипуляцию и взаимодействие внутри набора независимых и хорошо определенных структур данных, называемых объектами.Метод, основанный на управлении, рассматривает программу как управляющую последовательность событий (действий) над множеством структур данных этой программы.В обоих случаях задачей программы является преобразование некоторого заданного набора значений данных из некоторой входной или начальной формы к некоторой выходной или конечной форме.Хотя такое отличие на первый взгляд может показаться несколько неестественным или надуманным, имеющиеся отличия тем не менее весьма существенны.
В программировании, основанном на методе, который базируется на управлении, можно обнаружить, что по мере возрастания сложности и размера программы становится все труднее и труднее сохранять ясное представление о всей последовательности (или последовательностях) действий, которые выполняются этой программой.В этом случае, разбивка длинных последовательностей действий на короткие группы может оказать существенную помощь.Фактически хороший программист никогда не создает непрерывные последовательности недопустимо большой длины, разбивая их на серию подпрограмм.
Однако, данные, которые обрабатываются подпрограммами, не могут быть разбиты аналогичным образом на ряд независимых единиц.Как правило, большинство данных находятся в глобальных структурах, к которым имеют доступ все подпрограммы.Следовательно, большинство удобств, ожидаемых от подпрограмм, никогда не реализуются.Суть проблемы заключается в числе подпрограмм и их зависимости от формы и целостности разделяемых структур данных.По мере того как это число растет, становится все более трудным, если не невозможным, организовывать подпрограммы и данные в подходящие подструктуры, например в строго иерархическом порядке, даже при использовании алголоподобных языков, аналогичных Паскалю.
В противоположность этому, в объектно-ориентированном программировании работа начинается со связывания одной-единственной структуры данных с фиксированным набором подпрограмм.Единственными операциями, определяемыми над данным объектом, являются соответствующие подпрограммы.Такую структуру данных называют объектом, а связанный с ней набор операций - "пакетом".Обычно один набор таких операций "общего пользования" определяется и делается доступным для всех компонентов программы, имеющей доступ к этому объекту данных.Эти общедоступные операции имеют строго определенные спецификации.Тела их подпрограмм и любых подпрограмм, от которых эти тела могут в дальнейшем зависеть, можно обособить и сделать полностью скрытыми.Пакет также может быть использован для скрытия представления внутренней структуры объекта с тем, чтобы подпрограммы из других пакетов не могли обойти эти подпрограммы общего пользования и непосредственно манипулировать с объектом.
Такое обособление называется абстрактным типом данных и оно может привести написание программы к значительному упрощению, то есть сделать программу более понятной, корректной и надежной.В дополнение к этому, обеспечивается гибкость (и переносимость), поскольку части объектов, их представления и операции общего пользования могут быть изменены или заменены другим набором частей.
Опасность наследования
Опасность наследования
Когда главный тип абстракции - производный, он наследует все примитивные методы своих предков.Иногда некоторые из унаследованных методов не будут работать или будут не правильно работать с производной абстракцией (как правило из-за того, что они не имеют представления о новых атрибутах).В подобных случаях возникает необходимость переопределения унаследованных методов.
Упущение необходимости переопределения метода является одной из широко распространенных ошибок.Такую ошибку не всегда можно обнаружить во время тестирования самостоятельного модуля, без проверки всех унаследованных методов в контексте производной абстракции.
Контролируемые объекты Ada 95 - это первые кандидаты для повышенного внимания в подобных ситуациях.Любой контролируемый атрибут, добавленный в производной абстракции, требует переопределения унаследованных подпрограмм Adjust и Finalize (как минимум).Они должны вызываться, соответственно, для подгонки и очистки новых атрибутов.
Рассмотрим следующий пример:
package Coordinates is type Object is tagged private; . . . function Distance_Between (Left : in Coordinates.Object; Right: in Coordinates.Object) return Distance.Object; . . . end Coordinates;package Position is type Object is new Coordinates.Object with private; . . . function Distance_Between -- переопределяет (Left : in Position.Object; -- Coordinates.Distance_Between Right : in Position.Object) return Distance.Object; . . . end Position; |
Здесь, абстракция Position является производной от абстракции Coordinates, но метод Distance_Between должен быть переопределен для учета в векторе позиции компонента высоты.
Описания и их последовательность
Описания и их последовательность
Стандарт языка Паскаль требует использование правильного порядка следования описаний (константы, типы, переменные, подпрограммы), что ослабляется некоторыми реализациями Паскаль-систем.Ада обладает более гибкими требованиями к порядку следования описаний.Так, стандарт Ады подразумевает "базовые описания" и "поздние описания".Таким образом, к "базовым" описаниям можно отнести описания констант, типов и переменных, а к "поздним" описаниям - описания подпрограмм (процедур и функций).Следует заметить, что мы не рассматриваем остальные описания в целях упрощения.В описательной части, программы или подпрограммы, базовые описания могут быть свободно перемешаны (с естественным пониманием того, что перед тем как что-либо используется оно должно быть предварительно описано).Все базовые описания должны предшествовать всем поздним описаниям.
В Паскале, зарезервированные слова type, const и var должны появляться в описательной части только один раз.В Аде, описание каждого типа или подтипа должно соответственно начинаться с type или subtypeПримером описания константы может служить следующее:
FirstLetter: constant Character := 'A'; |
Зарезервированное слово var не используется вовсе, поэтому переменные описываются подобным образом:
Sum : Integer; |
Кроме того, описание типа записи, в Аде, всегда должно завершаться end record.
Пакеты Ады и их соответствие модулям Паскаля
Пакеты Ады и их соответствие модулям Паскаля
Реально, модули Паскаля (unit) не являются частью стандарта ISO Pascal.Однако, их использование предусматривается многими современными расширениями языка Паскаль, например, версиями Паскаль-систем от Borland, Symantec или Free Pascal.Модули Паскаля являются грубым эквивалентом пакетов Ады с двумя важными отличиями: В то время, когда интерфейс модуля Паскаля является, в основном, частью того же самого файла с исходным текстом который содержит детали внутренней реализации этого модуля, пакет Ады может быть нормально разделен на самостоятельные файлы спецификации и тела пакета.Некоторые компиляторы явно требуют такое разделение.В любом случае, такое разделение настоятельно рекомендуется, поскольку Ада, при перекомпиляции спецификации пакета, осуществляет принудительную перекомпиляцию всех модулей-клиентов, которые используют этот пакет, а при перекомпиляции тела пакета - нет. Паскаль-системы не предусматривают непосредственного эквивалента для приватных (private) типов Ады.
Пакеты как средство абстракции данных
Пакеты как средство абстракции данных
Пакет в языке Ада представляет собой конструкцию, которая может использоваться для определения абстрактных данных.Большинство экспертов рассматривают эту возможность как основной вклад языка Ада в решение задачи разработки программного обеспечения с наименьшими затратами, большей надежностью, гибкостью и верифицируемостью.Пакет предоставляет возможность связать отчетливо выраженный набор спецификаций для работы со структурой данных (или с классом структур данных) с некоторыми, возможно скрытыми, подробностями реализации.
При использовании пакетов для абстракции данных их можно разделить на два вида: пакеты-преобразователи и пакеты-владельцы.Пакеты-преобразователи могут использоваться для реализации чисто абстрактных типов данных, а пакеты-владельцы - для реализации более обобщенных видов менеджеров типа.Более точно это можно охарактеризовать следующим образом: Пакет-преобразователь (невладелец) может обновлять только те данные, которые были переданы ему через формальные параметры.Такой пакет не содержит внутри себя информацию о состоянии, относящуюся к этим данным.Это означает, что пакет не может содержать внутри себя обновляемых данных.Поскольку пакет-преобразователь "не владеет" никакой информацией о состоянии, каждая выполняемая им операция должна возвращать результат вызвавшей его программе.Пакет-преобразователь не может "помнить" результаты предыдущих обращений к нему.Простым примером пакета-преобразователя может служить пакет, который создает комплексные числа, производит операции над комплексными числами или осуществляет обе эти операции. Пакет-владелец "чувствителен к истории", поэтому он может "помнить" результаты предыдущих обращений к нему.Пакет-владелец "владеет" некоторой информацией о состоянии.Это означает, что пакет содержит внутри себя данные, которые могут быть обновлены в течение жизни этого пакета.Таким образом, для выполнения операции пакету могут быть переданы данные через его формальные параметры.Пакет-владелец после выполнения операции не должен обязательно возвращать результат вызвавшей его программе.Примером пакета-владельца может служить пакет, который поддерживает для компилятора базовую таблицу активных файлов.
Следует заметить, что пакеты-преобразователи, как правило, используются для реализации абстрактных типов данных, которые предоставляют клиентской программе тип данных с помощью которого в ней могут быть описаны объекты (переменные) этого типа.
В свою очередь, пакеты-владельцы, как правило, используются для реализации абстрактных объектов данных, что подразумевает инкапсуляцию одиночного объекта внутри тела пакета.Причем, такой объект непосредственно не доступен для клиента, а манипуляция этим объектом осуществляется через подпрограммы, которые указаны в спецификации пакета.
Каждый пакет имеет приватную и открытую части (также называемые видимой и невидимой частями).Открытая часть непосредственно доступна клиентской программе через: явное указание перед программой имен пакетов, к которым должен осуществляться доступ (такое указание носит название списка with) вложение пакета внутрь клиентской программы, делая этим видимую часть пакета локально доступной (что осуществляется согласно правилам, напоминающим правила видимости в языке Алгол)
Первый из этих механизмов представляет собой важное новшество, внесенное языками, подобными языку Ада.Использование его настоятельно рекомендуется.Второй из этих механизмов использовать нежелательно. Его применение ведет к появлению трудночитаемых и трудноотлаживаемых программ и является "вредным наследием" блочно-структурированных языков, подобных Алголу-6
В любом случае приватная часть пакета непосредственно недоступна для клиентской программы.Эта информация становится доступной только косвенным образом, посредством выполнения операций, указанных в информации об интерфейсе в видимой части.
Поскольку структура пакета обеспечивает четкое разделение на приватную и видимую части (спецификации), то процесс разработки приватной части может осуществляться отдельно от видимой.Это означает, что реализация приватной части может быть заменена на другую версию (когда появятся лучшие или более правильные идеи реализации) без изменения оставшейся части программы.
Параметры подпрограмм
Параметры подпрограмм
Режимы передачи параметров "in", "out" и "in out". для подпрограмм Ады, являются грубыми эквивалентами режимов передачи параметров по значению и по ссылке (var) для подпрограмм Паскаля.
В теле подпрограммы, "in"-параметры могут быть только прочитаны.Основное отличие между "out" и "in out" параметрами заключается в том, что текущее значение фактического "in out"-параметра передается в процедуру, а текущее значение фактического "out"-параметра в процедуру не передается и, следовательно, может считаться неопределенным до указания значения внутри подпрограммы.Функции Ады не могут иметь "out" и "in out" параметры.
В случае передачи в подпрограмму массива, использование режима передачи параметров "in out" не обеспечивает никакого выигрыша в эффективности по сравнению с режимом передачи параметров "in".Подобный прием является широко распространенным в языке Паскаль, когда большие массивы передаются как var-параметры.Правила языка Паскаль требуют, чтобы var-параметры передавались по ссылке, а параметры передаваемые по значению - копировались.Ада использует другие правила: параметры скалярных типов всегда передаются по значению/результату не зависимо от указанного режима передачи параметров.Ада допускает, чтобы параметры составных типов (массивы и/или записи) также передавались по значению/результату, но реальные Ада-компиляторы никогда не так не делают, в особенности, когда составной тип имеет большие размеры.Таким образом, реальные Ада-компиляторы передают в подпрограммы массивы и записи больших размеров по ссылке, даже если для них указан режим передачи параметров "in".Поскольку "in"-параметры гарантировано не могут быть записаны, то не существует опасности непреднамеренного изменения содержимого параметра внутри вызванной подпрограммы.
В случае использования режимов "in" и "in out" для передачи параметров скалярных типов, значения копируются обратно в вызвавшую подпрограмму после нормального завершения вызвынной процедуры.Таким образом, если вызов процедуры заканчивается распространением исключения в вызвавшую подпрограмму, то значения параметров не копируются обратно, в вызвавшую подпрограмму, и, следовательно, вызвавшая подпрограмма сохраняет оригинальные значения (те которые были до вызова процедуры).
Инструкции ввода/вывода Ады, являются обычными вызовами процедур, которые подразумевают, что только одиночные целочисленные, вещественные, символьные, строковые или перечислимые значения могут быть прочитаны или отображены с помощью каждого вызова Get или Put.При написании инструкций ввода/вывода, для этих процедур нельзя предоставлять произвольное число параметров, как это допускается в Паскале.Попытка выполнения этого, приведет к естественной ошибке компиляции, говорящей о не совпадении числа параметров ("unmatched procedure call"), когда компилятор пытается найти версию процедуры Get или Put, с соответствующим числом параметров.
Подсистемы
Подсистемы
Большие системы обычно состоят из сотен и даже тысяч самостоятельных компонентов.Обычно не требуется, чтобы каждый отдельный компонент активно взаимодействовал со всеми остальными частями системы.Таким образом, отдельные компоненты собираются в группы или кластеры модулей, которые интенсивно взаимодействуют между собой.При этом, взаимодействие отдельного компонента с остальной системой не так активно или вообще отсутствует.Подобные группы компонентов называют подсистемами (subsystem), и они могут собираться, управляться и обслуживаться изолировано от остальной системы.
Для поддержки построения и облегчения сопровождаемости больших систем, Ada95 предоставляет концепцию дочерних библиотечных модулей.Любой вид библиотечных модулей (подпрограммы, пакеты...) может быть описан как дочерний модуль библиотечного пакета (или настраиваемого модуля).Это позволяет строить иерархии библиотечных модулей.Каждый дочерний модуль (потомок) имеет свою приватную часть и тело, и способен "видеть" приватные описания своего предка, что аналогично текстовой вставке спецификации дочерних модулей в спецификацию пакетов родителей, предоставляя возможность раздельной компиляции их спецификаций и обеспечивая большую гибкость в управлении видимостью.
Подсистема (subsystem) - это иерархия библиотечных модулей.Она может состоять из подпрограмм, но чаще всего она состоит из иерархии пакетов.Подсистема может быть настраиваемой, в таком случае все потомки (дочерние модули) также должны быть настраиваемыми.Подсистемой можно манипулировать как тесно интегрированной сборкой отдельных компонентов, так и как единым целым.Спецификация всей подсистемы - это иерархия спецификаций ее публичных модулей.
Дочерний модуль может быть приватным модулем.Таким образом, его использование будет ограничено телами модулей той же самой иерархии, и будет доступно для спецификаций и тел других приватных модулей той же самой иерархии.Это является дополнительным уровнем управления для обеспечения возможности изоляции внутренних свойств подсистемы в спрятанных модулях, которые известны и совместно используются только внутри иерархически связанной группы библиотечных модулей.
Приватный потомок может быть добавлен к другому потомку, или даже удален из подсистемы, никак не затрагивая общую спецификацию подсистемы.В результате этого, при модификации приватного дочернего модуля, клиенты подсистемы не будут нуждаться в перекомпиляции.Хотя приватный дочерний модуль является внутренним (он не может быть сделан доступным через приватную часть других дочерних модулей), он предусматривает дополнительный уровень управления видимостью, поскольку он используется в телах модулей подсистемы.
Разделение компонентов подсистемы на модули предков и их потомков позволяет осуществлять изменения дочерних модулей без необходимости перекомпиляции модулей предков или независимых родственных дочерних модулей.Таким образом, есть возможность управлять тем, как много компонентов системы зависит от отдельных аспектов самой системы.Как результат, перекомпиляция может быть ограничена подмножеством перекомпилируемых модулей подсистемы, хотя внешне подсистема будет рассматриваться и выглядеть как одиночный библиотечный модуль.
Использование иерархических модулей предусматривает улучшенное управление пространством имен.Предположим, что кто-то приобрел два семейства программных компонентов.Одно, у Mr. Usable, который написал пакет Strings, для строковой обработки.Другое, у Mr. Reusable, которое также включает пакет с именем Strings.В такой ситуации, при одновременном использовании обеих семейств программных компонентов, возникает коллизия имен в пространстве имен библиотеки.С другой стороны, если для семейств компонентов предусмотрены префиксы Usable.Strings и Reusable.Strings, то коллизии имен не происходит.Это дает нам возможность сконцентрировать свои усилия на управлении пространством имен для подсистем или семейств компонентов вместо управления плоским пространством имен для всех компилируемых модулей.
Такое структурирование пространства имен можно обнаружить в предопределенном окружении Ады: теперь, все предопределенные пакеты являются дочерними модулями пакетов Ada, System и Interfaces (с соответствующими библиотечными переименованиями для поддержки обратной совместимости с Ada 83).
Предположим, что существует сложная абстракция, которая инкапсулирована в единую подсистему.Кто-либо может предусмотреть различное специализированное представление такой абстракции посредством различных интерфейсных пакетов подсистемы.Такой подход похож на представление в базе данных.
Например, простой банковский счет может иметь balance (остаток на счете) и interest_rate (процентная ставка).Представление заказчика заключается в том, что он может выполнить deposit (положить на счет) или withdraw (снять со счета), и посмотреть состояние interest_rate.Представление клерка в банке заключается в том, что он может изменить interest_rate для любого банковский счета.Очевидно, что два таких представления не должны быть объединены в единый интерфейс (банк не позволяет заказчикам изменять interest_rate).Таким образом, легко определить когда код написанный для представления заказчика незаконно использует код предназначенный для представления клерка.
Рассмотрим пример операционной системы (например POSIX).Очевидно, что никто не желает иметь один большой пакет с сотнями типов и операций.Должно существовать логическое группирование типов и операций.В Ada95 фактически, некоторые приватные типы, которые должны быть совместно используемы, больше не являются помехой для подобного разделения операций, благодаря видимости приватных описаний, которая обеспечена предками для дочерних модулей.Иерархические модули позволяют логическое структурировани и группирование, вне зависимости от используемых приватных описаний типов.
Подсистема может иметь маленький и простой интерфейс, и при этом быть очень сложной и иметь большую реализацию.Как только спецификация подсистемы (спецификации одного или более пакетов) описана, одна группа разработчиков может использовать подсистему пока другая (контрагент) - будет работать над ее реализацией.
Предположим,
что существуют две подсистемы.Например, Radar и Motor.Между разными группами разработчиков,
при разработке своих подсистем,
не будет возникать коллизия имен "вспомогательных пакетов".Например, для Radar.Control и Motor.Control.Таким образом,
для разных групп разработчиков
упрощаются описания подсистем и облегчается разработка,
а также значительно облегчаются
(или полностью исчезают)
проблемы общей интеграции.
Построение абстракции путем композиции
Построение абстракции путем композиции
Абстракция данных может быть построена с помощью композиции, когда новый абстрактный тип данных составляется из нескольких уже существующих абстрактных типов данных.В таком случае, новый тип данных описывается как запись (или тэговая запись) полями которой являются уже существующие типы данных.Функциональность нового типа данных обеспечивается путем перенаправления вызовов подпрограмм к соответствующим компонентам этого типа.Рассмотрим следующий пример:
with Lists; use Lists;package Queues is type Queue is private; procedure Remove_From_Head(Item : in out Queue; Value : out Integer); procedure Add_To_Tail(Item : in out Queue; Value : Integer); function Full(Item : Queue) return Boolean; function Empty(Item : Queue) return Boolean; function Init return Queue;private -- очередь состоит из следующих элементов: type Queue is record Values : List; No_Of_Elements : Integer; end record; end Queues;package body Queues is procedure Remove_From_Head(Item : in out Queue; Value : out Integer) is begin Remove_From_Head(Item.Values, Value); Item.No_Of_Elements := Item.No_Of_Elements - 1; end Remove_From_Head; procedure Add_To_Tail(Item : in out Queue; Value : Integer) is begin Add_To_Tail(Item.Values, Value); Item.No_Of_Elements := Item.No_Of_Elements + 1; end Add_To_Tail; procedure Reset (Item : in out Queue) is ... function Full(Item : Queue) return Boolean is begin return Full(Item.Values); end Full; function Empty(Item : Queue) return Boolean is begin return Item.No_Of_Elements = 0; end Empty;end Queues; |
В этом примере вызовы подпрограмм, обращенные к типу очереди Queue, будут "перенаправлены" к компоненту список (List), на который возлагается ответственность за реализацию некоторых аспектов абстракции очереди (в данном случае - это основная часть).
Пример программирования посредством расширения
Пример программирования посредством расширения
Программирование путем расширения подразумевает, что при необходимости добавления в программную систему каких-либо новых свойств она может быть выполнена простым добавлением нового кода, без необходимости внесения изменений в уже существующие модули программного обеспечения.При этом, поддержка обеспечиваемая со стороны языка программирования является очень полезной.
Представим себе функцию, которая читает до 20 сообщений с какого-либо интерфейса, обрабатывает статистику и генерирует какой-либо итоговый результат.Для остальной части системы непосредственно необходим только итоговый результат, и ее не заботят входные сообщения или работа интерфейса.В таком случае, простым решением будет помещение в самостоятельный пакет функции Summarizer, вычисляющей итоговый результат, вместе с перечислением переменных, которые указывают этой функции на то, какой интерфейс необходимо использовать:
package Simple_Approach is type Interfaces is (Disk_File, Serial_Interface, ...); type Summary_Type is record . . . end record; function Summarizer (Interface_To_Use: Interfaces) return Summary_Type;end Simple_Approach; |
Естественно, что в результате такого простого решения, при необходимоти добавить какой-либо новый интерфейс потребуется переписать заново тип Interfaces и функцию Summarizer, после чего, перекомпилировать все модули которые указывают пакет Simple_Approach в спецификаторе withs.
Необходимо заметить, что Ада очень часто используется в жизненно-критических системах (например, в авиационном оборудовании), где каждый программный модуль должен пройти длительный и дорогостоящий цикл тестирования скомпилированного объектного кода.Следовательно, после перекомпиляции модуля этот цикл тестирования должен быть пройден заново даже в случае отсутствия изменений в исходном тексте.
Программирование путем расширения позволяет избавиться от подобных переписываний и перекомпиляций.Первым вопросом, в этом случае, является: "что необходимо расширять?".В этом случае, необходима возможность расширения операций по получению сообщений от интерфейса.Таким образом, следует создать процедуру Get, которая получает сообщение от интерфейса основываясь на тэговом типе, что обеспечит ее полиморфность:
with Message; package Generic_Interface is type Flag is tagged null record; procedure Get (Which_Interface: Flag; Data: Message.Data_Type);end Generic_Interface; |
Теперь можно написать функцию Summarizer как надклассовую, которая, для получения сообщений от различных интерфейсов, использует соответствующую диспетчеризацию:
with Generic_Interface; package Extension_Approach is function Summarizer (Interface_To_Use: Generic_Interface.Flag'Class) return Summary_Type; end Extension_Approach; - - - - - - with Messages; package body Extension_Approach is function Summarizer (Interface_To_Use: Generic_Interface.Flag'Class) return Summary_Type is Data: array (0) of Message.Data_Type; begin for I in 1 .. 20 loop Get (Interface_To_Use, Data (I)); exit when Data (I).Last_Message; end loop; ... |
Тело процедуры Get, в пакете Generic_Interface, может возвращать исключение, или получать сообщение от интерфейса по умолчанию (в этом случае пакет, наверное, должен быть назван Default_Interface).
Для расширения Summarizer с целью получения сообщений от какого-либо нового интерфейса, необходимо просто расширить тип Generic_Interface.Flag и переопределить его процедуру Get:
with Message; with Generic_Interface; package Disk_Interface is type Flag is new Generic_Interface.Flag with null record; procedure Get (Which_Interface: Flag; Data: Message.Data_Type);end Disk_Interface; |
В результате этого, тип Flag способен хранить различные фактические данные для любых случаев.
Теперь мы можем обеспечить, чтобы Summarizer получал сообщения от диска и возвращал нам итоговый результат:
Summary := Extension_Approach.Summarizer (Disk_Interface.Flag'(null record)); |
Благодаря этому, даже в случае написания Disk_Interface после Summarizer, не возникает нужды переписывать Summarizer.
Затратив несколько больше усилий, можно запрограммировать почти все пользовательские системы так, что они не будут нуждаться в перепрограммировании или даже перекомпиляции для работы с любым новым интерфейсом.Сначала необходимо добавить описания к Generic_Interface, что позволяет пользователю сохранить переменную флага Flag:
type Interface_Selector is access constant Flag'Class; end Generic_Interface; |
Константа Selected помещается в каждый пакет интерфейса, избавляя пользователя от необходимости использования синтаксиса, имеющего вид "TYPENAME'(null record)".Кроме того, в каждый пакет интерфейса помещается ссылочная константа для обозначения Selected, которая может быть сохранена в переменной типа Interface_Selector.
Selected: constant Flag := Flag'(null record); Selection: constant Generic_Interface.Interface_Selector := Selected'Access;end Disk_Interface; |
Следует заметить, что эти добавления к пакетам _Interface являются простыми соглашениями. Пользовательский код может осуществлять такие описания самостоятельно.
Теперь, для точного указания используемого интерфейса, код может содержать:
with Disk_Interface; with Extension_Approach; use Extension_Approach;procedure User is Sum: Summary_Type; begin Sum := Summarizer (Disk_Interface.Selected); |
Для инкапсуляции сведений об интерфейсах в одиночном пакете, код может иметь следующий вид:
package Interfaces is Current: Generic_Interface.Interface_Selector; end Interfaces; - - - - - with Interfaces; with Extension_Approach; use Extension_Approach; procedure User is Sum: Summary_Type; begin Sum := Summarizer (Interfaces.Current); . . . |
Теперь, для расширения процедуры User, необходимо добавить еще один класс и немного кода (возможно, помещенного в интерфейс пользователя), который потребуется изменить так, чтобы он мог установить Interfaces.Current в New_Interface.Selection.Все остальные части системы не будут нуждаться в изменении и даже перекомпиляции.
Еще одна особенность программирования посредством расширения в Ada95 заключается в том, что при производстве нового типа, производного от тэгового, можно повторно использовать подпрограммы типа-предка, если они подходят для использования с производным типом.Например, предположим, что некоторые интерфейсы должны явно запускаться и останавливаться.Тогда код может иметь следующий вид:
with Message; package Generic_Interface is type Flag is tagged null record; procedure Get (Which_Interface: Flag; Data: Message.Data_Type); procedure Start (Which_Interface: Flag); -- ничего не выполняет procedure Stop (Which_Interface: Flag); -- ничего не выполняет . . .- - - - - - with Message; package Disk_Interface is type Flag is new Generic_Interface.Flag with null record; procedure Get (Which_Interface: Flag; Data: Message.Data_Type); -- нам нет нужды запускать или останавливать диск, таким образом, Start -- ничего не наследует от Generic_Interface . . .- - - - - - with Message; package Serial_Interface is type Flag is new Generic_Interface.Flag with null record; procedure Get (Which_Interface: Flag; Data: Message.Data_Type); -- запуск и остановка последовательного интерфейса procedure Start (Which_Interface: Flag); procedure Stop (Which_Interface: Flag); . . . |
При этом код пользователя может иметь вид подобный следующему:
with Interfaces; with Extension_Approach; use Extension_Approach; procedure User is Sum: Summary_Type; begin Start (Interfaces.Current); Sum := Summarizer (Interfaces.Current); Stop (Interfaces.Current); . . . |
Рассмотренный пример демонстрирует преимущества средств Ada95
для программирования посредством расширения.Он представляет своеобразный вид "бесконечного варианта"
для получения которого были использованы тэговые типы.Очевидно, что этот пример несколько искуственный.Следовательно этот пример может быть не полностью корректен для порождения реального кода.
Принудительная инициализация
Принудительная инициализация
В объектно-ориентированных языках программирования, которые используют классы (например язык C++), можно принудительно навязать автоматическую инициализацию для всех объектов класса, снабдив класс конструктором, который гарантированно вызывается при создании объекта.Чтобы добиться подобного эффекта в Аде, необходимо при описании объекта указать присваивание начального значения, которое осуществляется в результате вызова функции.Следует заметить, что в данном случае мы не рассматриваем средства, которые предоставляют контролируемые типы Ады.Рассмотрим следующий пример:
package Stacks is type Stack is tagged private; function Init return Stack; -- другие операции . . .private type List is array(0) of Integer; type Stack is record Values : List; Top : Integer range 0; end record; end Stacks;with Stacks; use Stacks; procedure Main is A : Stack := Init; B : Stack;begin null; end Main; |
В этом примере объект A будет надежно инициализирован для представления пустого стека.Причем, для этого можно использовать только функции предусмотренные пакетом Stacks.Выполнить инициализацию такого объекта каким-либо другим способом - нельзя, поскольку его тип - приватный.Однако, не трудно заметить, что объект B - не инициализирован.Таким образом видно, что Ада не предъявляет никаких требований по принудительной инициализации объектов.
Чтобы в подобных случаях возложить контроль за инициализацией объектов на компилятор необходимо использовать дискриминанты.Существует возможность указать наличие дискриминанта в неполном описании типа при фактическом отсутствии дискриминанта в полном описании типа.Поскольку Ада предпочитает ограниченные (constrained) объекты (то есть те объекты, размер которых известен), то применение комбинации из дискриминанта и приватного типа послужит причиной того, что компилятор будет требовать от программиста явной инициализации всех объектов такого типа.
package Stacks is type Stack(<>) is private; -- указание того, что Stack может иметь дискриминант... function Init return Stack; -- другие операции . . .private type List is array(0) of Integer; -- ...даже если вы не стремитесь иметь здесь дискриминант type Stack is record Values : List; Top : Integer range 0; end record; end Stacks;with Stacks; use Stacks; procedure Main is A : Stack := Init; B : Stack; -- теперь это недопустимо! -- вы должны вызвать функцию для инициализации Bbegin null; end Main; |
Подобный подход может показаться несколько интуитивным, однако за счет явности описаний инициализации он повышает общую читабельность исходного текста.
Производный тип как параметр настройки
Производный тип как параметр настройки
Использование производного типа как формального параметра настройки подобно использованию в качестве формального параметра настройки настраиваемого модуля.В этом случае, формальный параметр описывается как тип, производный от указанного предка, подразумевая, что он совпадает с любым типом в классе наследования, корнем которого будет указанный предок.Следует учитывать, что синтаксис для тэговых и не тэговых типов различен.
Указание производного не тэгового типа в качестве формального параметра настройки может иметь следующий вид:
generic -- внутри Module_G, известными для T операциями являются type NT is new T; -- операции для объектов типа NT с неявным package Module_G is ... -- преобразованием представления T в NT |
Указание производного тэгового типа в качестве формального параметра настройки может иметь следующий вид:
generic -- внутри Module_G, известные операции для T индицируют type NT is new T -- диспетчеризуемые операции доступные для объектов типа NT with private; -- причем, все вызовы используют NT-реализацию операций package Module_G is ... |
Формальный параметр производного типа осуществляет параметризацию настраиваемого модуля типом и его примитивными операциями не смотря на то, что указанный тип предка не известен.Подобным образом, в случае передачи настраиваемого абстрактного типа данных как формального параметра настройки (при использовании в качестве формального параметра настройки настраиваемого модуля), множество известных операций связывается вместе с фактическим типом.
В качестве примера рассмотрим тэговый абстрактный тип данных для рациональных чисел.Кто-либо может предусмотреть ввод/вывод таких чисел также как для любого другого типа, производного от этого абстрактного типа данных (например, кому-то может потребоваться оптимизация действий, выполняемых какими-либо знаками операций).Вариантом решения может служить настраиваемый пакет ввода/вывода, чьим формальным параметром настройки будет параметр производного типа, а абстрактный тип данных будет указан как тип предок.Тогда требуемые операции не будут нуждаться в перечислении, как дополнительные формальные параметры настройки, поскольку они определяются типом предка.
Спецификация абстрактного типа данных для рациональных чисел может иметь следующий вид:
package Rational_Numbers is
type Rational_Type is tagged private;
function To_Ratio (Numerator, Denominator : Integer)
return Rational_Type;
-- возбуждает Constraint_Error когда Denominator = 0 function Numerator (Rational : Rational_Type) return Integer;
function Denominator (Rational : Rational_Type) return Positive;
-- результат не нормализован к множителю общих делителей ... -- другие операции: "+", "-", "*", "/", ...
private
... end Rational_Numbers; |
Спецификация настраиваемого пакета ввода/вывода для типов, производных от Rational_Type:
with Rational_Numbers, Text_IO; generic type Num_Type is new Rational_Numbers.Rational_Type with private; package Rational_IO is procedure Get (File : in Text_IO.File_Type; Item : out Num_Type); procedure Put (File : in Text_IO.File_Type; Item : in Num_Type); end Rational_IO; |
Такой вид параметров настраиваемого модуля будет полезен при комбинировании абстракций и построении настраиваемых частей кода, которые будут адаптироваться к любому типу входящему в класс наследования, опираясь на базис свойств, которые применимы ко всем типам класса наследования (то есть известны для корневого типа класса наследования).
Проверка порядка элаборации
Проверка порядка элаборации
В некоторых языках программирования, которые подразумевают наличие аналогичных проблем элаборации (например Java и C++), программист вынужден заботиться о порядке элаборации самостоятельно.В следствие этого, часто встречаются случаи написания программ в которых выбор неправильного порядка элаборации приводит к неожиданным результатам, поскольку при этом используются переменные, значения которых не инициализированы.Язык Ада был разработан как надежный язык и заботы программиста о порядке элаборации не так значительны.В результате, язык предусматривает три уровня защиты:
Стандартные правила элаборации накладывают некоторые ограничения на выбор порядка элаборации.В частности, если какой-либо модуль указан в спецификаторе with, то элаборация его спецификации всегда осуществляется перед элаборацией модуля, который содержит этот спецификатор with.Подобным образом, элаборация спецификации модуля-предка всегда осуществляется перед элаборацией спецификации модуля-потомка, и, в заключение, элаборация спецификации всегда осуществляется перед элаборацией соответствующего тела.
Динамические проверки элаборации осуществляются во время выполнения программы.Таким образом, если доступ к какой-либо сущности осуществляется до ее элаборации (обычно это происходит при вызовах подпрограмм), то возбуждается исключение Program_Error.
Средства управления элаборацией позволяют программисту явно указывать необходимый порядок элаборации.
Рассмотрим перечисленные возможности более детально.Во-первых, правила динамической проверки.Одно из правил заключается в том, что при попытке использования переменной, элаборация которой не была выполнена, возбуждается исключение Program_Error.Недостатком такого подхода является то, что затраты производительности на проверку каждой переменной могут оказаться достаточно дорогостоящими.Вместо этого, Ada95 поддерживает два несколько более строгих правила, соблюдение которых легко проверить:
Ограничения для вызовов подпрограмм - Какая-либо подпрограмма может быть вызвана в процессе элаборации только в случае, когда осуществлена элаборация тела этой подпрограммы.Такое правило гарантирует, что элаборация спецификации подпрограммы будет выполнена перед вызовом подпрограммы, а не перед элаборацией тела.При нарушении этого правила возбуждается исключение Program_Error.
Ограничения для конкретизации настраиваемых модулей - Настраиваемый модуль может быть конкретизирован только после после осуществления элаборации тела настраиваемого модуля.Такое правило гарантирует, что элаборация спецификации настраиваемого модуля будет выполнена перед конкретизацией настраиваемого модуля, а не перед элаборацией тела.При нарушении этого правила возбуждается исключение Program_Error.
Смысл идеи заключается в том, что при осуществлении элаборации тела элаборация любых переменных, которые используются в этом теле, должна быть уже выполнена.Таким образом, при проверке элаборации тела гарантируется, что ни один из ресурсов, которые использует это тело, не окажется источником каких-либо неожиданностей.Как было замечено ранее, такие правила несколько более строгие, поскольку необходима гарантия корректности вызова подпрограммы, которая использует в своем теле не локальные для этой подпрограммы ресурсы.Однако, полностью полагаться на эти правила будет не безопасно, поскольку это подразумевает, что вызывающая подпрограмма должна заботится о деталях реализации, которые размещенны в теле, что противоречит основным принципам Ады.
Вероятная реализация такой идеи может выглядеть следующим образом.С каждой подпрограммой и с каждым настраиваемым модулем может быть ассоциирована логическая переменная.Первоначально такая переменная имеет значение "ложь" (False), а после осуществления элаборации тела эта переменная устанавливается в значение "истина" (True).Проверка значения этой переменной выполняется при осуществлении вызова или конкретизации, и когда значение переменной - "ложь" (False), возбуждается исключение Program_Error.
Рекомендации по построению абстракций
Рекомендации по построению абстракций
Как уже говорилось, пакет Ады является инструментом абстракции данных.В этом случае пакет содержит какой-либо главный тип, и такая конструкция очень подобна классам, используемым в других объектно-ориентированных языках программирования.Чтобы избежать неоднозначностей (напомним, что понятие класса в Аде отличается от понятия класса в других объектно-ориентированных языках), мы называем такую конструкцию абстракцией.В большинстве подобных случаев, главный тип описан как приватный тип или тип, имеющий приватное расширение.Спецификация пакета также может содержать описания других типов, мы будем называть их обычными типами.Эти типы, как правило, используются для построения интерфейса (когда они описаны публично), или для представления внутренних структур данных (когда они описаны приватно).
Простой демонстрацией подобного подхода построения абстракции может служить следующий схематический пример:
with Angle; package Coordinates is type Object is ... -- главный тип абстракции -- (приватный или с приватным расширением) type Geographic is -- обычный тип (описан публично) record Latitude : Angle.Radian; Longitude : Angle.Radian; end record;private . . .end Coordinates; |
Подобный подход является базисом.При этом, программисту предоставляется широкий выбор: должен-ли он использовать тэговые типы? контролируемые типы? ...
Родственное наследование
Родственное наследование
В дополнение к показанному выше способу, можно использовать несколько специализированный способ родственного наследования (sibling inheritance), с применением ссылочных дискриминантов.Такой способ используется в случаях когда тип действительно является производным от более чем одного типа предка, или когда клиенты типа требуют представление этого типа как их предка или какого-либо потомка их типа предка.
Основная идея родственного наследования заключается в том, чтобы выразить эффект множественного наследования путем ретрансляции одиночного наследования.Обычно, это заключается в идее представления абстракции, которая осуществляет наследование, можно сказать, от двух суперабстракций, путем установки взаимосвязанных объектов.
В терминах типов, такая идея рассматривает концептуальный тип C, производный от двух типов A и B, как множество типов C_A и C_B, соответственно производных от A и B. Объекты концептуального типа C создаются путем совместной генерации множества объектов типа C_A и типа C_B.Такие объекты называют родственными объектами (sibling objects) или сдвоенными объектами (twin objects).Они взаимосвязаны между собой таким образом, что множество объектов может управляться так будто они являются одним объектом, и так, что один объект обладает доступом к свойствам другого сдвоенного объекта.Они также доступны индивидуально.Таким образом, легко доступно частичное представление множества, которое соответствует отдельному объекту.
В качестве примера, рассмотрим создание концептуального типа Controlled_Humans, производного от стандартного типа Controlled и типа Human.Компонент First_Name - это ссылочный тип, а не строка фиксированной длины.Таким образом, имя может иметь динамически изменяемую длину.В данном случае используются средства контролируемых объектов для освобождения памяти ассоциируемой с First_Name при разрушении объекта типа Human.Спецификация типа Human может иметь подобный вид:
package Dynamic_Humanity is type String_Ptr is access String; type Human is tagged limited record First_Name: String_Ptr; end record; procedure Christen (H: in out Human; N: in String_Ptr); -- устанавливает имя для Human ... end Dynamic_Humanity; |
Для построения комбинации из этих двух типов, необходимо создать два родственных типа Human_Sibling и Controlled_Sibling, путем соответственного производства от Human и Limited_Controlled.Два этих типа, вместе формируют концептуальный тип Controlled_Humans:
with Dynamic_Humanity, Ada.Finalization;package Controlled_Human is type Human_Sibling; -- ввиду взаимозависимости типов, необходимо использование -- неполного описания типа type Controlled_Sibling (To_Human_Sibling: access Human_Sibling) is new Ada.Finalization.Limited_Controlled with null record; -- тип To_Human_Sibling является связкой с Human_Sibling procedure Finalize (C: in out Controlled_Sibling); type Human_Sibling is new Dynamic_Humanity.Human with record To_Controlled_Sibling: Controlled_Sibling (Human_Sibling'Access); -- To_Controlled_Sibling является связкой с Controlled_Sibling. -- Этот компонент автоматически инициализируется ссылкой -- на текущее значение экземпляра Human_Sibling end record; -- примитивные операции типа Human (могут быть переопределены)end Controlled_Human; |
Таким образом, эти типы - взаимосвязаны, и для обращения от одного сдвоенного объекта к другому:
Human_Sibling обладает компонентом связки с To_Controlled_Sibling
который подходит к типу Controlled_Sibling.
Controlled_Sibling обладает компонентом связки с To_Human_Sibling
который подходит к типу Human_Sibling.
Следует заметить, что эти связки имеют различную природу.Связка To_Controlled_Sibling - это связка членства: любой объект типа Controlled_Sibling заключен в каждый объект типа Human_Sibling.Связка, To_Human_Sibling - это ссылочная связка: To_Human_Sibling обозначает Human_Sibling.
В результате, существует возможность в любой момент получить представление любого из сдвоенных объектов посредством связки.
Хотя требуются некоторые дополнительные затраты (такие как, получение значения объекта, к которому отсылает ссылочное значение), такой подход обеспечивает всю функциональность, которая требуется от множественного наследования:
Последовательная генерация объектов
Поскольку To_Controlled_Sibling - это компонент Human_Sibling,
любой объект типа Controlled_Sibling каждый раз создается автоматически
при создании объекта типа Human_Sibling.Связка To_Controlled_Sibling автоматически инициализируется ссылкой
на заключенный объект, поскольку атрибут 'Access применяется к имени
типа записи, внутри описания, автоматически обозначая текущий экземпляр типа.Как результат, описание:
CH: Human_Sibling;
автоматически объявляет объект концептуального типа Controlled_Human
Переопределение операций
Примитивные операции могут быть переопределены тем сдвоенным типом который их реализует
(например, Finalize).
Например, для предотвращения утечки памяти, мы переопределяем Finalize
для автоматической очистки памяти, используемой First_Name,
когда Human становится не доступным:
package body Controlled_Human is procedure Free is new Unchecked_Deallocation (String_Ptr); procedure Finalize (C: in out Controlled_Sibling) is -- overrides Finalize inherited from Controlled begin Free (C.To_Human_Sibling.all.First_Name); end Finalize;end Controlled_Human; |
Компоненты каждого сдвоенного объекта могут быть выбраны и использованы любым из сдвоенных объектов, используя связки. Например, операция Finalize (описанная для Controlled_Sibling) может использовать компонент First_Name (описанный для Human_Sibling).
Примитивные операции могут быть вызваны тем же самым способом.
Добавление свойств
К концептуальному типу могут быть легко добавлены
новые свойства, компоненты и операции, путем расширения любого из сдвоенных объектов.
Для сохранения инкапсуляции, сдвоенные типы могут быть также описаны в приватной части расширения.
Расширение типа
Концептуальный тип может быть использован как предок для других типов.Это важно при производстве от сдвоенного типа Human_Sibling,
который содержит компоненты другого сдвоенного типа.Свойства другого сдвоенного типа также доступны для производства
через связку To_Human_Sibling.
Проверка принадлежности
Проверка принадлежности объекта к любому из типов предков
выполняется путем использования согласованного (надклассового) представления
любого из сдвоенных объектов:
declare CH: Human_Sibling; -- simultaneous object generationbegin ... CH in Human'Class ... -- True ... CH.To_Controlled_Sibling.all in Limited_Controlled'Class ... -- True end; |
Любой объект концептуального типа может быть присвоен объекту обоих типов предков обычным образом, то есть, путем выбора в концептуальном объекте сдвоенного объекта, который совпадает по типу с требуемым предком (используя преобразование представления), и присваивая этот сдвоенный объект назначению операции присваивания.
Такая модель может быть легко расширена для управления множественным наследованием от более чем двух типов.
Смешанное наследование
Смешанное наследование
Смешанное наследование является формой множественного наследования при котором одна или две комбинированных абстракции выступают в роли шаблонов (subpattern).Любой подобный шаблон предоставляет множество свойств (компонентов и операций) способных присутствовать в различных, иначе не связанных, типах, самостоятельное использование которых не достаточно для описания более чем одного абстрактного типа.
Построение шаблонов может быть выполнено без явного множественного наследования, путем использования одиночного наследования и средств предоставляемых настраиваемыми модулями: Шаблон абстрагируется в производную абстракцию посредством сборки множества свойств, то есть добавлением значений в расширение типа.Такой смешанный тип (mixin type) может быть использован для специализации общего поведения типа. Настраиваемый пакет используется как смешанный пакет (mixin package), который инкапсулирует смешанный тип.Этот пакет имеет формальный параметр настройки тэгового типа, который, в свою очередь, играет роль типа-предка к которому добавляются новые свойства.
Смешивание достигается при конкретизации смешанного пакета путем расширения фактического параметра тэгового типа производным абстрактным типом.При множественном наследовании, производная абстракция установится в обратном направлении иерархии наследования, как тип предок (абстрактный) от которого производится наследование.Этот способ называют смешанным наследованием (mixin inheritance).
Например, не смотря на множество свойств, которые описывают степень образованности, хорошо известно, что "образованность" сама по себе - не существует.Подобный шаблон может быть применен по отношению к людям, которые полностью закончили обучение в каком-либо учебном заведении, что и определяет степень (degree) образованности (graduation).Очевидно, что по отношению к людям, понятие "степень образованности" являться свойством, но самостоятельное использование понятия "степень образованности" - бессмысленно.Свойства, которые описывают степень образованности, могут быть упакованы в смешанный пакет Graduate_G:
with Degrees; use Degrees; -- экспортирует тип Degreegeneric type Person is tagged private;package Graduate_G is -- тип Graduate содержит Degree type Graduate is new Person with private; procedure Give (G: in out Graduate; D: in Degree); function Degree_of (G: Graduate) return Degree;private type Graduate is new Person with record Given_Degree: Degree; end record;end Graduate_G; |
При конкретизации этого пакета с формальным параметром настройки тэгового типа Woman, будет создан новый тип, производный от типа указанного при конкретизации.Этот новый тип будет обладать желаемыми свойствами:
package Graduate_Women is new Graduate_G (Woman);Anne : Graduate_Women.Graduate; -- тип полученный в результате -- конкретизации Graduate_G D : Degrees.Degree :=...Christen (Somebody, "Anne"); -- операция типа Woman Give (Somebody, Degree); -- операция типа Graduate |
Свойства обоих типов, типа-предка Woman и смешанного типа Graduate, будут доступны для нового типа Graduate_Woman.Graduate.
Тип-предок для смешанного типа (тип формального параметра настройки) может быть ограничен принадлежностью к иерархии наследования какого-либо класса.Такое ограничение осуществляется для обеспечения гарантии того, что тип-предок обладает требуемым множеством свойств, которые необходимы для реализации смешанного типа, или для гарантирования логичности смешанного типа.Например, будет абсолютно бессмысленно создавать стек степени образованности.
Ограничение для типа предка может быть выражено указанием класса наследования к которому обязан принадлежать производный тип формального параметра настройки (вместо тэгового типа формального параметра настройки).Следующий пример демонстрирует, что только тип Human и его потомки могут быть использованы для конкретизации смешанного настраиваемого модуля:
with Degrees; use Degrees;generic type Person is new Human with private;package Graduate_G is type Graduate is new Person with private; ... -- описания Give и Degree_of те же, что и в предыдущем примере function Title_of (G: Graduate) return String; -- переопределение функции Title_of -- для получения требуемого заголовкаprivate ... end Graduate_G; |
Поскольку свойства типа предка известны, то позже, в случае необходимости, ограниченный смешанный тип может быть переопределен.Так, например, функция Title_of переопределяется для предоставления более подходящей реализации.
Советы Паскаль-программистам
Советы Паскаль-программистам
Ада является языком программирования, который во многих аспектах подобен языку Паскаль.Однако, Ада не является "супермножеством" средств языка Паскаль.Таким образом, синтаксис инструкций Ады несколько отличается от синтаксиса инструкций языка Паскаль и множество средств языка Паскаль реализовано в Аде несколько иначе.Здесь приводится некоторое обобщение различий языков программирования Паскаль и Ада, которое может быть полезно для программистов которые хорошо знают язык Паскаль и желают изучить язык Ада.
Самое важное отличие Ады от Паскаля заключается в том, что Ада строжайшим образом стандартизированный язык программирования, дополнительно сопряженный с процессом сертификации компилятора.Таким образом, Ада свободна от наличия различных синтактических расширений, которые можно встретить, практически, в каждой Паскаль-системе.С другой строны, стандартно описанный язык программирования Ада содержит почти все средства и особенности современных расширений языка Пасскаль.
Совместимость типов и подтипов
Совместимость типов и подтипов
Важно помнить и понимать, что Ада использует именную, а не структурную эквивалентность типов.Например, пусть даны следующие описания:
A, B: array(0) of Float; C : array(0) of Float; |
Тогда, следующие инструкции присваивания:
A := B; C := B; |
будут недопустимыми, поскольку все три массива имеют различные анонимные типы, назначенные компилятором (некоторые компиляторы Паскаля будут позволять осушествление первого присваивания).Для обеспечения возможности присваивания массивов, необходимо явное указание имени для типа массива:
type List is array(0) of Float;A, B: List; C: List; |
Теперь оба показанных выше присваивания будут допустимы.При программировании на Аде, настоятельно рекомендуется избегать использования анонимных типов, подобно тому как это делается в Паскале.
Создание абстракций из настраиваемых абстракций
Создание абстракций из настраиваемых абстракций
Достаточно часто встречаются случаи, когда настраиваемый пакет, который предоставляет подходящую структуру данных, может быть использован для построения реализации другой абстракции.
Предположим, что существует пакет Generic_Lists спецификация которого имеет следующий вид:
generic type Element is private;package Generic_Lists is type List is private; Underflow : exception; procedure Insert_At_Head(Item : in out List; Value : in Element); procedure Remove_From_Head(Item : in out List; Value : out Element); procedure Insert_At_Tail(Item : in out List; Value : in Element); procedure Remove_From_Tail(Item : in out List; Value : out Element); function Full(Item : List) return Boolean; function Empty(Item : List) return Boolean; -- возвращает пустой инициализированный список function Init return List;private . . .end Generic_Lists; |
Мы можем конкретизировать этот настраиваемый модуль для создания экземпляра нового пакета, который будет сохранять для нас значения типа Integer.Необходимо обратить внимание на то, что конкретизацию такого настраиваемого модуля следует осуществлять в приватной секции пакета.Это позволяет избежать нарушения абстракции и предотвращает возможность появления каких-либо дополнительных трудностей разработки.
После этого, настраиваемый модуль может быть использован следующим образом:
with Generic_Lists;package Stacks is type Stack is private; Underflow : exception; procedure Push(Item : in out Stack; Value : in Integer); procedure Pop(Item : in out Stack; Value : out Integer); function Full(Item : Stack) return Boolean; function Empty(Item : Stack) return Boolean; -- возвращает пустой инициализированный стек function Init return Stack;private -- конкретизация настраиваемого модуля для получения нового пакета package Lists is new Generic_Lists(Integer); type Stack is new Lists.List; -- как и раньше, тип Stack наследует все операции типа Listend Stacks; |
Тело пакета Stack будет таким же как и описывалось ранее.Следует заметить, что в данном случае мы можем навязать приватность без изменения того, что тип выполняет для клиента.
Создание и удаление объектов
Создание и удаление объектов
Если абстракция описывает функцию Create, которая возвращает экземпляр объекта главного типа абстракции, то все потомки этой абстракции будут наследовать такую операцию.Это может быть опасно в случаях, когда абстракция-потомок имеет больше атрибутов чем абстракция-родитель.Действительно, унаследованная от предка функция Create может не инициализировать дополнительные атрибуты абстракции-потомка.В концепции Ada 95 предлагаются два варианта решения для предотвращения наследования функции Create: вместо возвращения экземпляра объекта главного типа абстракции, функция Create возвращает ссылку на объект этого типа функция Create описывается во внутреннем или дочернем пакете
К сожалению, ни одно из этих решений нельзя назвать полностью удовлетворительным.
Первое решение заставляет выполнять явную деаллокацию каждого экземпляра объекта, который больше не используется.В некоторых случаях деаллокация просто не возможна, то есть
A := Create (...).all; |
Второе решение затрудняет автоматическую генерацию кода.Кроме того, оно вызывает принудительное копирование объектов после их создания (минимум однократно), что является дополнительным расходом производительности.
К счастью, в Аде реально не требуется явная подпрограмма Create.Любой экземпляр объекта абстракции создается автоматически, или при его описании (статический экземпляр), или с использованием динамического размещения:
Instance := new Class.Object |
Следовательно, должна быть описана только подпрограмма инициализации.Если инициализация принятая по умолчанию не удовлетворяет потребностям, то могут быть описаны новые подпрограммы инициализации (с дополнительными параметрами или без).Чтобы такие подпрограммы были не наследуемыми, они должны принимать в качестве главного параметра объект надклассового типа ('Class).Для простых абстракций, вместо Initialize, может быть использован метод Set_<<Attribute_Name>>.
Для удаления объекта абстракции, когда главный тип абстракции - контролируемый, подпрограмма Delete может быть безболезненно заменена на Finalize.В случае когда главный тип абстракции - не контролируемый, необходимость в подпрограмме Delete - отсутствует.
Сравнение пакетов и классов
Сравнение пакетов и классов
Необходимо обратить внимание на то, что использование пакетов Ады, как инструмента абстракции данных, подобно классам в других объектно-ориентированных языках программирования, где в качестве средства абстракции данных используется класс, определяющий тип данных, множество операций и действия, необходимые для инициализации.
При сравнении подобного использования пакетов Ады с классами можно обнаружить следующее: При описании объекта (переменной) с помощью типа класс она будет автоматически инициализирована с помощью действий инициализации класса.Определенные для такого объекта операции обозначаются именем объекта, за которым указывается имя операции. При использовании для реализации абстрактного типа данных пакета, инициализация выполняется явным вызовом процедуры инициализации, а имя каждого абстрактного объекта передается как параметр подпрограммы (операции) пакета.
Таким образом, класс имеет вид более чистой реализации абстрактного типа,
однако он требует некоторых дополнительных затрат производительности.Класс обеспечивает неявное дублирование объектов,
предоставляя (по крайней мере в принципе)
новое множество операций для каждого объекта,
генерируемого классом.Пакет нельзя дублировать,
и поэтому все операции пакета одни и те же для всех объектов.При сравнении пакетов и классов доводы в пользу классов,
как правило, основаны на эстетических соображениях.Практически
пакеты Ады полностью соответствуют требованиям реализации абстрактных типов данных,
и, кроме того, имеют по сравнению с классами множество других применений.
Средства Ады для работы с абстрактными типами данных
Средства Ады для работы с абстрактными типами данных
Ада предоставляет набор средств, которые обеспечивают поддержку разработки и использования абстрактных типов данных.Такими средствами являются: Подтипы - позволяют описывать классы численных и перичислимых значений, и назначать им ограничения диапазонов значений, что позволяет компилятору осуществлять строгий контроль над корректностью используемых значений. Инициализация полей записи - позволяет описывать тип записи так, что каждое индивидуальное поле записи, в каждой переменной этого типа, будет предварительно инициализировано предопределенным значением. Пакеты - являются идеальным средством группирования совместно используемых ресурсов (типы, процедуры, функции, важные константы...) и предоставления этих ресурсов клиентским программам.При этом осуществляется строгий контроль над контрактной моделью пакета: все что обещано в спецификации пакета должно быть обеспечено в теле пакета, а клиентская программа должна корректно использовать ресурсы предоставляемые пакетом (например, вызывать процедуры только с корректными параметрами). Приватные типы - позволяют разрабатывать пакеты, которые предоставляют клиентским программам новый тип данных таким образом, чтобы клиентская программа не имела возможности выполнить непреднамеренное изменение значений путем использования приватной информации о внутреннем представлении данных приватного типа, ограничивая тем самым возможность использования внутреннего представления приватного типа данных только внутри тела пакета. Совмещение знаков операций - позволяет определять новые арифметические знаки операций и знаки операций сравнения для вновь определяемых типов данных, и использовать их также как и предопределенные знаки операций. Определяемые пользователем исключения - позволяют разработчику пакета предусматривать исключения для клиентской программы, которые будут сигнализировать клиентской программе о том, что внутри пакета выполнены какие-либо неуместные действия.В свою очередь, разработчик клиентской программы может предусмотреть обработку исключений, которые могут возникнуть внутри используемого клиентской программой пакета. Атрибуты - позволяют создавать подпрограммы для манипуляции структурами данных без наличия всех деталей организации данных (например, атрибуты 'First и 'Last для массивов). Настраиваемые модули - позволяют создавать подпрограммы и пакеты, которые имеют более общий характер.Это подразумевает, что в процессе написания настраиваемого модуля отсутствует информация о фактически обрабатываемом типе данных.Тип данных может быть указан как параметр настройки позже, в процессе конкретизации настраиваемого модуля.
Структура абстрактного типа данных
Структура абстрактного типа данных
В настоящее время, абстрактный тип данных является одной из общих концепций программирования, вне зависимости от используемого языка программирования.Как правило, абстрактный тип данных состоит из спецификации одного или более типов данных и множества операций над типом или типами.В общем случае, абстрактный тип данных - это составной тип данных, как правило, запись.Операции, которые выполняются над абстрактным типом данных, могут быть логически разделены на несколько групп: Конструкторы, которые выполняют создание (или построение) объекта абстрактного типа данных путем объединения отдельных компонентов в единое целое Селекторы, которые осуществляют выборку какого-либо отдельного компонента абстрактного типа данных Операции опроса состояния абстрактного типа данных Операции ввода/вывода
Структуры управления
Структуры управления
Все структуры управления последовательностью выполнения (иначе, управляющие структуры) Ады имеют соответствующие закрывающие инструкции, такие как "if ... end if", "loop ... end loop", "case ... end case". Далее, в Аде символ двоеточия ';' используется для завершения инструкции, а не для разделения инструкций, как в Паскале.Это обеспечивает синтаксис, который, по сравнению с синтаксисом Паскаля, легче использовать корректно.Например, инструкция Паскаля:
if X < Y then A := B; |
будет написана в Аде следующим образом:
if X < Y then A := B; end if; |
А инструкция Паскаля:
if X < Y then begin A := B; Z := X end else begin A := X; Z := B end; |
будет написана в Аде следующим образом:
if X < Y then A := B; Z := X; else A := X; Z := B; end if; |
Использование подобного синтаксиса в Аде гарантирует остутствие "висячих" else.
Переменные управляющие циклами for всегда описываются неявно, что является единственным исключением из правил, требующих чтобы все было описано явно.Счетчик цикла for локален для тела цикла.Описание счетчика цикла for как переменной, так как это принято в Паскале, не наносит реального ущерба, но описывает самостоятельную переменную, которая спрятана за фактическим счетчиком цикла и, следовательно, не видима в теле цикла.
Диапазоны, для цикла for, часто назначаются как имена типов или подтипов, подобно следующему:
for Count in Index'Range loop |
Ада не имеет структуры цикла repeat.Вместо этого используется "loop ... end loop" с инструкцией выхода из цикла "exit when" в конце тела цикла.
Переменная выбора в инструкции case должна быть дискретного (целочисленного или перечислимого) типа.Различные альтернативы выбора case должны указывать все возможные значения переменной выбора в неперекрывающейся манере.
Сущность абстрактного типа данных
Сущность абстрактного типа данных
Суть абстракции данных заключается в определении типа данных как множества значений и множества операций для манипулирования этими значениями.Таким образом, какой-либо абстрактный тип данных является простым формальным именем типа для которого определено множество допустимых значений и множество допустимых операций.
Программу, которая использует абстрактный тип данных, называют клиентской программой.Такая программа может описывать объекты абстрактного типа данных и использовать различные операции над этим типом данных.Причем, клиентская программа не нуждается в детальной информации как о внутреннем представления типа данных, так и о фактической реализации операций над этим типом данных.Все эти детали могут быть скрыты от клиентской программы.Таким образом достигается разделение использования типа данных и операций над ним от деталей внутреннего представления данных и реализации операций.Другими словами, такой подход позволяет отделить то что предоставляет определенный тип данных в качестве сервиса от того как этот сервис реализован.
Такой подход предоставляет определенные преимущества, позволяя осуществлять независимую разработку как клиентской программы, так и абстрактного типа данных.Например, изменение реализации какой-либо операции над абстрактным типом данных не требует внесения изменений в клиентскую программу.Кроме того, мы можем изменять представление внутренней структуры данных абстрактного типа данных без необходимости изменения клиентской программы.
Абстрактный тип данных является одним из видов повторно используемого программного компонента, который может быть использован большим количеством клиентских программ.Абстрактный тип данных не нуждается в информации о клиентской программе, которая его использует, а клиентская программа не нуждается в информации о внутреннем устройстве абстрактного типа данных.Таким образом абстрактный тип данных можно рассматривать как своеобразный "черный ящик".
Использование абстрактных типов данных облегчает построение крупных программных проектов, поскольку они, как правило, располагаются в библиотеках программных ресурсов, что позволяет избавиться от необходимости бесконечно "изобретать колесо".
"Сюрпризы" численных типов
"Сюрпризы" численных типов
Достаточно часто распространены случаи, когда программист, который только начинает знакомиться с языком программирования Ада, попадается в ловушку численных типов.Рассмотрим следующий пример программы:
with Ada.Text_IO;procedure Numeric is type My_Integer_Type is new Integer range ; My_Integer_Variable : My_Integer_Type := 2; Standard_Integer_Variable : Integer := 2; Result_Variable : Integer;begin Result_Variable := Standard_Integer_Variable * My_Integer_Variable; Ada.Text_IO.Put_Line ("Result_Variable = " & Result_Variable'Img); end Numeric; |
Хотя на первый взгляд все выглядит достаточно логично, данная программа компилироваться не будет.Причиной ошибки будет тип My_Integer_Type, или точнее - отсутствие описания знака операции умножения "*", который одновременно воспринимает левый операнд, стандартного целочисленного типа Integer, и правый операнд, целочисленного типа My_Integer_Type.Такое поведение компилятора Ады продиктовано требованиями строгой именной эквивалентности типов.
Чтобы решить возникшую проблему, необходимо описать тип My_Integer_Type в самостоятельном пакете и описать соответствующие знаки операций, которые будут допустимы для использования над значениями данного целочисленного типа.Вполне естественно, что подобный вариант решения проблемы малопривлекателен, а подобная строгость типизации может показаться чрезмерной даже приверженцам языка Паскаль.
Следует заметить, что существует альтернативный вариант, который гораздо проще и удобнее.Следует описать My_Integer_Type не как тип, производный от типа Integer, а как подтип типа Integer с ограниченным диапазоном значений.После таких изменений наш пример программы будет иметь слудующий вид:
with Ada.Text_IO;procedure Numeric is subtype My_Integer_Type is Integer range ; My_Integer_Variable : My_Integer_Type := 2; Standard_Integer_Variable : Integer := 2; Result_Variable : Integer;begin Result_Variable := Standard_Integer_Variable * My_Integer_Variable; Ada.Text_IO.Put_Line ("Result_Variable = " & Result_Variable'Img); end Numeric; |
В данном случае процедура Numeric будет успешно откомпилирована компилятором.
Тэговые типы - не для всех абстракций!
Тэговые типы - не для всех абстракций!
Первое правило - очень простое: не следует использовать тэговые типы для очень простых абстракций, таких как расстояние, угол, температура, и так далее:
во-первых, такие абстракции нет смысла расширять. во-вторых, такие абстракции достаточно интенсивно используются, а тэговые типы требуют дополнительные затраты производительности в третьих, если тип описан как тэговый и вы порождаете от него другой тип, то вам потребуется переопределить все примитивные операции, такие как "+" и "-".
Тэговый тип как параметр настройки
Тэговый тип как параметр настройки
Поскольку стандарт Ada95 различает два вида типов: тэговые и не тэговые, - то бывают случаи когда при работе с настраиваемыми модулями их необходимо различать.Подобная необходимость более точного указания свойств ожидаемого формального параметра настройки (формальный тип тэговый или нет) может возникнуть при желании более строгого определения контрактной модели настраиваемых модулей.Указание тэгового типа как формального параметра настройки может иметь следующий вид:
generic type T is tagged private; package Module_G is ... |
В случае использования тэговых типов как формальных параметров настройки особый интерес представляют конструкции для расширения типа и для надклассового программирования.Конструкция для расширения типа позволяет добавлять новые компоненты, и является основой для множественного наследования.Конструкции для надклассового программирования позволяют предоставить подпрограммы, которые применимы к T'Class, или описать типы надклассовых ссылок (подобные описания будут точно адаптированы для любого типа в указанном классе наследования при конкретизации).
generic type T is tagged private; package Module_G is type NT is new T -- расширение типа with record B : Boolean; end record; function Equals (Left, Right : T'Class) -- надклассовая подпрограмма return Boolean; type T_Poly_Ref is -- тип надклассовой ссылки access T'Class; end Module_G; |
Типы и структуры данных
Типы и структуры данных
Двумерные массивы не являются массивами массивов.Следовательно, A(J)(K) не является тем же самым, что и A(J,K).Разработчики, в действительности, относят последний из двумерных массивов к массивам массивов.Смысл этого заключается в том, что существуют различные структуры Ады для которых стандарт не определяет отображение на хранение многомерного массива в памяти (ориентирование на строку или на столбец).Например, это позволяет предусмотрительным реализаторам Ада-систем использовать нелинейное отображение.На практике, большая часть существующих Ада-компиляторов использует отображение ориентирование на строку, соответствующее правилам Паскаля и Си.
В качестве типов полей записи всегда должны использоваться имена типов или подтипов, что означает, что поле записи не может иметь анонимный тип, подобно array или record.Для построения иерархических типов записей, сначала необходимо построить типы записей нижнего уровня, а затем использовать имена этих типов для описания полей записей более высого уровня.
Ада не обеспечивает какого-либо средства, которое соответствует with в Паскале.Это значит, что все обращения к элементам массивов и записей должны использовать полную точечную нотацию.
Ада более жестко контролирует использование записей с вариантами чем Паскаль.Таким образом, нет возможности описать "свободное объединение", или вариантную запись без указания дискриминанта.В Паскале и Си, свободные объединения часто используются для обхода проверки типа, но такой подход не может быть использован в Аде (Ада имеет настраиваемую функцию Unchecked_Conversion, которая может быть использована для действительного обхода проверки типа).
В отличие от Паскаля, Ада не имеет предопределенного средства построения типа множества set.Однако, средства Ады обеспечивают программисту возможность построить подобный тип самостоятельно.
Управление порядком элаборации
Управление порядком элаборации
Выше мы обсудили правила согласно которых возбуждается исключение Program_Error, когда выбран неправильный порядок элаборации.Это предотвращает ошибочное выполнение программы, но Ада также предоставляет механизмы, которые позволяют явно указавать необходимый порядок элаборации и избежать возбуждение исключения.Следует заметить, что при разработке небольших программных проектов, как правило, нет необходимости вмешиваться в последовательность элаборации множества программных модулей.Однако, при разработке больших систем, такое вмешательство бывает необходимым.
Во-первых, существует несколько способов указать компилятору, что данный модуль потенциально не содержит никаких проблем связанных с элаборацией:
Пакеты, которые не требуют наличия тела.
Ада не допускает наличие тела для библиотечного пакета, который не требует наличия тела.Это подразумевает, что можно иметь пакет подобный следующему:
package Definitions is generic type m is new integer; package Subp is type a is array (1 .. 10) of m; type b is array (1 .. 20) of m; end Subp; end Definitions; |
В данном случае, пакет, который указывает в спецификаторе with пакет Definitions, может безопасно конкретизировать пакет Definitions.Subp поскольку компилятор способен определить очевидное отсутствие тела
Директива компилятора pragma Pure
Данная директива накладывает строгие ограничения на модуль, гарантируя, что обращение к любой подпрограмме модуля не повлечет за собой никаких проблем элаборации.Это подразумевает, что компилятору нет нужды заботиться о порядке элаборации подобного модуля, в частности, нет необходимости осуществлять проверку обращений к подпрограммам в этом модуле.
Директива компилятора pragma Preelaborate
Данная директива накладывает несколько менее строгие ограничения на модуль чем директива Pure.Однако эти ограничения остаются достаточно значительными, чтобы гарантировать отсутствие проблем при вызове подпрограмм модуля.
Директива компилятора pragma Elaborate_Body
Данная директива требует, чтобы элаборация тела модуля была осуществлена сразу после элаборации спецификации.Предположим, что модуль A содержит данную директиву, а модуль B указывает в спецификаторе with модуль A.Напомним, что стандартные правила требуют, чтобы элаборация спецификации модуля A была выполнена перед элаборацией модуля, который указывает модуль A в спецификаторе with.Указание этой директивы компилятора в модуле A говорит о том, что элаборация тела модуля A будет выполнена перед элаборацией B.Таким образом, обращения к модулю A будут безопасны и не нуждаются в дополнительных проверках.
Примечательно, что в отличие от директив Pure и Preelaborate использование директивы Elaborate_Body не гарантирует, что программа свободна от проблем связанных с элаборацией, поскольку возможно наличие ситуации, которая не удовлетворяет требования порядка элаборации.Вернемся к примеру с модулями Unit_1 и Unit_2.Если поместить директиву Elaborate_Body в Unit_1, а модуль Unit_2 оставить без изменений, то порядок элаборации будет следующий:
Spec of Unit_2 Spec of Unit_1 Body of Unit_1 Body of Unit_2 |
В этом случае подразумевается, что нет необходимости проверять вызов Func_1 из модуля Unit_2 поскольку он должен быть безопасным.Вызов Func_2 из модуля Unit_1, когда результат вычисления выражения Expression_1 равен 1, может быть ошибочным.Следовательно, ответственность за отсутствие подобной ситуации возлагается на программиста.
Когда все модули содержат директиву компилятора Elaborate_Body, то проблемы порядка элаборации отсутствуют, кроме случаев когда вызовы подпрограмм осуществляются из тела, забота о котором в любом случае возлагается на программиста.Следует заметить, что повсеместное использование этой директивы не всегда возможно.В частности, для показанного ранее примера с модулями Unit_1 и Unit_2, когда оба модуля содержат директиву Elaborate_Body, отсутствие возможности определить правильный порядок элаборации очевидно.
Показанные выше директивы компилятора позволяют гарантировать клиентам безопасное использование серверов, и такой подход является предпочтительным.Следовательно, маркирование модулей как Pure или Preelaborate, если это возможно, является хорошим правилом.В противном случае следует маркировать модули как Elaborate_Body.Однако, как мы уже видели, существуют случаи, когда эти три директивы компилятора не могут быть использованы.Таким образом, для клиентов предусмотрены дополнительные методы управления порядком элаборации серверов от которых эти клиенты зависят:
Директива компилятора pragma Elaborate (unit)
Эта директива помещается после указания спецификатора with, и она требует, чтобы элаборация тела указанного модуля unit осуществлялась перед элаборацией модуля в котором указывается эта директива.Эта директива используется когда в процессе элаборации текущий модуль вызывает, прямо или косвенно, какую-либо подпрограмму модуля unit.
Директива компилятора pragma Elaborate_All (unit)
Это более строгая версия директивы Elaborate.Рассмотрим следующий пример:
Модуль A указывает в with модуль B и вызывает B.Func в процессе элаборации Модуль B указывает в with модуль C, и B.Func вызывает C.Func |
Если поместить директиву Elaborate (B) в модуль A, то это гарантирует, что элаборация тела B будет выполнена перед вызовом, однако элаборация тела C перед вызовом не выполняется.Таким образом, вызов C.Func может стать причиной возбуждения исключения Program_Error.Результат действия директивы Elaborate_All более строгий.Она требует, чтобы была осуществлена предварительная элаборация не только тела того модуля, который указан в директиве (и в спецификаторе with), но и тела всех модулей, которые используются указанным модулем (для поиска используемых модулей используется транзитивная цепочка спецификаторов with).Например, если поместить директиву Elaborate_All (B) в модуль A, то она потребует, чтобы перед элаборацией модуля A была осуществлена элаборация не только тела модуля B, но и тела модуля C, поскольку модуль B указывает модуль C в спецификаторе with.
Теперь можно продемонстрировать использование этих правил для предупреждения проблем связанных с элаборацией, в случаях, когда не используются динамическая диспетчеризация и ссылочные значения для подпрограмм.Такие случаи будут рассмотренны несколько позже.
Суть правила проста.Если модуль содержит код элаборации, который может прямо или косвенно осуществить вызов подпрограммы модуля указанного в спецификаторе with, или конкретизировать настраиваемый модуль расположенный внутри модуля указанного в спецификаторе with, то в случае, когда модуль, который указан в спецификаторе with, не содержит директиву Pure, Preelaborate или Elaborate_Body, клиент должен указывать директиву Elaborate_All для модуля, который указан в спецификаторе with.Соблюдение этого правила гарантирует клиенту, что вызовы и конкретизация настраиваемых модулей могут быть выполнены без риска возбуждения исключения.Если это правило не соблюдается, то программа может попасть в одно из четырех состояний:
Правильный порядок элаборации отсутствует:
Отсутствует порядок элаборации, который соблюдает правила учитывающие использование любой из директив Pure, Preelaborate или Elaborate_Body.В этом случае, компилятор Ada95 обязан распознать эту ситуацию на этапе связывания, и отвергнуть построение исполняемой программы.
Порядок элаборации, один или более, существует, но все варианты не правильны:
В этом случае, редактор связей может построить исполняемую программу, но при выполнении программы будет возбуждаться исключение Program_Error.
Существует несколько вариантов порядка элаборации, некоторые правильны, некоторые - нет:
Подразумевается, что программист не управляет порядком элаборации.Таким образом, редактор связей может выбрать или не выбрать один из правильных вариантов порядка элаборации, и программа может не возбуждать или возбуждать исключение Program_Error во время выполнения.Этот случай является наихудшим, поскольку программа может перестать выполняться после переноса в среду другого компилятора, или даже в среду другой версии того же самого компилятора.
Порядок элаборации, один или более, существует, все варианты правильны:
В этом случае программа выполняется успешно.Такое состояние может быть гарантировано при соблюдении указанных ранее правил, но оно также может быть получено без соблюдения этих правил.
Следует обратить внимание на одно дополнительное преимущество соблюдения правила Elaborate_All, которое заключается в том, что программа продолжает оставаться в идеальном состоянии даже в случае изменения тел некоторых подпрограмм.В противном случае, если программа, которая не соблюдает указанное правило, в некоторый момент времени оказывается безопасной, то это состояние программы может также незаметно исчезнуть, в результате изменений вызванных нуждами сопровождения программы.
Введение ссылочных типов для подпрограмм усложняется обработкой элаборации.Трудность заключается в том, что не возможно определить вызываемую подпрограмму на этапе компиляции.Это подразумевает, что в таком случае редактор связей не может проанализаровать требования элаборации.
Если в точке, в которой создается ссылочное значение, известно тело подпрограммы, для которого необходимо выполнить элаборацию, то ссылочное значение будет безопасным и его использование не требует проверки.Это может быть достигнуто соответствующей организацией последовательности описаний, если подпрограмма расположена в текущем модуле, или использованием директивы Pure, Preelaborate или Elaborate_Body, когда подпрограмма расположена в другом модуле, который указан в спецификаторе with.
Если тело используемой подпрограммы, для которого необходимо выполнить элаборацию, не известно в точке создания ссылочного значения, то любое используемое ссылочное значение должно подвергаться динамической проверке, и такая динамическая проверка должна возбуждать исключение Program_Error в случае, когда элаборация тела не осуществлена.
При использовании динамической диспетчеризации для тэговых типов
необходимые динамические проверки генерируются аналогичным образом.Это означает, что в случае вызова любой примитивной операции тэгового типа,
который выполняется до элаборации тела примитивной операции,
будет возбуждаться исключение Program_Error.
Управление видимостью
Управление видимостью
При построении большой модульной системы, существует необходимость в определении того, что должно быть видимым и того, что не должно быть видимым за пределами какого-либо модуля (пакет, тип или подсистема).При этом, могут существовать различные промежуточные уровни видимости.
Предположим, что модуль является компонентом какой-либо большой системы.В этом случае, ресурсы данного модуля могут быть не доступны вне системы.Внутри системы, ресурсы модуля могут быть видимы для остальных частей общей системы, но допуская при этом частичную видимость, которая предоставляет некоторые компоненты или операции модуля только для выбранной группы модулей системы.
Необходимо обратить внимание на то, что данные проблемы подобны, вне зависимости от того - рассматриваем-ли мы типы, пакеты или классы наследования.Таким образом, некоторые свойства могут быть: публичные (public), то есть видимые всем приватные (private), то есть видимые, но не для всех, а только для определенного круга "друзей", или наследников и дочерних модулей внутренние (internal), то есть не видимы ни для кого, включая дочерние модули
В качестве примера, можно привести следующую аналогию: то что на фасаде дома - то видно всем (public), то что внутри дома - то видно только членам семьи (private), то что в чьем-либо теле - не видно никому (internal).
Благодаря существованию трех уровням видимости, существуют три уровня управления: публичные (public) свойства описываются в интерфейсе (спецификация пакета) приватные (private) свойства описываются в спрятанной части интерфейса (приватная часть спецификации пакета) внутренние (internal) свойства описываются в части реализации (тело пакета)
Следует заметить, что различие между приватными и внутренними описаниями не требовалось в Ada 8Необходимость в таком различии появилась в Ada95, поскольку появилась новая концепция - концепция дочерних модулей, и сопутствующая ей необходимость в управлении уровнями видимости.
Взаимно рекурсивные типы
Взаимно рекурсивные типы
Бывают случаи, когда необходимо иметь два (или более) типа, которые являются взаимно рекурсивными.Такие типы содержат указатели, ссылаются друг на друга.В качестве примера рассмотрим типы: Doctor - врач и Patient - пациент.Эти типы нуждаются в указателях, которые ссылаются друг на друга.Ада потребует поместить оба этих типа в один пакет.Чтобы сохранить возможность раздельной компиляции, можно описать два базовых типа в одном пакете:
package Doctors_And_Patients is type Doctor is abstract tagged null record; type Doctor_Ptr is access all Doctor'Class; type Patient is abstract tagged null record; type Patient_Ptr is access all Patient'Class;end Doctors_And_Patients; |
Позже, для непосредственного фактического использования, можно создать в самостоятельных пакетах производные типы, которые используют описания этих базовых типов.Подобные описания могут иметь следующий вид:
with Doctors_And_Patients; use Doctors_And_Patients;package Doctors is type Doc is new Doctor with private; -- подпрограммы . . .private type Doc is new Doctor with record Pat : Patient_Ptr; . . . end record; end Doctors;with Doctors_And_Patients; use Doctors_And_Patients;package Patients is-- далее подобно Doctors... . . . |
При описании базовых типов, в пакете Doctors_And_Patients, можно также описать операции, которые будут определять элементарную функциональность базовых типов.Дополнительные операции могут быть добавлены позднее, в описаниях производных типов (например в типе Doc).
Примечательно, что тело одного пакета может видеть спецификацию другого пакета.Таким образом, тела пакетов могут иметь следующий вид:
with Doctors; use Doctors; package body Patients is -- тело пакета Patients имеет доступ к сервисам Doctors -- описанным в спецификации пакета Doctors . . .end Patients;with Patients; use Patients; package body Doctors is -- тело пакета Doctors имеет доступ к сервисам Patients -- описанным в спецификации пакета Patients . . .end Doctors; |
Язык Ада - взгляд "сверху вниз"
Язык Ада - взгляд "сверху вниз"
Знакомство с языком программирования Ада можно осуществлять несколькими различными путями.Первая часть рассматривает большинство синтаксических конструкций языка программирования Ада и представляет материал по принципу "снизу вверх", демонстрируя конструкции языка Ада с постепенно возрастающей структурой и/или семантической сложностью.
Теперь попытаемся рассмотреть процесс программирования, как процесс проектирования системы, то есть, воспользуемся принципом "сверху вниз".С инженерной точки зрения под системой обычно подразумевается множество взаимосвязанных компонент (возможно, функционирующих параллельно), с каждой из которых может быть связана некоторая информация о состоянии.
Таким образом, законченную программу на языке Ада можно рассматривать как совокупность взаимосвязанных компонент, называемых модулями или программными единицами.Следует заметить, что важной отличительной чертой языка Ада, по сравнению с остальными широко распространенными языками программирования, является акцент, сделанный на различие спецификации и реализации программных единиц.Следовательно, такой подход обеспечивает более удобные возможности разработки программ в виде совокупностей взаимодействующих и поддерживающих свое состояние программных единиц.Кроме того, это позволяет группам программистов создавать и управлять большими системами и программами с минимальными затратами.
Любая программа на языке Ада состоит из набора пакетов и одной главной подпрограммы (то есть начальной или "стартовой" задачи), которая активизирует эти пакеты.Структурное представление программы в виде набора программных единиц имеет преимущество в том, что по мере возрастания размеров и числа программных единиц программист в большей степени сохраняет для себя общее представление о работе программы в целом (как последовательности действий), поскольку число возможных взаимоотношений между шагами задания и данными ограничено благодаря использованию пакетов.Пакеты ограничивают операции, которые могут быть выполнены над индивидуальными объектами типа "данные", благодаря закону (своего рода алгебре), установленному для каждого из пакетов.
Можно сказать, что пакет может быть единым менеджером над созданием и использованием объектов некоторого заданного типа.Учитывая строгую типизацию языка программирования Ада, программист имеет гарантию того, что все экземпляры объектов данного типа будут обработаны соответствующим образом, то есть целостность "содержимого" каждого объекта будет поддерживаться надлежащим образом, о чем программисту не следует заботиться, так как ответственность за это целиком возлагается на реализацию соответствующего пакета.Программирование по такому принципу позволяет пользователю, выполняющему над пакетом некоторую операцию, сфокусировать свое внимание только на объекте типа "данные" или же только на отдельных его частях.Язык Ада позволяет определять неограниченное число типов объектов типа "данные" и назначать создание и использование отдельных объектов типа "данные" за соответствующими четко выраженными менеджерами типа.
В действительности, здесь только слегка затронуты "дисциплинарные преимущества" пакетов, используемых в языке Ада.В более общем случае пакеты могут включать в свою спецификацию не только набор значимых операций над несколькими (одним или более) объектами типа "данные", но и содержать описания, определяющие тип таких объектов (определения типа).
Использование языка Ада для подсистем и прикладных задач заманчиво не только из-за возможности работы с пакетами.В частности, привлекательной является также возможность декомпозиции программы на группы взаимосвязанных задач.Задачи в языке Ада могут создаваться (и прерываться) как статически, так и динамически.Они позволяют организовывать выполнение задач на конкурентной основе (абстрактной или фактической), что достигается параллельной или конвейерной организацией задач.Представление системы в виде набора задач обеспечивает более ясное понимание ее работы, а также более быстрое функционирование (хотя выполнение задач на конкурентной основе возможно только при наличии в системе нескольких доступных процессоров).
Программа на языке Ада начинает свое выполнение с единственной нити управления, связанной с главной подпрограммой (стартовой задачей).При вызове подпрограммы внутри пакета нить управления "перемещается" по кодам, составляющим этот пакет.В итоге нить управления возвращается к стартовой задаче подобно тому, как нить управления перемещается от самого верхнего блока программы на Паскале к различным вызываемым подпрограммам или от них.
В некоторых точках программы (например, во время обработки спецификации задачи) могут быть порождены новые нити управления.Процесс порождения нитей управления может последовательно развиваться, образуя дерево задачи.Окончание такой задачи может произойти только после окончания работы всех порожденных ею задач.
Любая задача может вызвать подпрограмму из другого пакета, однако в результате такой операции новые нити управления не создаются.Под "вызовом пакета" подразумевается обращение к подпрограмме, то есть операция, принадлежащая к общедоступной части пакета.В случае вызова пакета нить управления может быть представлена как линия связи от кода вызывающей программы к коду вызываемого пакета и в обратном направлении при возврате из вызываемого пакета.
В противоположность этому задача "вызывает другую задачу", то есть осуществляет обращение ко входу, посылая ей сообщение.В результате вызова одной задачи из другой нить управления вызывающей задачи задерживается до тех пор, пока вызываемая задача не отреагирует на посланное ей сообщение (завершит прием этого сообщения).Если все завершается благополучно, то вызывающую задачу информируют о том, что сообщение было получено.После этого вызывающая программа возобновляет свою работу.Это означает, что нить управления вызывающей задачи возобновляет свою работу, а вызываемая задача продолжает выполнять свой независимый участок программы.
Как мы уже знаем, такой протокол, включающий в себя временную остановку вызывающей задачи, называется рандеву.При завершении рандеву вызывающая задача может снова начать выполнение, возможно, параллельно с вызванной ею задачей.Кроме механизма рандеву, для организации взаимодействия задач могут быть использованы защищенные объекты, которые, в отличие от задач, не являются активными сущностями.Использование рандеву и защищенных объектов гарантирует поддержание "структурности" программы в терминах "структурированного программирования".В общем случае дерево задач, состоящее из m порожденных и активных в текущий момент задач, может обрабатываться с уровнем параллельности, равным m, если в системе имеется m доступных процессоров.
Таким образом, если рассматривать всю систему как совокупность компонент (возможно, работающих параллельно), причем, каждая компонента может включать в себя информацию о своем состоянии, то способность языка Ада реализовывать разнообразные богатые структуры пакетов и задач является крупным достижением в разработке языков программирования.
Рассмотрим противоположную ситуацию на примере языка Паскаль.В этом случае большинство информации о состоянии (например, о скалярных переменных, массивах, записях, и т.д.) связано со всей программой, а не с отдельными подпрограммами.Таким образом, за исключением данных, которые объявлены во внешнем блоке программы, время жизни всей объявленной информации ограничено временем нахождения в активном состоянии блока или подпрограммы.
Исходя из этого, программист, работающий на языке Паскаль, может испытывать затруднения при моделировании реальной системы или при объяснении принципов работы созданной им программы другим лицам, которые знакомы с реальной моделью.Это объясняется в первую очередь тем, что отсутствие разнообразных пространств состояний в Паскале запрещает сохранение соответствия между системой и моделирующей ее программой.Более того, наличие в моделируемой системе параллельно функционирующих компонент еще более уменьшает соответствие между программой на Паскале и моделируемой ею системой.
Чем в меньшей степени поведение системы соответствует поведению моделирующей ее программы, тем труднее осуществлять проверку этого соответствия.Такие программы также труднее поддерживать (модифицировать) по мере того, как вносятся изменения в моделируемую систему (из-за изменений в постановке проблемы или в требованиях к результатам).Поскольку расходы на модификацию особенно при больших размерах программ могут быть значительными, то в этом случае одним из существенных преимуществ, склоняющих к программированию на языке Ада, оказывается возможность поддержания ясного структурного соответствия между программой и моделируемой системой.
В результате,
при использовании языков программирования ориентированных на управление
(подобных языку программирования Паскаль),
большая часть усилий концентрируется на сохранении логического
и функционального соответствия между программами и моделируемыми ими системами.