Rationale for Ada 2005: Object oriented model
RUSTOPBACKNEXT
ENG |
4. Interfaces
@ In Ada 95, a derived type can really only have one immediate ancestor. This means that true multiple inheritance is not possible although curious techniques involving discriminants and generics can be used in some circumstances General multiple inheritance has problems. Suppose that we have a type T with some components and operations. Perhaps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rationale for Ada 2005: Object oriented model
@ENGRUSTOPBACKNEXT4. Интерфейсы
@ В Аде 95 дочерний тип может иметь только одного непосредственного предка. Это означает, что истинное многократное наследование не возможно, хотя любопытные методики с использованием дискриминантов и настраиваемых средств могут использоваться при определённых обстоятельствах, но тем не менее, многократное наследование имеет проблемы. Предположим, что мы имеем тип T с некоторыми компонентами и операциями:
|
@ Предположим, что мы получаем два новых типа из T следующим образом:
|
@ Теперь предположим, что мы могли бы получить дальнейший тип TT из T1 и T2:
|
@ Это ещё самый простой пример, который можно было бы вообразить. Мы не добавили никаких дополнительных компонентов или операций. Но что ТТ унаследовал бы от своих двух родителей? Ясно, что запись не может иметь две компоненты с одинаковыми идентификаторами, Она имеет только одну компоненту A и одну компоненту B. Но что относительно C? Наследуется символ или цвет? И действительно ли это незаконно из-за противоречий? Предположим, что T2 имел бы компоненту D вместо C. Всё при этом было бы в порядке? ТТ тогда имел бы четыре компоненты? Теперь рассмотрим операции. По-видимому, TT имеет и Op1 и Op2. Но какую из Op1? Действительно ли это - первоначальная Op1 унаследованная из T через T1 или заменённая (overriding) версия унаследованная из T2? Ясно это TT не может иметь их обоих. Но нет никакой причины почему он не может иметь и Op3 и Op4, унаследованные каждая от своего родителя.
@ Проблемы возникают при наследовании компонентов от больше чем одного родителя и наследовании различных реализаций одной и той же операции от больше чем одного родителя. Но нет никаких проблем с наследованием только спецификации операций от двух родителей.
@ Последнее наблюдения подсказывает суть решения. Самое большее один родитель может иметь компоненты, и самое большее один родитель может иметь конкретные операции - для простоты мы делаем их тем же самым родителем. Но абстрактные операции могут быть унаследованы от нескольких родителей. Это может быть выражено утверждением, что этот вид множественного наследования - о слиянии контрактов, которые будут удовлетворены вместо того, чтобы слить алгоритмы или состояния.
@ Так Ада 2005 вводит понятие интерфейса, который является теговым типом без компонентов и конкретных операций. Также вводится понятие пустой процедуры как операции тегового типа; такая процедура не имеет никакого тела, но ведет себя, как будто имеет тело с единственным оператором null. Таким образом, интерфейсам разрешают иметь только абстрактные подпрограммы и пустые процедуры.
@ Мы покажем способы, которыми интерфейсы могут быть объявлены и составлены символическим способом и затем закончим более практическим примером.
@ Объявим пакет Pi1 содержащий интерфейс Int1:
|
@ Обратите внимание на синтаксис применения нового зарезервированного слова interface. Здесь не указывается ключевое слово tagged, хотя все типы интерфейса теговые. Процедура Op1 явно объявлена абстрактной как её и положено. Пустая процедура N1 также использует новый синтаксис. Напомним, что пустая процедура ведет себя, как будто её тело включает единственное null утверждение; но она фактически не имеет конкретного тела.
@ Основное правило наследования типов следущее: теговый тип может иметь или не иметь в качестве предка один обычный теговый тип и иметь несколько интерфейсных типов либо не иметь их вовсе. Таким образом, мы можем записать:
|
@ где Int1 и Int2 - интерфейсные типы. Нормальный теговый тип, если он имеется, должен быть указан на первом месте в объявлении. Первый тип считается родителем, таким образом, родитель может быть обычным теговым типом или интерфейсом. Другие типы считаются прародителями. Дополнительные компоненты и операции наследуются обычным способом.
@ Термин прародители (progenitors) может показаться странным, но термин предки (ancestors) в этом контексте тоже был бы ещё более запутывающим. Но новый термин был всё же необходим. Термин 'прародители' происходит из латинского слова 'progignere' - порождать, что весьма отражает суть обозначаемого явления.
@ Возможно, думалось, что будет вполне выполнимо избежать формального введения понятия интерфейса, просто разрешив множественное наследование при условии, что только первому родителю разрешено имееть компоненты и конкретные операции. Однако, это бы значительно увеличило сложности выполнения с риском нарушения секретности и дополнительные накладные расходы. Кроме того, это вызвало бы проблемы обслуживания, начиная с простого добавления компонентов к типу, или создания одного из его абстрактных конкретных операций, вызвало бы ошибки в другом месте в системе, если бы он использовался как вторичный родитель. Таким образом, намного лучше обработать интерфейсы как существенно новое понятие. Другое преимущество состоит в том, что это обеспечивает новый класс настраиваемых параметров более аккуратно без сложных правил для реализаций.
@ Если нормальный теговый тип T, находится в пакете Pt с операциями Opt1, Opt2 и так далее, тогда мы можем бы написать:
|
@ Мы конечно, должны обеспечить конкретную процедуру для Op1, унаследованную от интерфейса Int1, так как мы объявили NT как конкретный тип. Мы могли также обеспечить замену (overriding) для N1, но если мы не делаем это, тогда мы просто наследуем пустую процедуру Int1. Мы могли также заменить (override) унаследованные из T операции Opt1 и Opt2 обычным способом.
@ Интерфейсы могут быть составлены из других интерфейсов следующим образом:
|
@ Рассмотрим синтаксис. Объявление тегового типа имеет атрибут interface либо tagged with (в противном случае это не теговый тип). Когда мы наследуем интерфейсы таким образом мы можем добавить новые операции так, что новый интерфейс Int4 будет иметь все операции из Int1 и Int2 плюс возможно объявленные операции непосредственно в Int4. Все эти операции должны быть абстрактными или пустыми, и есть довольно очевидные правила относительно того, что случается, если имеются два или больше интерфейса имеющих одну и ту же операцию. Таким образом, пустая процедура заменяет абстрактную, в противном случае одинаковые операции должны иметь совместимые конфигурации.
@ Мы ссылаемся ко всем интерфейсам в интерфейсном списке как на прародителей. Таким образом Int1 и Int2 - прародители Int4. Не родители, ибо этот термин используется только при получении типа, а не интерфейса.
@ Заметим, что термин предок охватывает все поколения, тогда как родитель и прародители - только первое поколение.
@ Подобные правила применяются, когда теговый тип получен из другого типа плюс один или более интерфейсов как в случае типа NT:
|
@ В этом случае могло бы быть, что T уже имеет некоторые операции из Int1 и/или Int2. Если так, тогда операции T должны соответствовать таковым из Int1 или Int2 (быть совместимого типа и т.д).
@ Мы неофициально говорим об определенном теговом типе как о осуществлении интерфейса, из которого это унаследовано (прямо или косвенно). Фраза "осуществление интерфейса" не используется официально в определении Ады 2005, но это полезно в целях обсуждения.
@ Таким образом, в вышеупомянутом примере теговый тип NT должен реализовать все операции интерфейсов Int1 и Int2. Если тип T уже реализует некоторые из операций, тогда тип NT реализует их автоматически, потому что он унаследует реализацию от T. Он может конечно заменить такие унаследованные операции обычным способом.
@ Нормальные "go abstract" правила применяется в случае функций. Таким образом, если одна операция - функция F то:
|
@ и T уже имеет подобную операцию:
|
@ тогда в этом случае тип NT должен обеспечить конкретную функцию F. См. однако обсуждение в конце этой статьи для случая когда тип NT имеет нулевое расширение.
@ Надклассовые типы также применяется к интерфейсным типам. Надклассовый тип Int1'Class покрывает все типы, унаследованные из интерфейса Int1 (и другие интерфейсы, такие как нормальные теговые типы). Мы можем тогда использовать объект конкретного тегового типа в том классе обычным способом, так как мы знаем, что любая абстрактная операция Int1 будет заменена (overridden). Таким образом, мы могли бы иметь:
|
@ Заметим, что преобразование разрешается между ссылкой на надклассовый тип Int1_Ref и любым ссылочным типом, который наследуется из интерфейсного типа Int1.
@ Интерфейсы могут также использоваться в приватных секциях и как настраиваемые параметры.
@ Таким образом:
|
@ Важное правило относительно приватных секций состоит в том, что полное представление и частичное представление должны соответствовать набору интерфейсов, которые они осуществляют. Таким образом, хотя родитель в полном представлении не должен быть T, но может быть любым типом, унаследованным из T, то же самое не верно для интерфейсов, которые оба должны осуществлять один и тот же набор. Это правило необходимо для воспрепятствования клиентским типам заменять приватные операции родителя, если клиент осуществляет интерфейс добавленный в приватной секции.
@ Настраиваемые параметры принимают форму:
|
@ и затем фактический параметр должен быть интерфейсом, который реализует всех предков Int1, Int2 и т.д. Формальным мог быть только тип FI являющийся интерфейсом; когда как фактический параметр может быть любым интерфейсом. Мог бы быть подпрограммами, которые передают как дальнейшие параметры, которые будут требовать, чтобы фактические имели определенные операции. Интерфейсы Int1 и Int2 могли бы непосредственно быть формальными параметрами, описанные ранее в списке параметров.
@ Интерфейсы (и формальные интерфейсы) могут также быть ограничеными (это обозначается таким образом: type LI is limited interface;) Мы можем составить целый набор из ограниченных и неограниченных интерфейсов, но если хотя бы один из них неограничен, тогда получающийся интерфейс не должен быть определен как ограниченный. Это потому что должны осуществляться операции равенства и назначения, подразумеваемые неограниченным интерфейсом. Подобные правила относятся к типам, которые осуществляют один или более интерфейсов. Мы возвратимся к этой теме через мгновение.
@ Есть другие формы интерфейсов, а именно, синхронизированные (synchronized) интерфейсы, интерфейсы задач, и защищенные (protected) интерфейсы. Они поддерживают полиморфизм, надклассовое объектно-ориентированное программирование в реальном времени. Они будут описаны в позже.
@ Описав общие идеи в символических терминах, мы теперь обсудим более конкретный пример.
@ Перед тем как мы начнём, важно подчеркнуть, что интерфейсы не могут иметь компоненты и поэтому если мы должны выполнить многократное наследование, тогда мы должны думать в терминах абстрактных операций чтения и записи компонентов, а не о компонентах непосредственно. Это - стандарт OOП мышления, потому что это сохраняет абстракцию, скрывая детали выполнения.
@ Таким образом, вместо того, чтобы иметь компоненту типа Comp лучше иметь пару операций. Функцию для чтения компоненты можно просто назвать Comp. Процедура для обновления компоненты могла бы называться Set_Comp. Мы будем в основном использовать это соглашение, хотя не всегда правильно обрабатывать компоненты как несвязанные объекты.
@ Предположим теперь, что мы хотим представить изображения геометрических объектов. Пусть корневой тип объявлен так:
|
@ Тип Object является частным, и по умолчанию обе координаты имеют значение нуль. Процедура Move, которая является надклассовой, перемещает любой объект в указанное место.
@ Предположим также, что мы имеем пакет для рисования со следующей спецификацией:
|
@ Идея этого пакета состоит в том, чтобы представить изображение как набор линий. Атрибуты изображения - оттенок и ширина линий и есть пара подпрограмм чтобы установить и читать эти свойства любого объекта интерфейса Printable и его потомков. Эти операции конечно абстрактны.
@ Чтобы привести объект к форме, которая может быть напечатана, он должен быть преобразован в набор линий. Функция To_Lines преобразовывает объект типа Printable в набор линий; она также является абстрактной. Детали различных типов, типа Line и Line_Set не показаны.
@ Наконец, пакет Line_Draw объявляет конкретную процедуру Print, которая берет объект типа Printable'Class и делает фактический рисунок, используя подчиненную процедуру Draw_It объявленную в частной секции. Отметим, что процедура Print - надклассовая и конкретная. Это - важный момент. Хотя все примитивные операции интерфейса должны быть абстрактными, это не относится к надклассовым операциям, так как они не примитивные.
@ Тело процедуры Print могло бы иметь вид:
|
@ но всё это скрыто от пользователя. Отметим, что процедура Draw_It объявлена в частной части, так как она не должна быть видимой пользователю.
@ Одна из причин, почему пользователь должен обеспечить процедуру To_Lines состоит в том, что только пользователь знает о деталях как лучше всего представить объект. Например, круг может быть представлен грубо как многоугольник из 100 сторон.
@ Мы можем иметь по крайней мере два различных подхода. Например, мы можем написать:
|
@ Тип Printable_Object является потомком и Object и Printable и все конкретные типы потомков Printable_Object будут поэтому иметь все операции и Object и Printable. Обратите внимание, что мы должны поместить Object сначала в объявлении Printable_Object и что следующее было бы незаконным:
|
@ Это потому, что только первый тип в списке может быть нормальным теговым типом; любые другие должны быть интерфейсами. Напомним, что первый тип всегда известен как родительский и в этом случае здесь родительским типом является - Object.
@ Тип Printable_Object объявлен как абстрактный, потому что мы не хотим реализовать To_Lines на данном этапе. Однако мы можем обеспечить конкретные подпрограммы для всех других операций интерфейса Printable. Мы дали типу частное расширение, и в частной секции пакета мы могли бы иметь:
|
@ Только для иллюстрации компонентам дали значения по умолчанию. В теле пакета - операции типа функции Hue простые:
|
@ К счастью, правила видимости не позволяют бесконечной рекурсии! Отметим, что информация содержащая компоненты стиля находится в структуре записи после геометрических свойств. Это - простая линейная структура, так как интерфейсы не могут добавлять компоненты. Однако, начиная с типа Printable_Object имеет все операции и Object и Printable, это немного усложняет расположение таблиц отправки. Но эта деталь скрыта от пользователя.
@ Ключевой пункт состоит в том, что теперь мы можем передать любой объект типа Printable_Object или его потомков процедуре:
|
@ и затем (как выделено выше) в пределах Print мы можем определить цвет функцией Hue и ширину линии функцией Width, и мы можем преобразовать объект в ряд линий, вызывая функцию To_Lines.
@ И теперь мы можем объявить различные типы Circle, Triangle, Square и так далее, делая их потомками типа Printable_Object, и в каждом случае мы будем иметь соответствующую реализацию функции To_Lines.
@ Неудачный аспект этого подхода состоит в тоим, что мы должны будем нарушить геометрическую иерархию. Например, пакет треугольника мог бы теперь быть:
|
@ Мы можем теперь объявить Printable_Triangle таким образом:
|
@ Здесь объявляется равносторонний треугольник со сторонами длинной 4.0. Его частные компоненты Hue и Width устанавливаются по умолчанию. Его координаты, которые также являются частными, по умолчанию устанавливаются в нуль. (Читатель может улучшить пример, делая компоненты A, B и C также частными.) Мы можем свободно переместить его в любое место процедурой Move, каторая будучи надклассового типа применяется ко всем типам, унаследованным из Object. Таким образом, мы можем написать:
|
@ И теперь мы можем сделать красный признак
|
@ Объявив объект Sign, Мы можем назначить ему ширину и оттенок:
|
@ Поскольку мы заметили ранее, что этот подход имеет недостаток, из-за того что мы должны были изменить геометрическую иерархию. Другой подход, который позволяет избежать этого, состоит в том, чтобы объявить пригодные для печатания объекты только тех видов, которые мы хотим.
@ Предположим, что мы имеем пакет Line_Draw и первоначальный пакет Geometry и его дочерние пакеты. Предположим, что мы хотим сделать пригодные для печатания треугольники и круги. Мы могли бы написать:
|
@ и тело пакета обеспечит различные тела подпрограмм.
@ Теперь предположим, что мы уже имеем нормальный треугольник:
|
@ Чтобы напечатать A_Triangle, мы сначала должны объявить пригодный для печатания треугольник:
|
@ и теперь мы можем установить компоненты треугольника используя преобразование представления:
|
@ и теперь мы можем написать:
|
@ Второй подход вероятно лучше, так как он не требует изменения геометрической иерархии. Обратной стороной этого является то, что мы должны объявить подпрограммы для оттенка и ширины неоднократно. Мы можем сделать это намного проще, объявляя настраиваемый пакет следующим образом:
|
@ Этот настраиваемый пакет может использоваться, чтобы сделать любой тип пригодным для печатания. Мы просто пишем:
|
@ Реализация пакета создает тип Printable_T, который имеет все операции для оттенка и ширины и необходимые дополнительные компоненты. Однако, он просто наследует абстрактную функцию To_Lines и, таким образом, непосредственно должен быть абстрактным типом. Отметим, что функция To_Lines должна быть специально закодирована для каждого типа индивидуально кроме оттенка и операций ширины, которые могут быть одни на всех.
@ Теперь мы делаем дальнейшее преобразование чтобы дать типу Printable_T необходимое имя Printable_Triangle, и на данном этапе мы обеспечиваем конкретную функцию To_Lines.
@ Мы можем продолжать и далее. Таким образом, настраиваемые средства делают этот процесс очень простым - любой тип может быть сделан пригодным для печатания, только пишущий три линии плюс тело функции To_Lines.
@ Мы надеемся, что этот пример проиллюстрировал множество важных пунктов использования интерфейсов. Здесь ключевая вещь состоит в том, что мы можем использовать процедуру Print, для печати чего - нибудь через интерфейс Printable.
@ Ранее мы обеспечили пару операций, чтобы читать и обновлять свойства, типа Hue и Set_Hue и Width и Set_Width. Это не всегда приемлемо. Таким образом, если мы связали компоненты типа X_Coord и Y_Coord тогда, хотя вполне можно было бы обойтись индивидуальными функциями, несомненно лучше обновлять два значения вместе отдельной процедурой, типа процедуры Move, объявленной ранее. Таким образом, если мы желаем переместить объект из точки (0.0, 0.0) в точку (3.0, 4.0) то сделать это можно двумя вызовами:
|
@ тогда окажется, что он будто бы временно побывал в точке (3.0, 0.0). Здесь имеются и некоторые другие риски. Мы могли бы забыть устанавливать одну из компонент или случайно установить одну и ту же компоненту дважды.
@ Наконец, как обсуждалось ранее, пустые процедуры - новый вид подпрограмм, и определяемые пользователем операции интерфейса должны быть пустыми процедурами или абстрактными подпрограммами - нет конечно такой вещи как пустая функция. (Неограниченные интерфейсы действительно имеют одну конкретную операцию, и это - предопределенное равенство; это могло даже быть отменено с абстрактным.) Null процедуры будут найдены полезными для интерфейсов, но фактически применимы к любым типам. Как пример, пакет Ada.Finalization теперь использует пустые процедуры для Initialize, Adjust и Finalize как описано во Введении.
@ Мы заключаем этот раздел с несколькими дальнейшими замечаниями по ограниченности (limitedness). Мы отметили ранее, что интерфейс может быть явно заявлен как ограниченный, так мы могли бы иметь
|
@ Интерфейс ограничен, только если он имеет атрибут limited (или synchronized и т.д). Как было упомянуто ранее, потомок неограниченного интерфейса должен быть неограничен, так как это должно осуществить назначение и равенство. Так, если интерфейс составлен из набора ограниченных и неограниченных интерфейсов, он должен быть неограничен.
|
@ Другими словами, ограниченность никогда не наследуется от интерфейса, и должна устанавливаться явно. Это относится к композиции типового и интерфейсного наследования. С другой стороны, в случае образования типа ограниченность наследуется от родителя, если это не интерфейс. Это необходимо для совместимости с Адой 95. Пусть мы имеем:
|
@ тогда:
|
@ Последнее незаконно, потому что T, как ожидается, будет ограничен, потому что он наследуется из ограниченного родительского типа LT, и все же он - также потомок неограниченного интерфейса NLI.
@ Чтобы избежать определенных курьёзных трудностей, Ада 2005 требует явной установки атрибута limited при наследовании типа. (Было бы хорошо настаивать на этом всегда для ясности, но такое изменение будет слишком большой несовместимостью.), Если мы действительно заявляем limited явно тогда, родитель должен быть ограничен (не зависимо от того тип это или интерфейс).
@ Использование limited необходимо, если мы желаем получить ограниченный тип из ограниченного интерфейса таким образом:
|
@ Эти правила сводятся к одной вещи. Если родитель или прародитель (действительно предок) неограничены тогда, потомок должен быть неограничен. И наоборот, если тип (включая интерфейс) ограничен, тогда все его предки должны быть ограничены.
@ Более ранняя версия Ады 2005 столкнулась с трудностями в этой области, потому что в случае типа, наследуемого только из интерфейсов, поведение могло зависеть от порядка их появления в списке (потому что правила для родителя и прародителей немного отличны). Но в заключительной версии языка порядок не имеет значения. Так:
|
@ Но следующее конечно незаконно:
|
@ Есть также подобные изменения к настраиваемым формальным параметрам и расширениям типов - Ада 2005 требует указывать атрибут limited явно в обоих случаях.
2010-10-31 15:16:26
. .