Изоляция

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

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

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

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

Простейшая инкапсуляция

package Encapsulate is procedure Hello; private procedure Hello2; -- Not visible from external units end Encapsulate;
with Ada.Text_IO; use Ada.Text_IO; package body Encapsulate is procedure Hello is begin Put_Line ("Hello"); end Hello; procedure Hello2 is begin Put_Line ("Hello #2"); end Hello2; end Encapsulate;
with Encapsulate; procedure Main is begin Encapsulate.Hello; Encapsulate.Hello2; -- Invalid: Hello2 is not visible end Main;

Абстрактные типы данных

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

package Stacks is type Stack is private; -- Declare a private type: You cannot depend -- on its implementation. You can only assign -- and test for equality. procedure Push (S : in out Stack; Val : Integer); procedure Pop (S : in out Stack; Val : out Integer); private subtype Stack_Index is Natural range 1 .. 10; type Content_Type is array (Stack_Index) of Natural; type Stack is record Top : Stack_Index; Content : Content_Type; end record; end Stacks;
package body Stacks is procedure Push (S : in out Stack; Val : Integer) is begin -- Missing implementation! null; end Push; procedure Pop (S : in out Stack; Val : out Integer) is begin -- Dummy implementation! Val := 0; end Pop; end Stacks;

В приведенном выше примере мы определяем тип для стека в публичной части (известной как видимый раздел спецификации пакета в Аде), но детали реализация этого типа скрыты.

Затем в личном разделе мы определяем реализацию этого типа. Мы также можем объявить там другие вспомогательные типы, которые будут использованы для описания основного публичного типа. Создание вспомогательных типов - это полезная и распространенная практика в Аде.

Несколько слов о терминологии:

  • То, как выглядит тип стека Stack в видимом разделе, называется частичным представлением типа. Это то, к чему имеют доступ клиенты.

  • То, как выглядит тип стека Stack из личного раздела или тела пакета, называется полным представлением типа. Это то, к чему имеют доступ разработчики.

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

--  No need to read the private part to use the package
package Stacks is
   type Stack is private;

   procedure Push (S   : in out Stack;
                   Val :        Integer);
   procedure Pop (S   : in out Stack;
                  Val :    out Integer);
private
   ...
end Stacks;

А вот как будет использоваться пакет Stacks:

--  Example of use
with Stacks; use Stacks;

procedure Test_Stack is
   S : Stack;
   Res : Integer;
begin
   Push (S, 5);
   Push (S, 7);
   Pop (S, Res);
end Test_Stack;

Лимитируемые типы

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

package Stacks is type Stack is limited private; -- Limited type. Cannot assign nor compare. procedure Push (S : in out Stack; Val : Integer); procedure Pop (S : in out Stack; Val : out Integer); private subtype Stack_Index is Natural range 1 .. 10; type Content_Type is array (Stack_Index) of Natural; type Stack is limited record Top : Stack_Index; Content : Content_Type; end record; end Stacks;
package body Stacks is procedure Push (S : in out Stack; Val : Integer) is begin -- Missing implementation! null; end Push; procedure Pop (S : in out Stack; Val : out Integer) is begin -- Dummy implementation! Val := 0; end Pop; end Stacks;
with Stacks; use Stacks; procedure Main is S, S2 : Stack; begin S := S2; -- Illegal: S is limited. end Main;

Это нужно, например, для тех типов данных, для которых встроенная операция присваивания работает неправильно (например, когда требуется многоуровневевое копирование).

Ада позволяет вам определить операторы сравнения = и /= для лимитируемых типов (или переопределить встроенные объявления для нелимитируемых).

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

Дочерние пакеты и изоляция

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

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

Часть дочернего пакета

Доступ к личному разделу родительской спецификации

Спецификация: видимый раздел

Нет

Спецификация: личный раздел

Да

Тело

Да

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

Давайте сначала рассмотрим пример, в котором тело дочернего пакета P.C имеет доступ к личному разделу спецификации его родителя P. В предыдущем примере исходного кода мы видели, что процедуру Hello2, объявленную в личном разделе пакета Encapsulate нельзя использовать в процедуре Main, поскольку ее там не видно. Однако это ограничение не распространяется на некоторые части дочерних пакетов. Фактически, тело дочернего пакета Encapsulate.Child имеет доступ к процедуре Hello2 и она может быть вызыванна оттуда, как вы можете видеть в реализации процедуры Hello3 пакета Child:

package Encapsulate is procedure Hello; private procedure Hello2; -- Not visible from external units -- But visible in child packages end Encapsulate;
with Ada.Text_IO; use Ada.Text_IO; package body Encapsulate is procedure Hello is begin Put_Line ("Hello"); end Hello; procedure Hello2 is begin Put_Line ("Hello #2"); end Hello2; end Encapsulate;
package Encapsulate.Child is procedure Hello3; end Encapsulate.Child;
with Ada.Text_IO; use Ada.Text_IO; package body Encapsulate.Child is procedure Hello3 is begin -- Using private procedure Hello2 -- from the parent package Hello2; Put_Line ("Hello #3"); end Hello3; end Encapsulate.Child;
with Encapsulate.Child; procedure Main is begin Encapsulate.Child.Hello3; end Main;

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

package My_Types is type Priv_Rec is private; private type Priv_Rec is record Number : Integer := 42; end record; end My_Types;
package My_Types.Ops is procedure Display (E : Priv_Rec); end My_Types.Ops;
with Ada.Text_IO; use Ada.Text_IO; package body My_Types.Ops is procedure Display (E : Priv_Rec) is begin Put_Line ("Priv_Rec.Number: " & Integer'Image (E.Number)); end Display; end My_Types.Ops;
with Ada.Text_IO; use Ada.Text_IO; with My_Types; use My_Types; with My_Types.Ops; use My_Types.Ops; procedure Main is E : Priv_Rec; begin Put_Line ("Presenting information:"); -- The following code would trigger a -- compilation error here: -- -- Put_Line ("Priv_Rec.Number: " -- & Integer'Image (E.Number)); Display (E); end Main;

В этом примере у нас нет доступа к компоненте Number типа записи Priv_Rec в процедуре Main. Вы можете увидеть это в вызове Put_Line, который был закомментирован в реализации Main. Попытка получить доступ к компоненте Number вызовет ошибку компиляции. Но у нас есть доступ к этой компоненте в теле пакета My_Types.Ops, поскольку это дочерний пакет пакета My_Types. Следовательно, тело Ops имеет доступ к объявлению типа Priv_Rec, которое находится в личном разделе его родительского пакета My_Types. По этой причине тот же вызов Put_Line, который вызовет ошибку компиляции в процедуре Main, отлично работает в процедуре Display пакета My_Types.Ops.

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

Как мы упоминали ранее, в дополнение к телу пакета личный раздел спецификации дочернего пакета P.C также имеет доступ к личному разделу спецификации его родителя P. Давайте посмотрим на пример, в котором мы объявляем объект личного типа Priv_Rec в личном разделе дочернего пакета My_Types.Child и напрямую инициализируем компоненту Number записи Priv_Rec:

package My_Types.Child is

private

   E : Priv_Rec := (Number => 99);

end My_Types.Ops;

Естественно, мы не смогли бы инициализировать этот компонент, если бы переместили это объявление в общедоступный (видимый) раздел того же дочернего пакета:

package My_Types.Child is

   E : Priv_Rec := (Number => 99);

end My_Types.Ops;

Объявление выше вызывает ошибку компиляции, поскольку тип Priv_Rec является личным. Поскольку видимый раздел My_Types.Child также виден за пределами дочернего пакета, Ада запрещает доступ к личной информации в этом разделе спецификации.