Ада-95. Компилятор GNAT
66ac8edd

Пример программирования посредством расширения


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

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

Представим себе функцию, которая читает до 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 (1..20) 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 для программирования посредством расширения.
Он представляет своеобразный вид "бесконечного варианта" для получения которого были использованы тэговые типы.
Очевидно, что этот пример несколько искуственный.
Следовательно этот пример может быть не полностью корректен для порождения реального кода.
Copyright (C) А.Гавва V-0.4w май 2004
Содержание раздела