учебники, программирование, основы, введение в,

 

OO-программирование и язык Ada

Немного контекста
Создание языка Ada было реакцией на кризис середины 70-х годов, ощутимый для политики в области разработки ПО в Департаменте Обороны США (DoD). В отчете, предшествовашем появлению языка Ada, отмечалось, что в военной отрасли в тот момент использовалось более 450 языков программирования, многие из которых технически устарели. Все это мешало управлению подрядными работами, обучению программистов, техническому прогрессу, разработке качественного ПО и контролю цен.
Помня об успехе языка COBOL, разработанного в 50-х годах по запросу DoD, был объявлен конкурс на разработку современного языка создания ПО. Одна из заявленных целей - возможность поддержки встроенных приложений в режиме реального времени. В результате были отобраны четыре, затем - два, и, наконец, в 1979 году, после действительно справедливого отбора, победителем оказался язык Green, созданный Жаном Ичбиа (Jean D. Ichbiah) и его группой CII-Honeywell Bull. На основе опыта нескольких лет и первых промышленных реализаций язык был пересмотрен и в 1983 году был принят как стандарт ANSI.
Язык Ada (так был назван язык Green) начал новый этап в разработке языков. Никогда раньше язык не подвергался такому интенсивному испытанию перед выпуском. Никогда раньше создание языка не трактовалось как крупномасштабный инженерный проект. Лучшие эксперты многих стран в составе рабочих групп проводили недели, рассматривая предложения и делая - в те доинтернетовские дни - большое количество комментариев. Подобно языку Algol 60 в предыдущем поколении языков, Ada определил не только языковую перспективу, но и само понятие разработки языка.
Дальнейший пересмотр языка Ada привел к новой версии языка, официально называемой Ada 95, описываемой в конце данной лекции. В других частях курса название Ada без дальнейшего уточнения относится к версии Ada 83, широко используемой и сегодня.
Был ли язык Ada успешным? И да, и нет. Департамент Обороны получил то, что заказывал: благодаря строгому выполнению "поручения" язык Ada стал через несколько лет доминирующим техническим языком различных отраслей Американской военной промышленности и военных организаций некоторых других стран. Он используется в таких невоенных правительственных агентствах, как NASA и Европейское Космическое Агентство. Но, кроме некоторого проникновения в сферу обучения теории вычислительных систем - частично по инициативе Департамента Обороны, - этот язык имел лишь ограниченный успех в остальном мире ПО. Возможно, он бы распространился шире, если бы не конкуренция со стороны объектной технологии, внезапно появившейся на сцене, как раз тогда, когда язык Ada и промышленность созрели друг для друга.
По иронии судьбы разработчики языка Ada были хорошо знакомы с ОО-идеями. Хотя это не всем известно, Ичбиа создал один из первых компиляторов для Simula 67 - первого ОО-языка. Позже, когда его спрашивали, почему он не представил ОО-проект Департаменту Обороны, он объяснял, что в контексте конкуренции такой проект посчитали бы настолько далеким от основного направления, что у него было бы шансов на победу. И он, без сомнения, прав. Действительно, до сих пор можно удивляться смелости проекта, принятого DoD. Было разумно ожидать, что процесс приведет к чему-то вроде усовершенствованной версии языка JOVIAL (языка военных приложений 60-х гг.). Но все четыре отобранных языка были основаны на языке Pascal, с его явным академическим привкусом. А Ada являлся воплощением новых смелых идей во многих областях, например, в обработке исключений, универсальности и параллелизме. Ирония состоит и в том, что язык Ada, направленный на поддержание соответствия проектов DoD прогрессу в разработках ПО, вытесняя старые подходы, в последующие годы невольно привел к задержке принятия новой (post-Ada) технологии в военном и космическом сообществе.
Уроки языка Ada остаются незаменимыми, и жаль, что многие ОО-языки 80-х и 90-х гг. не обращали большего внимания на акцент качества программной инженерии, характерный для языка Ada. Хотя в этой книге мы неоднократно будем противопоставлять решения, принятые в языке Ada, методам, принятым в объектной технологии, но эти замечания следует воспринимать не как укор, а как дань уважения к предшественнику, в сравнении с которым должны оцениваться новые методы.
Пакеты
Любой инкапсулирующий язык предлагает модульную конструкцию для группирования логически связанных программных элементов. В языке Ada она называется пакетом, модулем - в Modula-2 и Mesa, кластером - в CLU.
Класс определяется и как структурный системный компонент - модуль, и как тип. Напротив, пакет - это только модуль. Ранее отмечалось, что пакеты являются чисто синтаксическими понятиями, а классы имеют и семантическое значение. Пакеты дают способ распределения элементов системы (переменных, подпрограмм ...) в согласованные подсистемы, но они нужны только для управляемости и удобочитаемости ПО. Декомпозиция системы на пакеты не затрагивает ее семантики: можно трансформировать многопакетную систему Ada в однопакетную систему, дающую те же самые результаты, посредством чисто синтаксической операции - сняв все границы пакетов, расширяя родовые порождения (это объясняется ниже) и разрешая конфликт имен посредством переименования. Классы являются семантической конструкцией, представляя одновременно единицу модульной декомпозиции, они описывают поведение объектов во время выполнения. Благодаря наследованию семантика обогащается полиморфизмом и динамическим связыванием.
Пакет языка Ada - это свободное соединение элементов программы. Он используется для различных целей. Разумное использование этого понятия включает создание пакета, содержащего:

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

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

Реализация стеков

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

function item (s: STACK) return X;

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

Простой интерфейс

Первую версию интерфейса пакета, задающего стек, можно выразить следующим образом. Заметим, что ключевое слово package (пакет) вводит интерфейс; тело, появляющееся позднее, вводится сочетанием package body (тело пакета).

package REAL_STACKS is
    type STACK_CONTENTS is array (POSITIVE range <>) of FLOAT;
    type STACK (capacity: POSITIVE) is
        record
            implementation: STACK_CONTENTS (1..capacity);
            count: NATURAL := 0;
        end record;
    procedure put (x: in FLOAT; s: in out STACK);
    procedure remove (s: in out STACK);
    function item (s: STACK) return FLOAT;
    function empty (s: STACK) return BOOLEAN;
    Overflow, Underflow: EXCEPTION;
end REAL_STACKS;

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

  • Удивительно видеть все детали представления стека в объявлениях типов STACK и STACK_CONTENTS, появившихся в том, что должно быть чистым интерфейсом. Кратко рассмотрим причину этой проблемы и способ ее устранения.
  • В отличие от класса, пакет не определяет тип. Тип STACK следует определить отдельно. Одним из следствий этого отделения для программиста, создающего пакет вокруг реализации абстрактного типа данных, является необходимость изобретения двух различных имен - одно для пакета, другое - для типа. Другое следствие состоит в том, что подпрограммы имеют еще один аргумент по сравнению со своими ОО-аналогами: здесь все они имеют первым аргументом стек s, в то время как для класса он задается неявно (см. предыдущие лекции).
  • Объявление может определять не только тип сущности, но и ее исходное значение. Здесь объявление count в типе STACK предписывает исходное значение 0. Оно устраняет необходимость явной операции инициализации, задаваемой процедурой создания (конструктором) класса. Однако этот способ не работает, если требуется более сложная инициализация.
  • Для понимания объявления типа следует привести некоторые детали языка Ada: POSITIVE и NATURAL обозначают подтипы INTEGER, включающие, соответственно, положительные и неотрицательные целые, спецификация типа вида array (TYPE range <>), где <> известно как Box-символ, описывает шаблон для типов массивов. Для получения действительного типа из такого шаблона нужно выбрать конечный отрезок TYPE. Здесь это делается при определении типа STACK, использующем интервал [1..capacity] типа POSITIVE. STACK является примером параметризованного типа. Любое объявление сущности типа STACK должно задавать фактическое значение емкости стека capacity, как в:
s: STACK (1000)
  • В языке Ada каждый аргумент подпрограммы характеризуется статусом in, out или in out, определяющим права подпрограммы на использование фактических аргументов (только для чтения, только для записи, для обновления). В отсутствии явного ключевого слова состояние по умолчанию - in.
  • Наконец, интерфейс определяет два имени исключений Overflow и Underflow. Исключение - это ситуация, когда из-за ошибок прерывается нормальный порядок вычислений. Интерфейс пакета должен перечислить любые исключения, которые могут возбуждаться в процессе работы подпрограмм пакета и передаваться для обработки клиентам. Подробно механизм исключений языка Ada описывается ниже.

Использование пакета

Приведем пример из клиентского пакета, использующего стек вещественных чисел:

s: REAL_STACKS.STACK (1000);
REAL_STACKS.put (3.5, s); ...;
if REAL_STACKS.empty (s) then ...;

Среда языка Ada должна иметь возможность компилировать такой клиентский код, располагая только интерфейсом REAL_STACKS, не имея доступа к его телу.
Синтаксически каждое использование сущности (здесь "сущности" включают имена программ и типов) повторяет имя пакета REAL_STACKS. Это утомительно - необходима неявная форма квалификации. Если включена директива:

use REAL_STACKS;

в начале клиентского пакета, то выражения записываются проще:

s: STACK (1000);
put (3.5, s); ...;
if empty (s) then ...;

Конечно, используется и полная форма для сущности, чье имя вступает в конфликт с именем, указанным в другом доступном пакете (скажем, объявленное в самом пакете или в пакете из списка в директиве use).
В литературе по языку Ada иногда встречается совет программистам вообще не использовать директиву use, поскольку она мешает ясности: неквалифицированная ссылка, например вызов empty (s), сразу не говорит о поставщике empty (в нашем примере REAL_STACKS). Его аналог в ОО-подходе, s.empty, однозначно определяет поставщика через цель s.
В ОО-мире подобная проблема возникает из-за наследования: имя в классе может ссылаться на компонент, объявленный любым из предков. Техника, частично решающая проблему, - это плоская форма класса.

Реализация

Тело пакета REAL_STACKS может объявляться следующим образом. Полностью показана только одна подпрограмма.

package body REAL_STACKS is
    procedure put (x: in FLOAT; s: in out REAL_STACK) is
        begin
            if s.count = s.capacity then
                raise Overflow
            end if;
            s.count := s.count + 1;
            s.implementation (count) := x;
        end put;
    procedure remove (s: in out STACK) is
            ... Реализация remove ...
        end remove;
    function item (s: STACK) return X is
            ... Реализация item ...
        end item;
    function empty (s: STACK) return BOOLEAN is
            ... Реализация empty ...
        end empty;
end REAL_STACKS;

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

Универсальность

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

generic
    type G is private;
package STACKS is
    ... Все, как и ранее, заменяя все вхождения FLOAT на G ...
end STACKS;

Предложение generic синтаксически более тяжелое, чем наша ОО-нотация для универсальных классов (class C [G]...), но зато в нем больше возможностей. В частности, параметры, объявляемые в generic, могут представлять не только типы, но и подпрограммы. В приложении B эти возможности обсуждаются при сравнении универсальности и наследования.
В теле пакета generic не повторяется, там достаточно конкретный тип FLOAT заменить родовым G.
Спецификация is private заставляет остальную часть пакета рассматривать G как закрытый тип. Это означает, что сущности этого типа могут использоваться только в операциях, применимых ко всем типам языка Ada: в качестве исходного или целевого объекта при присваивании, как операнд в проверке равенства, как фактический аргумент в подпрограмме, и в некоторых других специальных операциях. Это близко к соглашению для неограниченных формальных параметров универсальных классов нашей нотации. В языке Ada доступны и другие возможности. Можно ограничить операции, объявляя параметр как limited private, что запрещает все использования кроме фактических аргументов подпрограмм.
Называясь пакетом, универсально параметризованный модуль, такой как STACKS, в действительности является шаблоном пакета, поскольку клиенты не могут использовать его непосредственно; они должны получить из него действительный пакет, используя фактические родовые параметры. Новую версию нашего пакета стеков действительных величин можно определить через следующее родовое порождение:

package REAL_STACKS_1 is new STACKS (FLOAT);

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

Скрытие представления: частная история

Пакет STACKS в том виде, как он задан, не реализует принцип скрытия информации. Объявления типов STACK и STACK_CONTENTS, находясь в интерфейсе, позволяют клиентам непосредственный доступ к представлению стеков. Например, клиент может включить код вида:
[1]
use REAL_STACKS_1;...
s: STACK; ...
s.implementation (3) := 7.0; s.last := 51;
грубо нарушая основную спецификацию абстрактных типов данных.
Концептуально объявления типа должны находиться в теле. Почему их туда не помещают с самого начала? Объяснение находится вне языка и требует рассмотрения проблем программного окружения.
Одно из уже упомянутых требований к языку Ada состояло в возможности независимой компиляции пакета при наличии доступа к его интерфейсу, но необязательно к его телу. Принятая технология предполагала построение сверху вниз: для продолжения работы над модулем достаточно знать спецификацию необходимых ему средств. Действительная реализация могла появиться значительно позже.
Если есть доступ к интерфейсу REAL_STACKS_1 (то есть к интерфейсу STACKS, REAL_STACKS_1 является просто его родовым порождением), можно компилировать любого из его клиентов. Такой клиент будет содержать объявления вида:
use REAL_STACKS_1;...
s1, s2: STACK; ...
s2 := s1;
Компилятор не сможет их хорошо обрабатывать, не зная размера объекта типа STACK. Но это может определяться только из объявлений типа для STACK и вспомогательного типа STACK_CONTENTS.
Отсюда концептуальная дилемма, стоявшая перед проектировщиками языка Ada: вопросы реализации требуют помещения объявлений типа в рай - интерфейс, в то время как им место в аду - теле пакета.
Пришлось создать чистилище: специальный раздел пакета, физически видимый в интерфейсе и компилируемый с ним, но такой, что клиенты не могут обращаться к его элементам. Чистилище - это закрытая часть интерфейса, она вводится ключевым словом private. Любое объявление, появляющееся здесь, недоступно клиентам. Эта схема иллюстрируется нашей последней версией интерфейса пакета, задающего стек:
generic
type G is private;
package STACKS is
type STACK (capacity: POSITIVE) is private;
procedure put (x: in G; s: in out STACK);
procedure remove (s: in out STACK);
function item (s: STACK) return G;
function empty (s: STACK) return BOOLEAN;
Overflow, Underflow: EXCEPTION;
private
type STACK_VALUES is array (POSITIVE range <>) of G;
type STACK (capacity: POSITIVE) is
record
implementation: STACK_VALUES (1..capacity);
count: NATURAL := 0;
end record
end STACKS;
Отметим, тип STACK теперь должен объявляться дважды: сначала в открытой части интерфейса, где он специфицируется как private, затем еще раз в закрытой части, где дается полное описание. Без первого объявления строка вида s: REAL_STACK не будет разрешенной в клиенте, поскольку доступ есть только к сущностям, объявляемым в открытой части. Первое объявление, специфицируя тип как private, запрещает клиентам доступ к любым свойствам помимо универсальных операций: присваивания, проверки на равенство и использование в качестве фактических аргументов.
Заметьте, тип STACK_VALUES чисто внутренний и не нужен клиентам. Поэтому он не объявляется в открытой части интерфейса пакета.
Важно понять, что информация, помещаемая в закрытую часть интерфейса, должна была быть в теле пакета и появляется в спецификации пакета только по причинам реализации языка. С новой формой STACKS клиентский код, выше помеченный как [1], имевший прямой доступ к представлению в клиенте, становится неправильным.
Авторы клиентских модулей могут видеть внутреннюю структуру экземпляров STACK, но они не могут воспользоваться ею в своих модулях. Это могло бы приводить разработчиков к танталовым мукам. (Хорошая среда языка Ada могла бы скрывать эту часть от клиента, также как это делает инструмент short, описанный в предыдущих лекциях.) Удивительная для новичков, эта политика не противоречит правилу скрытия информации. Как отмечалось ранее, цель скрытия не в том, чтобы не дать авторам клиента возможности прочитать скрытые подробности, а чтобы не дать им использовать эти подробности.
Те, кому хотелось бы все усложнить, могли бы подвести итог в двух предложениях (произнесенных очень быстро, чтобы произвести впечатление и на друзей, и на врагов):. Закрытый раздел интерфейса пакета перечисляет реализацию тех концептуально закрытых типов, которые должны быть объявлены в интерфейсе, хотя их реализация недоступна для использования. В открытой части интерфейса эти типы объявлены закрытыми.

Исключения

Пакет STACKS определяет два исключения в своем интерфейсе: Overflow и Underflow. Язык Ada допускает как собственные исключения с произвольными именами, так и предопределенные исключенияи, запускаемые оборудованием или операционной системой.
Некоторые элементы механизма обработки исключений языка Ada были введены вкурса "Основы объектно-ориентированного программирования", так что здесь можно ограничиться коротким изучением исключений в подходе Ada к построению ПО.

Упрощение управляющей структуры

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

action1;
if error1 then
    error_handling1;
else
    action2;
    if error2 then
        error_handling2;
    else
        action3;
        if error3 then
            error_handling3;
        else
            ...

Механизм исключений в Ada предназначен для борьбы со сложностью подобной схемы - где элементы, выполняющие "полезные" задачи, выглядят как острова в океане кода, обрабатывающего ошибки программы. Ada отделяет обработку ошибок от их обнаружения. Конечно же, обработка ошибок должна включать тесты, определяющие тип возникшей ситуации. Единственным решением является в момент возникновения ошибки возбуждение определенного сигнала - исключения, обрабатывающегося далее где-то в другом месте.

Возбуждение и обработка исключений

Чтобы возбудить исключительную ситуацию, а не обрабатывать ошибки на месте, можно переписать текст следующим образом:

action1;
if error1 then raise exc1; end;
action2;
if error2 then raise exc2; end;
action3;
if error3 then raise exc3; end;
...

При выполнении команды raise exc нормальный порядок вычислений прерывается, и управление передается обработчику исключений (exception handler), представленному специальным блоком подпрограммы и имеющему вид:

exception
    when exc1, ...=> treatment1;
    when exc2 ...=> treatment2;
    ...

При возбуждении исключения exc первым его обрабатывает захвативший его обработчик из динамической цепи вызовов - списка элементов, начинающегося подпрограммой, содержащей вызвавшее исключение предложение raise, и всеми вызывающими подпрограммами, как показано на:
Говорят, что обработчик захватывает exc, если exc появляется в одном из его предложений when (или он содержит предложение вида when others). Такой обработчик выполняет соответствующие команды (после символа =>), после чего управление передается вызывающей программе или заканчивается в случае главной программы. (Ada имеет понятие главной программы.) Если никакой обработчик в динамической цепи не обрабатывает exc, выполнение приложения заканчивается, и управление возвращается к операционной системе, а она, вероятно, выведет системное сообщение об ошибке.

Обсуждение

Интересно сравнить механизм обработки исключений языка Ada с механизмом, разработанным выше в этом курсе в лекции, посвященной исключениям. Между ними есть технические различия и различия в методологии.
К техническим различиям можно отнести способы задания исключений. В одном случае используются множественные предложения when, в другом - наследование от класса EXCEPTIONS. Более важно включение в объектную нотацию возможности повторной попытки, что потребовало введения специального ключевого слова retry. Язык Ada не имеет подобной поддержки и требует для ее реализации использования goto или подобных управляющих структур.
Методологическое различие вытекает из принятой строгой политики, ведущей к принципу Дисциплинированной Обработки Исключений. Каждый обработчик исключений должен заканчиваться, кроме редких случаев "ложной тревоги", либо попыткой повторения, либо официальным отказом ("организованной паникой"). Язык Ada менее строг в этом отношении, что может приводить к некорректному использованию исключения, при котором вызывающая программа получит управление без устранения возникающих проблем.
Стоит повторить основное правило:


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

Исключения в Ada - это управляющие структуры, предназначенные для отделения обнаружения аварийных ситуаций от их обработки и сохранения простоты структуры ПО. Однако на практике этого часто не происходит.
Запись raise some_exception дает впечатление освобождения от запутанной и скучной задачи слежения за необычными ситуациями, позволяя сосредоточиться на самом алгоритме, имеющем дело с нормальной ситуацией. Но вызов исключения еще не решает задачи. Исключения в пакете STACKS типичны. Попытка поместить элемент в полный стек вызывает ошибку Overflow, а попытка доступа к пустому стеку вызывает Underflow. Как обрабатывать Underflow, ошибку, возникающую при вызове remove или item на пустом стеке? Обсуждение Проектирования по Контракту показало, что эти подпрограммы не могут знать, что следует делать в такой ситуации. Вся ответственность лежит на клиенте, вызвавшем эти подпрограммы, только он может решить, что следует делать. У него и должен содержаться код вида:

[2]
    use REAL_STACKS;
    procedure proc (...) is
        s: STACK; ...
    begin
        ... remove (s); ...
    exception
        when Underflow => action1;
        ...
    end proc;

Клиент должен точно определить, что происходит в случае ошибки. Опустить оператор when Underflow было бы ошибкой проекта. Сравните это с обычной, не основанной на исключении, формой вызова:

[3]
    if not s.empty then s.remove else action1 end

(или вариантом, определяющим ошибку апостериори). Форма [2], использующая исключения, отличается от формы [3] только двумя аспектами:

  • код для обработки ошибки action1 текстуально отделен от вызова, приведшего к ошибке;
  • обработка ошибки одинакова для всех подобных вызовов.

Хотя и желательно избегать глубоко вложенных структур обработки ошибок if... then... else..., приведенных в начале лекции, то место в алгоритме, где обнаруживается ошибка, часто предоставляет наилучшую информацию для ее обработки. Если разделить обнаружение и обработку, то могут потребоваться сложные управляющие структуры для случаев, требующих повторного запуска или продолжения обработки.
Кроме того, для подпрограммы, содержащей несколько вызовов remove, способ работы с пустыми стеками вряд ли будет одним и тем же в каждом случае.
Существуют два общих стиля использования исключений. Стиль управляющей структуры рассматривает исключения как нормальный механизм для обработки всех случаев, отличающихся от обычных. Стиль аварийных случаев рассматривает их как непредсказуемые ситуации, когда все другие механизмы не работают. Объектный подход rescue/retry, описанный ранее, тяготеет к стилю аварийных случаев, хотя может использоваться и для первого стиля. Обработка исключений в языке Ada больше ориентирована на стиль управляющей структуры.
Каждый должен решить, какой стиль ему больше нравится. Но в любом случае, следует помнить, что не нужно наивно возлагать надежды на использование исключений. Есть механизм исключений или его нет, ошибки при выполнении программы - это факт жизни системы, и она должна их явно обрабатывать. Хороший методологический подход, поддерживаемый эффективным механизмом исключений, может быть полезным. Но проблеме обработки ошибок присуща природная сложность, и никакая волшебная палочка от нее не избавит.

Задачи

Кроме пакетов, Ada предлагает еще одну интересную модульную конструкцию - задачу. Задачи - это основной механизм Ada для обработки параллелизма. Лежащая в основе модель параллелизма близка к подходу ВПП, описанному в лекции о параллелизме. Задачи заслуживают упоминания за свои концепции модульности, которые ближе чем пакеты совпадают с объектными понятиями.
Синтаксически, задачи имеют много общего с пакетами. Главное различие в том, что задача - это не просто единица модульности, но и представление процесса, выполняемого параллельно с другими процессами. Поэтому подобно классу (и в отличие от пакета) задача является как синтаксической так и семантической конструкцией.
Как и пакет, задача имеет интерфейс и тело. Вместо подпрограмм, спецификация задачи вводит несколько входов (entry). Для клиента входы выглядят как процедуры, например, интерфейс задачи, управляющей буфером, может выглядеть так:

task BUFFER_MANAGER is
    entry read (x: out G);
    entry write (x: in G);
end BUFFER_MANAGER;

(Задачи не могут быть универсальными, так что тип G должен быть глобально доступным, или родовым параметром охватывающего пакета.) Только реализация входов отличает их от процедур: команды accept, появляющиеся в теле, будут специфицировать синхронизацию и другие ограничения на выполнение записей. Здесь, например, можно предписать, чтобы только одно read или write действовало в любой момент времени, чтобы read ожидало, пока буфер не станет непустым, а write - пока он не будет неполным.
Кроме отдельных задач, можно специфицировать тип задачи и использовать его для создания стольких задач - экземпляров типа задачи - сколько требуется во время выполнения. Это делает задачи похожими на классы без наследования. Действительно, можно представить реализацию в Ada ОО-концепций, представляющих классы типами задач, а объекты - их экземплярами (возможно, даже используя команды accept с различными условиями для эмуляции динамического связывания.) Поскольку в последовательном ОО-вычислении можно ожидать, что классы будут иметь много экземпляров, это упражнение представляет в основном академический интерес, учитывая издержки создания нового процесса в текущих операционных системах. Возможно, когда-нибудь наступит день, и широкомасштабные параллельные среды оборудования станут явью...

От Ada 83 к Ada 95

Версия языка Ada 95 предусматривает добавление ОО-концепций. В ней нет понятия класса в нашем смысле слова (модуль плюс тип), но есть поддержка наследования и динамического связывания для типов записей.

ОО-механизмы языка Ada 95: пример

Текст ниже приведенного пакета иллюстрирует некоторые технические приемы Ada 95. Его смысл должен быть достаточно ясен для читателя. Для получения нового типа с дополнительными полями (форма наследования Ada 95), нужно объявить уже существующий тип, такой как ACCOUNT, как дескрипторный (tagged). Это, конечно, противоречит принципу Открыт-Закрыт, поскольку необходимо знать заранее, какие типы могут иметь потомков, а какие - нет. Множественное наследование отсутствует, так что тип new можно получить только из одного типа. Обратите внимание на синтаксис получения нового типа без добавления атрибутов (null record, к удивлению, без end).

package Accounts is
    type MONEY is digits 12 delta 0.01;
    type ACCOUNT is tagged private;
        procedure deposit (a: in out ACCOUNT; amount: in MONEY);
        procedure withdraw (a: in out ACCOUNT; amount: in MONEY);
        function balance (a: in ACCOUNT) return MONEY;
    type CHECKING_ACCOUNT is new ACCOUNT with private;
        function balance (a: in CHECKING_ACCOUNT) return MONEY;
    type SAVINGS_ACCOUNT is new ACCOUNT with private;
        procedure compound (a: in out SAVINGS_ACCOUNT; period: in Positive);
private
    type ACCOUNT is tagged
        record
            initial_balance: MONEY := 0.0;
            owner: String (1..30);
        end record;
    type CHECKING_ACCOUNT is new ACCOUNT with null record;
    type SAVINGS_ACCOUNT is new ACCOUNT with
        record
            rate: Float;
        end record;
end Accounts;

Дескрипторные типы по-прежнему объявляются как записи. Основное свойство большинства ОО-языков - операции над типом являются частью типа и фактически определяют тип - здесь не работает. Подпрограммы задаются вне объявления типа и принимают в качестве аргумента значение типа. (В ОО-языках, deposit и т. д. будут частью объявления ACCOUNT, а compound - частью SAVINGS_ACCOUNT, им не требуются их первые аргументы.) Здесь же все, что требуется, - так это объявление подпрограмм и типа как части одного и того же пакета; им даже не нужно находиться рядом друг с другом. В приведенном примере, только расположение показывает читателю, что определенные программы концептуально связаны с определенными дескрипторными типами записей.
Это отличается от обычного взгляда на создание ОО ПО. Хотя дескрипторный тип и связанные с ним подпрограммы с теоретической точки зрения являются частью одного абстрактного типа данных, они не образуют синтаксической единицы. Это противоречит принципу Лингвистических Модульных Единиц, предполагающему тесную связь между модульной концепцией и синтаксической структурой.
Появление нового объявления для balance в SAVINGS_ ACCOUNT сигнализирует о переопределении. Процедуры withdraw и deposit не переопределяются. Как будет понятно, это означает, что Ada 95 использует механизм перегрузки для получения ОО-эффекта от переопределения подпрограмм. Не существует синтаксической метки (как redefine), сигнализирующей о переопределении. Чтобы увидеть, что функция balance в SAVINGS_ACCOUNT отличается от базовой версии в ACCOUNT, следует просмотреть весь текст пакета. В данном случае каждая версия подпрограммы находится рядом с соответствующим типом, с отступами для выделения этой связи, но это условность стиля, а не правило языка.
Дескрипторный тип может объявляться как abstract, соответствуя понятию отложенного класса. Подпрограмму также можно сделать abstract, не создавая для нее тело.
Функция, возвращающая результат абстрактного типа, должна сама быть абстрактной. Это правило сначала может показаться странным и помехой для написания эффективной функции, возвращающей, скажем, вершину стека фигур в предположении, что тип FIGURE абстрактный. В языке Ada, однако, результат такой функции обычно будет принадлежать не типу FIGURE, а "типу доступа", описывающему ссылки на экземпляры FIGURE. Так что можно будет написать эффективную функцию.
К сущностям дескрипторного типа можно применить динамическое связывание, как в следующем примере:

procedure print_balance (a: in ACCOUNT'Class) is
            -- Печать текущего баланса.
    begin
        Put (balance (a));
        New_Line;
     end print_balance;

Динамическое связывание следует задать явным образом. Подпрограмма объявляется как "выходящая за рамки класса" (classwide operation) заданием классификатора 'Class для типа аргумента. Это напоминает объявление в C++ любой динамически связываемой функции как "виртуальной". Только здесь клиент выбирает статическое или динамическое связывание.
Ada 95 позволяет определить "дочерний пакет" A1.B существующего пакета A. Это дает новому пакету возможность получить свойства из A и добавить свои собственные расширения и модификации. (Это понятие, конечно, близко к наследованию, но отличается от него.) Вместо объявления трех типов счетов в одном пакете, возможно, лучше было бы разделить пакет на три, где Accounts.Checking представляет CHECKING_ACCOUNT и его подпрограммы, а Accounts.Saving делает то же для SAVINGS_ACCOUNT.

Ada 95 и объектная технология: оценка

Если рассматривать язык Ada 95 с позиций объектной технологии, то сначала он может привести в замешательство. Со временем, освоив различные языковые механизмы, можно добиться эффекта единичного наследования, полиморфизма и динамического связывания.
Однако цена этого - сложность. К сложному языку Ada 83 добавился новый набор понятий со многими внутренними связями и связями со старыми конструкциями. При сравнении с ОО-методом, где введено достаточно простое понятие класса, обнаружится, что в Ada 95 нужно изучить, по крайней мере, пять сложных понятий:

  • пакеты, являющиеся модулями, но не типами, могут быть родовыми, предлагая нечто похожее на наследование: дочерние пакеты (как и ряд других возможностей, не описанных подробно выше, таких как возможность объявления дочернего пакета как private);
  • дескрипторные типы записей, являющиеся типами, но не модулями и имеющие некоторую форму наследования, хотя в отличие от классов они не позволяют синтаксического включения подпрограмм в объявление типа;
  • задачи, являющиеся модулями, но не типами и не имеющие наследования;
  • типы задач, являющиеся модулями и типами, но без возможности быть родовыми (хотя они могут включаться в родовые пакеты) и не имеющие наследования;
  • "защищенные типы" (понятие, до сих пор не встречавшееся), являющиеся типами и включающие подпрограммы, что делает их похожими на классы, но без наследования:
    protected type ANOTHER_ACCOUNT_TYPE is
        procedure deposit (amount: in MONEY);
        function balance return MONEY;
    private
        deposit_list: ...; ...
    end ANOTHER_ACCOUNT_TYPE;

Комбинация возможностей взаимодействия поразительна. Например, пакеты имеют, в добавление к понятию дочернего пакета, механизмы Ada use и with. В одном из руководств дается следующее объяснение:
Закрытые потомки предназначены для "внутренних" пакетов, которые должны применять механизм with только к ограниченному числу пакетов. Закрытый потомок может применить механизм with только к телу своего родителя или к его потомкам. В обмен на такое ограничиние потомок получает новые полномочия: его спецификация автоматически видима в открытых и закрытых частях спецификаций всех его предков.
Без сомнения, можно уловить смысл подобных объяснений. Но стоит ли результат усилий?


Интересно отметить, что Жан Ичбиа, создатель языка Ada, публично покинул аналитическую группу Ada 95 после тщетных попыток сохранить расширения простыми. В его пространном заявлении об уходе дается следующий комментарий: дополнительные возможности приведут в результате к огромному увеличению сложности в 9X [позже Ada 95]... В 9X количество рассматриваемых взаимодействий приближается к 60000.

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

Обсуждение: наследование модулей и типов

При изучении языка Ada 95 попутно интересно отметить, что разработчики Ada 95 считали необходимым помимо механизма наследования для дескрипторных типов ввести понятие пакета потомка. Язык Ada, конечно, всегда разделял понятия модуля и типа, в то время как классы объединяют эти два понятия. Но методологи языка Ada 95 предлагают при введении типа наследника, такого как SAVINGS_ACCOUNТ, объявлять его в целях ясности и модульности не в первоначальном пакете (Accounts), а в пакете потомка. Если обобщить этот совет, то дойдет до создания, наряду с иерархией типов, иерархии модулей, строго ему следующей.
У классов в объектной технологии такие вопросы не возникают. Классы являются модулями, и существует только одна иерархия.
Выбор, сделанный в Ada 95, является еще одним примером популярного взгляда, что "следует отделять наследование типа от повторного использования кода". Понимание же объектной технологии, начиная с языка Simula, заключается в соединении понятий - модуля и типа, подтипов и модульного расширения. Как и любое другое смелое соединение понятий, считавшихся ранее совершенно различными, эта идея могла временами пугать, но без нее мы бы лишились замечательного упрощения архитектуры ПО.

Вперед к ОО-языку Ada

Язык Ada 95 кажется сложным. Но это не значит, что сама идея создания ОО-языка Ada обречена. Просто следует ставить реальные цели и постоянно заботиться о простоте и состоятельности. Сообщество Ada может снова попытаться разработать ОО-расширение, сопровождающееся удалением некоторых возможностей. Возможны два общие направления:

  • Первая идея, близкая по духу к замыслу Ada 95, состоит в сохранении пакетной структуры и введении понятия класса, обобщающего типы записей Ada, с поддержкой наследования и динамического связывания. Но это должны быть действительные классы, включающие применимые подпрограммы. Такое расширение, в принципе, подобно расширению, ведущему от C к C++. Оно должно стремиться к минимализму, пытаясь применять как можно шире уже существующие механизмы (такие как with и use для пакетов), не вводя новых возможностей, приводящих потом к проблемам взаимодействия, упоминаемых Ичбиа.
  • Другой подход может строиться на замечании, сделанном при представлении задач в данной лекции. Отмечалось, что типы задач близки по духу к классам, поскольку они могут иметь экземпляры, созданные во время выполнения. Структурно они обладают многими свойствами пакетов. Можно было бы ввести модуль, имеющий, грубо говоря, синтаксис пакетов и семантику классов. Можно думать о нем как о пакет-классе, или о типе задач, необязательно являющихся параллельными. Понятие "защищенного типа" может стать отправной точкой, будучи интегрировано в существующий механизм.

Упражнения в конце данной лекции предлагают исследовать эти возможности.

Ключевые концепции

  • Язык Ada, изучаемый как представитель класса "инкапсулирующих языков", включающего также Modula-2, предлагает конструкции модульной декомпозиции - пакеты (и задачи).
  • Внимание уделяется скрытию информации: интерфейс и реализация объявляются отдельно.
  • Универсальность увеличивает гибкость пакетов.
  • Конфликты между методологическими и реализационными требованиями порождают "закрытый" раздел - концептуально секретный, но синтаксически включаемый в интерфейс.
  • Пакет - это чисто синтаксический механизм. Модули отделены от типов. Невозможен никакой механизм наследования.
  • Исключения отделяют обнаружение ошибок от их обработки, но не дают чудесного решения проблемы ошибок времени выполнения.
  • Механизм исключений Ada должен использоваться только дисциплинированным путем; любое выполнение обработчика исключений должно приводить либо к повтору операции, либо к появлению исключения в вызывающей программе.
  • Типы задач могут, в принципе, использоваться для реализации классов без наследования, но это решение непрактично в современном окружении.
  • Ada 95 делает возможным определение нового типа, порожденного существующим типом с поддержкой переопределения подпрограмм, полиморфизма и динамического связывания.

Упражнения

У15.1 Как выиграть, не используя скрытия

Проблема компиляции пакетов Ada, приведшая к появлению закрытого раздела в интерфейсе, в равной степени затрагивает и ОО-языки, если среда программирования поддерживает независимую компиляцию классов. В действительности, проблема кажется более серьезной из-за наследования: объявленная переменная типа C, может во время выполнения ссылаться на экземпляры не только типа C, но и любого класса-наследника. Поскольку любой наследник может добавить свои атрибуты, размер этих экземпляров различен. Если C - отложенный класс, невозможно даже присвоить его экземплярам размер по умолчанию. Объясните, почему, несмотря на эти замечания, ОО-нотация этой книги не нуждается в языковой конструкции, подобной механизму private языка Ada. (Подсказка: Ваши рассуждения должны рассматривать, в частности, следующие понятия: расширенные типы в сравнении со ссылочными типами, отложенные классы и технические приемы, используемые в нашем ОО-каркасе для создания спецификации абстрактных классов, не требующие от автора классов ш_ написания двух отдельных частей модуля.) Обсудите компромиссы того и другого решения. Можете ли Вы предложить другие подходы к решению проблемы каркаса языка Ada?

У15.2 Родовые параметры подпрограммы

Родовые параметры пакетов Ada могут быть не только типами, но и подпрограммами. Объясните релевантность этой возможности для реализации ОО-понятий и ее ограничения. (См. также приложение В.)

У15.3 Классы как задачи (для программистов Ada)

Перепишите класс COMPLEX как тип задачи Ada. Приведите примеры, использущие результирующий тип.

У15.4 Добавление классов к Ada

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

У15.5 Пакеты-классы

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

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика