Основы объектно-ориентированного проектирования

         

Анализ зависимостей


Как это и должно быть в любой современной среде разработки, процесс перекомпиляции является автоматическим. Вы просто нажимаете кнопку Melt в инструментарии Project Tool, описанном ниже, и механизмы тихо определят набор элементов, подлежащих перетрансляции. Нет никакой потребности в файлах Make, а нотация не содержит понятия "include file".

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

Для снижения времени в качестве единицы перекомпиляции выбирается не класс, а отдельная подпрограмма.

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



Bench и процесс разработки


Центральное место занимает Bench - графическое рабочее место для компиляции, просмотра классов и их компонентов, документирования, выполнения, отладки. Разработчик системы постоянно взаимодействует с Bench.

Пока Вы плавите и замораживаете, Вы можете оставаться в Bench. При выполнении заключительной компиляции (она запускается нажатием соответствующей кнопки, хотя для этой операции и многих других доступны и неграфические команды) на выходе формируется программа на C, компилируемая далее в машинный код для соответствующей платформы. Замораживание также использует промежуточный код на C. Использование C имеет несколько преимуществ. Язык C доступен практически на всех платформах. Низкий уровень языка позволяет писать код, учитывающий возможности реализации на разных платформах. Компиляторы C производят собственную обширную оптимизацию. Два других преимущества заслуживают особого внимания:

Благодаря исходному коду на C среду можно использовать для кросс-платформенной разработки, компилируя код на другой платформе. Это особенно полезно для создания встроенных систем, для которых типично использование различных платформ для разработки и для выполнения.Использование C способствует реализации открытости, обсуждаемой ранее, в особенности упрощаются интерфейсы к программам, написанным на C, C++ и производных от них.

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

В случае кросс-разработки встроенной системы можно обеспечить минимальный размер системы, исключив, например, ввод-вывод.

На рис. 18.2 показана общая схема среды разработки, где в центре находится рабочее место разработчика.



Библиотеки


Перечень библиотек приведен на рис. 18.2. Они играют значительную роль в процессе разработки ПО, обеспечивая разработчиков богатым набором (несколько тысяч классов) компонентов повторного использования. Набор библиотек содержит:

Библиотеки Base, включающие около 200 классов. Они содержат фундаментальные структуры данных - списки, таблицы, деревья, стеки, очереди, файлы и т. д. Наиболее фундаментальные классы составляют библиотеку Kernel, регулируемую международным стандартом (ELKS).Графические библиотеки: Vision для независимого от платформы графического интерфейса пользователя, WEL для Windows, MEL для Motif, PEL для OS/2-Presentation Manager.Net для клиент-серверных разработок позволяет передавать по сети объекты произвольной сложности. Платформы могут быть одинаковыми или разными (использование independent_store делает формат независимым от платформы).Lex, Parse для анализа языка. Parse, в частности, обеспечивает интересный подход к синтаксическому анализу, основанному на последовательном приложении ОО-концепций к синтаксическому анализу (каждое правило моделируется с помощью класса, см. библиографию). Общедоступное средство YOOCC служит препроцессором для Parse.Math - библиотека, обеспечивающая ОО-представление фундаментальных численных методов. Она основана на библиотеке NAG и охватывает большой набор средств. Некоторые из ее концепций были представлены в лекции 13 курса "Основы объектно-ориентированного программирования", как пример ОО-изменения архитектуры не ОО-механизмов.


Рис. 18.3.  Работа с кластером, классом и компонентом в Case (Вариант для Sun Sparcstation с Motif, доступны версии для Windows и других операционных систем)

ObjEdit обеспечивает возможность редактирования объектов в интерактивном режиме в процессе выполнения.Web поддерживает обработку форм, отправленных клиентами на Web-сервер с целью создания CGI-скриптов.

В нижней части рис. 18.2 показаны библиотеки, используемые для поддержки сохраняемости во время выполнения. Класс STORABLE и дополнительные инструментальные средства, обсуждаемые ранее, поддерживают хранение, поиск и передачу по сети объектных структур в соответствии с принципом Замыкания Сохраняемости. Библиотека Store обеспечивает интерфейс с базами данных, реализуя механизмы доступа и сохранения данных в реляционных (Oracle, Ingres, Sybase) и ОО-базах данных.

Этот список не является исчерпывающим, постоянно разрабатываются новые компоненты. Ряд коммерческих и свободно распространяемых библиотек создан пользователями среды.

Особый интерес представляет совместное использование Net, Vision and Store для формирования клиент-серверных систем. Сервер обеспечивает работу базы данных с помощью Store и выполняет громоздкие вычисления, используя Base, Math и т. д. Тонкие клиенты, использующие Vision (или одну из библиотек для конкретных платформ), обеспечивают практически только интерфейс пользователя.



Инструментальные средства


На рис. 18.2 представлена общая организация среды. Сама среда, конечно, написана в OO-нотации (за исключением некоторых элементов системы поддержки выполнения), это делает ее превосходной системой отладки технологии и живым доказательством масштабируемости и реализуемости больших, амбициозных систем. Конечно, мы не хотели бы их разрабатывать иным способом!



Рис. 18.5.  Project Tool в процессе компиляции

Class Tool может быть нацелен на конкретный класс, например, LIST (рис. 18.6).


Рис. 18.6.  Class Tool, вид по умолчанию

Feature Tool совместно с Project Tool во время сеанса отладки (рис. 18.7) показывают компонент и ход выполнения с механизмами пошаговой отладки, отображают состояние стека (см. значения локальных сущностей в Project Tool). Feature Tool нацелен на компонент call_this_routine класса TEST.


Рис. 18.7.  Отладка в Project и Feature Tool

В процессе выполнения можно следить за отдельным объектом с помощью инструментария Object Tool, показанного на рис. 18.8.

В окне отображаются различные поля объектов. Одно из них, guardian, содержит непустую ссылку, следуя которой можно просмотреть соответствующий объект, как вскоре будет показано.

Можно использовать столько экземпляров Class Tool, Feature Tool и Object Tool, сколько необходимо, но только один System Tool и один Project Tool доступен в течение сеанса.


Рис. 18.8.  Объект и его поля в процессе выполнения


Инструментальные средства высокого уровня


В верхней части рис. 18.2 присутствуют два инструментальных средства высокого уровня.

Build - интерактивный генератор приложений, основанный на модели Контекст-Событие-Команда-Состояние (см. лекцию 14). Его можно использовать для визуальной разработки графического интерфейса пользователя (GUI) в интерактивном режиме.

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


Рис. 18.2.  Общая структура среды

Разрабатывать системные структуры в графической среде, создавая визуальные представления классов ("пузырьки") и определяя их клиентские отношения и отношения наследования с помощью стрелок с последующей группировкой их в кластеры. В конце Case сгенерирует соответствующие программные тексты (прямое проектирование - forward engineering).Обработать существующий текст класса и воспроизвести соответствующее графическое представление, облегчая анализ и реструктурирование (обратное проектирование - reverse engineering).

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

Обозначения в Case заимствованы из BON (см. лекцию 9). BON поддерживает возможность изменения масштаба (zooming). Это существенно для больших систем, разработчики могут работать со всей системой, подсистемой, с одним небольшим кластером, точно выбирая необходимый уровень абстракции.

На рис. 18.3 приведен пример работы с Case, показан кластер описания химического предприятия, свойства одного из его классов (VAT) и свойства одного из компонентов этого класса (fill).





Язык


Язык - это нотация, введенная в лекциях 7-18 курса "Основы объектно-ориентированного программирования" и применяемая в ней. Мы по существу полностью ее рассмотрели за исключением нескольких технических деталей, таких как представление специальных символов.



Компоненты среды


Среда1) объединяет следующие элементы:

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

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



Оптимизация


Для максимальной реализации цели C1 одного замораживания кода недостаточно. Для получения законченной, устойчивой системы требуется дополнительная оптимизация:

Удаление мертвого кода, то есть любых подпрограмм, которые никогда не вызываются, прямо или косвенно, из корневой процедуры создания системы. Это особенно важно, если использовано много предкомпилированных библиотек. Выигрыш в размере системы нередко составляет около 50%.Статическое связывание, автоматически выполняемое компилятором для компонентов, не являющихся полиморфными или не переопределяемых потомками.Подстановка кода подпрограмм.

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

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

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



Открытость


Одно из предназначений языка программирования состоит в использовании его для обертывания компонентов, написанных на других языках. Механизм включения внешних элементов с помощью предложения external был описан ранее. Библиотека Cecil позволяет внешнему ПО использовать ОО-механизмы: создавать экземпляры классов и вызывать компоненты этих объектов через динамическое связывание (конечно, при ограниченном статическом контроле типов).

Особый интерес представляют интерфейсы с языками C и C++. Для C++ доступно средство под названием Legacy++, позволяющее на основе существующего класса C++ создать обертывающий класс (wrapper class), автоматически инкапсулирующий все экспортируемые компоненты оригинала. Это особенно полезно для организаций, которые использовали C++ как первую остановку на пути к ОО и теперь хотят без потерь инвестиций перейти к законченной и систематической форме объектной технологии. Инструментарий Legacy++ сглаживает такой переход.



Перенастройка и просмотр


Существуют разные способы перенастройки инструмента, например перенастройка Class Tool с LIST на ARRAY. Можно просто ввести новое имя класса в соответствующее поле (если Вы точно его не помните, то можно использовать символ подстановки "*" - ARR* для получения меню со списком соответствующих имен).

Можно также использовать кратко представленный ранее механизм pick-and-throw1 ("выбрать и перетащить") (см. лекцию 15 курса "Основы объектно-ориентированного программирования"). Если щелкнуть правой кнопкой мыши на имени класса, например, на CHAIN в Class Tool настроенном на класс LIST, то курсор превратится в "камешек" (pebble) в форме эллипса, показывая, что выбран класс. Далее нужно выбрать "лунку" (hole) такой же формы в Class Tool (том же самом или другом) и положить камешек в лунку, щелкнув правой кнопкой мыши. В качестве лунки может выступать кнопка на панели инструментов или клиентская область окна соответствующего инструментального средства.


Рис. 18.9.  Пример pick-and-drop ("выбрать и переложить")

Механизм pick-and-drop - обобщение drag-and-drop. Вместо необходимости постоянно удерживать нажатую кнопку операция разбивается на три шага. Сначала выбирается объект щелчком правой кнопки мыши, появляется камешек. Далее в режиме перетаскивания камешек постоянно связан нитью с выбранным элементом


(рис. 18.9). Наконец, еще один щелчок правой кнопкой мыши уже в целевой лунке. Можно назвать три преимущества по сравнению с обычным drag-and-drop:

Нет необходимости постоянно держать нажатой кнопку мыши. При частом выполнении операций drag-and-drop в конце рабочего дня возникает значительная мышечная усталость.При ослаблении давления на кнопку на долю секунды операция может завершиться в неправильном месте, часто с неприятными или катастрофическими последствиями. (Это случилось с автором в Windows 95 при перетаскивании значка, представляющего файл. Потом пришлось долго выяснять, что с этим файлом произошло.)Обычный drag-and-drop не позволяет отменить операцию! Как только объект выбран, с ним необходимо что-то сделать.
В механизме pick-and- drop щелчком левой кнопки операцию можно отменить в любой момент.Следует особо отметить, что механизм типизирован, камешек можно положить только в соответствующую лунку. Допускаются некоторые отклонения: аналогично тому, как полиморфизм позволяет присоединить объект RECTANGLE к сущности POLYGON, можно положить компонент в лунку класса и увидеть соответствующий класс с подсвеченным компонентом. Это еще один пример непосредственного применения концепций метода при построении среды. (Здесь различие с механизмами drag-and-drop не является критическим, поскольку они также могут быть ограниченно типизированными.)Однако все это связано только с интерфейсом пользователя. Более важная роль pick-and-drop проявляется в соединении с другими механизмами среды - поддержка интегрированного набора механизмов для всех задач разработки ПО. Если вновь обратиться к Class Tool, отображающем отложенный класс LIST библиотеки Base (рис. 18.10), то второй сверху ряд кнопок позволяет выбрать формат вывода. Возможные варианты:

class text (текст класса) ;


ancestors (предки) ;


short form (краткая форма) ;


routines (подпрограммы) ;


deferred routines (отложенные подпрограммы)


и так далее. Щелчок на одной из них отобразит текст класса в соответствующем формате. Например, если нажимается кнопка Ancestors (Предки), то Class Tool отобразит структуру наследования (рис. 18.10).


Рис. 18.10.  Родословная класса

В любом окне инструментальных средств все важные элементы интерактивны (clickable). Это означает, что для получения информации о классе CURSOR_STRUCTURE достаточно щелкнуть на нем правой кнопкой мыши и использовать pick-and-drop для перенастройки этого или другого инструментального средства на выбранный класс. После этого можно выбрать другой формат, например краткую форму. Далее можно снова применить pick-and-drop и настроить Feature Tool на интересующую Вас подпрограмму. В Feature Tool можно просмотреть предысторию, то есть все приключения компонента в играх наследования: все версии после переименования, переопределения и т.


д. Для любого упомянутого класса и компонента можно вновь использовать pick-and-drop.

В процессе сеанса отладки, показанного ранее (рис. 18.7), необходимую информацию можно также получить с помощью pick-and-drop. Щелчок правой кнопкой на объекте 0X142F18 (внутренний идентификатор, сам по себе ничего не говорящий, но интерактивный) позволяет запустить Object Tool, использованный для отображения экземпляра PERSON (рис. 18.8). Этот инструментарий обеспечит просмотр всех полей и ссылок объекта, также интерактивных. Так можно легко исследовать структуры данных во время выполнения.

Можно осуществить вывод в каждом из доступных форматов (HTML, TЕX, RTF, FrameMaker MML, troff), причем компактный язык описаний позволяет определить собственные форматы или модифицировать существующие. Вывод может быть отображен, сохранен с файлами класса или в отдельном каталоге для подготовки документации проекта или кластера.

Механизмы просмотра не делают никаких различий между встроенными библиотеками и классами, определенными разработчиком. Если используется базовый класс INTEGER, то его точно так же можно просматривать в Class Tool в любом доступном формате. Автор библиотеки может закрыть доступ к исходному тексту, но краткая и плоско-краткая формы доступны и остаются интерактивными. Это вполне соответствует общим принципам однородности и бесшовности. В течение всех этапов разработки ПО используются единые концепции, насколько это возможно.

Автор пробовал в вышеупомянутой демонстрационной версии Java Workshop получить информацию о переопределенном компоненте класса, выбранного наугад, но получил сообщение, что браузер не может этого сделать. Оказалось, что это компонент предопределенной графической библиотеки. Для получения информации пришлось с помощью другой программы обратиться к документации, содержавшей краткое описание компонента. Просмотр INTEGER был невозможен, поскольку базовые типы Java не являются классами.
Все механизмы времени выполнения, в частности средства отладки (выполнение в пошаговом режиме, точки останова и т.


д.), следуют из основных концепций. Например, для добавления точки останова достаточно "переложить" инструкцию или подпрограмму в лунку Stop Point.



Некоторые лунки, известные как "кнопки-лунки" (buttonholes), одновременно выполняют функции кнопки. Например, щелчок левой кнопкой на лунке Stop Point приведет к отображению в Project Tool информации о точках останова. Это представление тоже интерактивно и позволяет легко удалить существующие точки останова или добавить новые.

Систематическое применение этих методов обеспечивает механизм просмотра, при котором все представляющие интерес элементы являются гиперссылками. Такое решение гораздо предпочтительнее модальных сред, постоянно вынуждающих Вас задавать себе вопросы: "Я просматриваю? О нет, я отлаживаю, так что придется запустить браузер. А какой инструментарий нужно запустить для получения документации?"

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


Платформы


Приведенные иллюстрации получены во время сеанса работы на Sun Sparcstation исключительно по причине удобства. На время написания книги поддерживались другие платформы, включая Windows 95 и Windows NT, Windows 3.1, OS/2, Digital VMS (Alpha и Vax) и все основные версии Unix (SunOS, Solaris, Silicon Graphics, IBM RS/6000, Unixware, Linux, Hewlett-Packard 9000 Series и т. д.).

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

На рис. 18.4 представлен набор окон среды во время работы. Рисунок черно-белый, но в реальной среде активно используется цвет, особенно для синтаксического выделения различных частей текстов класса. По умолчанию ключевые слова выделены синим цветом, идентификаторы - черным, комментарии - красным. Пользователь может изменить эти настройки.



Предкомпиляция


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

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

В новую систему можно включить неограниченное число откомпилированных библиотек. Механизм объединения таких библиотек поддерживает совместное использование. Если две откомпилированные библиотеки B и C ссылаются на A (например, графическая библиотека Vision и клиент-серверная библиотека Net, обсуждаемые далее, обе используют библиотеку структур данных и фундаментальных алгоритмов Base), то только одна копия A включается в систему.

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



Развитие


Первая реализация языка выпущена в конце 1986 г. Единственный существенный пересмотр (1990 г.) не затронул никаких фундаментальных концепций, но упростил выражение некоторых из них. С тех пор ведется постоянная работа по уточнению и упрощению, затрагивающая только детали. Два недавних расширения связаны с механизмом параллелизма (рассмотренным в лекции 12, где добавлено единственное ключевое слово separate) и конструкцией Precursor для облегчения переопределения. Стабильность языка, редкое явление в этой области, была для пользователей среды одним из важных преимуществ.



Реализация интерфейса


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

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



Технология компиляции


Первой задачей среды разработки является выполнение ПО.



Технология тающего льда


Для решения указанных проблем технология компиляции, известная как Технология тающего льда (Melting Ice Technology), использует сочетание дополняющих друг друга методов. Откомпилированную систему называют замороженной, уподобляя ее куску льда в морозильной камере. Образно говоря, чтобы начать работу над системой, ее нужно достать из холодильника и немного подогреть. Растаявшие элементы представляют собой изменения. Эти элементы не станут причиной цикла "перекомпиляция - сборка" для удовлетворения требования C2. Вместо этого "растаявший" код будет непосредственно обрабатываться исполняющей машиной, встроенной в окружение.

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

Быстрая перекомпиляция. Типичное время ожидания - несколько секунд.


Рис. 18.1.  "Замороженная" и "растаявшая" части системы

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

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



Требования к компиляции


Технология компиляции разрабатывалась и совершенствовалась многие годы для решения следующих задач:

C1 Эффективность сгенерированного кода должна быть сопоставимой с эффективностью, достигаемой при использовании классического языка типа C. Нет никаких причин платить за OO-методы снижением производительности.C2 Время перекомпиляции после внесения изменений должно быть коротким. Точнее, оно должно быть пропорционально объему изменений, а не размеру полной системы. Важнейшим требованием для разработчиков, создающих большие системы, является возможность немедленно увидеть результаты сразу после внесения изменений.C3 Третье требование появилось позже, но становится важным для пользователей: поддержка быстрой доставки приложений через Internet для непосредственного выполнения.

Согласовать два первых требования очень трудно. Требование C1 обычно обеспечивается путем экстенсивной оптимизации, в результате приводящей к замедлению перекомпиляции и компоновки. Интерпретирующие среды хорошо соответствуют C2, выполняя ПО "на лету" после минимальной обработки, но приносят в жертву производительность (C1) и статический контроль типов.



Удаленное выполнение


Интерпретируемый код, сгенерированный после таяния, традиционно известный как байт-код, является независимым от платформы. Для выполнения байт-кода достаточно иметь копию Исполняющей Машины (Execution Engine), известной как 3E и свободно загружаемой через Интернет.

Установка 3E в качестве дополнительного модуля (plug-in) Web-браузера дает возможность непосредственного выполнения кода. 3E автоматически выполнит соответствующий код при активизации пользователем гиперссылки, соответствующей байт-коду. Этот механизм удаленного выполнения стал популярен благодаря Java.

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

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



Извлечения из библиотек Base


В течение всего нашего обсуждения мы неоднократно встречались со ссылками на базовые библиотеки Base, в особенности на библиотеку ядра Kernel, в которой сгруппированы наиболее фундаментальные классы.

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

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

Детальное представление библиотек дано в [M 1994a], где также описаны теоретические предпосылки - принципы общей таксономии, используемые для классификации основных структур данных в информатике. Некоторые из основных идей обсуждались при расмотрении наследования.

К наиболее важным классам, чьи концепции обсуждались в предыдущих лекциях и чьи тексты находятся на CD-ROM, относятся:

ARRAY, описывающий одномерные массивы, основанный на гибком и общем виде этого понятия (в частности, массивы могут динамически изменять размер во время выполнения системы).LINKABLE, описывающий элементы списковых структур с одной связью.BI_LINKABLE, эквивалент элементов с двумя связями.LIST, отложенный класс, представляющий общее понятие списка как "активной структуры данных" с курсором без обязательств конкретного представления. Следующие три классса обеспечивают специальные реализации списка, используя множественное наследование и технику "брака по расчету".ARRAYED_LIST, представляющий реализацию списка массивом, чья возможность динамического изменения размера здесь существенно используется.LINKED_LIST, представляющий реализацию односвязным списком и внутренне использующий для элементов класс LINKABLE.TWO_WAY_LIST, двусвязный список, использующий класс BI_LINKABLE.TWO_WAY_TREE, широко использующий реализацию деревьев, основанную на классе TWO_WAY_LIST для представления деревьев, учитывающую сделанное ранее наблюдение в лекции о множественном наследовании. Его суть в том, что при слиянии общих понятий дерева и узла дерева можно полагать, что дерево одновременно является списком и элементом списка.

Все эти классы, представляющие контейнеры, явялются универсальными с единственным родовым параметром, задающим тип элементов.



Эмуляция наследования с помощью универсальности


Являются ли наследование и универсальность взаимозаменяемыми? Давайте рассмотрим возможность эмуляции каждой из этих техник средствами другой техники.

Рассмотрим сначала язык, подобный Ada (Ada 83), с поддержкой универсальности, но не наследования. Что можно сделать в этом случае для достижения эффекта наследования?

Простой путь - перегрузка имен. Как известно, Ada допускает многократное использование одних и тех же имен подпрограмм для операндов различных типов. Значит можно определить типы TAPE, DISK и другие, каждый с его собственной версией подпрограмм:

procedure open (p: in out TAPE; descriptor: in INTEGER); procedure close (p: in out DISK);

Никакая двусмысленность не возникнет, если подпрограммы отличаются, по крайней мере, типом одного операнда. Но это решение не поддерживает полиморфизм и динамическое связывание. Как добиться различного результата вызова d.close после присваиваний d := di и d := ta, где di - DISK, а ta - TAPE?

Для получения такого эффекта придется использовать записи с вариантными полями:

type DEVICE (unit: DEVICE_TYPE) is record ... Поля одинаковые для устройств всех типов ... case unit is when tape => ... поля для ленточных накопителей ...; when disk => ... поля для дисковых накопителей ...; ... Другие варианты ...; end case end record

где DEVICE_TYPE - перечислимый тип с элементами tape, disk и т. д. Тогда для каждого усройства можно определить индивидуальную версию каждой процедуры (open, close и т. д.)

case d'unit is when tape => ... действия для ленточных накопителей ...; when disk => ... действия для дисковых накопителей ...; ... другие варианты ...; end case

Здесь каждый случай явно выделен и список выбора закрыт, поэтому для добавления новых вариантов выбора придется внести изменения во все аналогичные подпрограммы. Такая программная архитектура явно противоречит принципу Единственного выбора.

Следовательно, ответ на вопрос, поставленный в данном разделе, отрицательный:

Эмуляция наследования

Эмуляция наследования с помощью универсальности не представляется возможной.



Эмуляция ограниченной универсальности: обзор


Довольно естественной является идея связывания ограниченного формального родового параметра с некоторым классом, в котором определены ограничивающие операции. Этот класс можно рассматривать как АТД. Расмотрим наши два примера Ada с ограниченными родовыми параметрами - minimum and matrices:

generic type G is private; with function "<=" (a, b: G) return BOOLEAN is <> generic type G is private; zero: G; unity: G; with function "+"(a, b: G) return G is <>; with function "*"(a, b: G) return G is <>;

Можно рассматривать эти предложения как определения двух абстрактных типов данных - COMPARABLE и RING_ELEMENT. Первый характеризуется наличием операции сравнения "<= ", а второй компонентами zero, unity, + and *.

На ОО языке такие типы могут быть непосредственно представлены как классы. Определить эти классы полностью невозможно, так как нет универсального решения для операций "<= ", "+" и т.д. Следовательно, необходимо использовать абстрактные классы, возложив детали реализации на их потомков:

deferred class COMPARABLE feature infix "<=" (other: COMPARABLE): BOOLEAN is deferred end end deferred class RING_ELEMENT feature infix "+" (other: like Current): like Current is deferred ensure equal(other, zero) implies equal(Result, Current) end; infix "*" (other: like Current): like Current is deferred end zero: like Current is deferred end unity: like Current is deferred end end

В отличие от Ada, ОО-нотация позволяет описывать абстрактные семантические свойства, хотя в данный пример включено только одно (постусловие x + 0 = x при любом x для операции infix "+").

Использование закрепленных типов (like Current) позволяет избежать недопустимых комбинаций, как поясняется в следующем примере COMPARABLE. На этом этапе замена всех таких типов для RING_ELEMENT не оказывала бы эффекта.



Эмуляция универсальности с помощью наследования


Обратимся теперь к решению обратной задачи: можно ли добиться эффекта универсальности в стиле Ada средствами наследования ОО-языка.

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

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



Наследование


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

class DEVICE feature open (file_descriptor: INTEGER) is do ... end close is do ... end opened: BOOLEAN end

Пример использования этого класса:

d1: DEVICE; f1: INTEGER; ... create d1.make; d1.open (f1); if d1.opened then ...

Теперь рассмотрим понятие ленточного накопителя. Это устройство обладает всеми свойствами, представленными в классе DEVICE, плюс способность перематывать ленту. Вместо формирования нового класса на пустом месте можно использовать наследование и объявить класс TAPE, расширяя и модифицируя DEVICE. Новый класс расширяет DEVICE, добавляя новую процедуру rewind в соответствии с особенностями именно ленточных устройств. Кроме того необходима новая версия open, модифицированная с учетом специфики накопителей на магнитной ленте.

Объекты типа TAPE автоматически обладают всеми свойствами объектов DEVICE плюс их собственные (перемотка). Придется модифицировать ряд компонентов DEVICE в классе TAPE (open) и добавить новые (rewind). Класс DEVICE может иметь и других потомков, например класс DISK с его собственными специфическими особенностями прямого доступа.

Наследованию сопутствует полиморфизм, разрешая присваивания x:= y, если тип x - предок типа y. Следующая особенность - динамическое связывание: если x устройство, то вызов x.open (f1) будет выполнен различным образом в зависимости от значения присвоенного x перед вызовом. После присваивания x := y, где y - лента, будет выполнена версия открытия для ленты.

Как уже указывалось, преимуществами наследования являются возможность повторного использования и расширяемость. Ключевым является принцип Открыт-Закрыт: программный элемент типа DEVICE пригоден как для непосредственного использования, так и в качестве предка новых классов.


Далее следуют отложенные компоненты и классы. Нужно отметить, что устройства Unix являются файлами специального типа, поэтому DEVICE может быть потомком класса FILE, другими потомками которого могут быть TEXT_FILE и BINARY_FILE. На рис. B.1 приведен граф наследования, в данном случае дерево наследования.


Рис. B.1.  Простая иерархия наследования с отложенными и эффективными классами

Открыть и закрыть можно любой файл, но способ выполнения этих операций зависит от того, является ли файл устройством, подкаталогом и т. д. Следовательно, FILE - абстрактный класс с отложенными подпрограммами open и close, реализация которых возлагается на потомков:

deferred class FILE feature open (file_descriptor: INTEGER) is deferred end close is deferred end; endЭффективные потомки FILE обеспечат реализацию open и close.


Неограниченная универсальность


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

procedure swap (x, y) is local t; begin t := x; x := y; y := t; end swap;

В этой форме не специфицируются типы обмениваемых элементов и локальной переменной t. Здесь слишком много свободы, так вызов swap (a, b), где a имеет тип integer, а b - character string, не будет отвергнут, хотя и приведет к ошибке.

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

procedure G_swap (x, y: in out G) is t: G; begin t := x; x := y; y := t; end swap;

Требование определенности типа G предотвращает ошибки несовместимости, но в постоянном споре между безопасностью и гибкостью пострадала гибкость в угоду безопасности. Теперь для элементов каждого типа необходима новая процедура, например INTEGER_swap, STRING_swap и так далее. Такие множественные объявления удлиняют и затеняют программы. Выбранный пример особенно показателен, так как все объявления подобных процедур будут отличаться лишь двумя вхождениями G.

Статическая типизация в данном случае накладывает избыточные ограничения. Единственное реальное требование - идентичность типов фактических параметров и локальной переменной t. Конкретный тип не имеет значения.

В дополнение к этому аргументы должны иметь статус in out, чтобы процедура могла изменить их значения. Это разрешено в Ada.

Универсальность обеспечивает компромисс между избыточной свободой бестиповых языков и излишней строгостью, свойственной Pascal. В родовых языках можно объявить G как родовой параметр процедуры swap или охватывающего модуля.
Язык Ada предлагает как родовые подпрограммы, так и родовые пакеты, описанные в лекции 15 курса "Основы объектно-ориентированного проектирования". На квази-Ada можно написать так:

generic type G is private; procedure swap (x, y: in out G) is t: G; begin t := x; x := y; y := t; end swap;Единственное отличие от реальной записи на Ada выражается в необходимости отделения интерфейса от реализации. Поскольку скрытие информации несущественно для обсуждения в этой лекции, интерфейсы и реализации объединены для простоты представления.

Предложение generic... вводит тип в качестве параметра. Определяя G как "private", автор процедуры позволяет применять к сущностям типа G (x, y, t) операции, применимые ко всем типам, такие как присваивание или сравнение, и только их.

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

procedure int_swap is new swap (INTEGER); procedure str_swap is new swap (STRING);и т. д. Если теперь i и j переменные типа INTEGER, а s и t - STRING, то из следующих вызовов:

int_swap (i, j); str_swap (s, t); int_swap (i, s); str_swap (s, j); str_swap (i, j);допустимы только два первых, а остальные будут отклонены компилятором.

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

generic type G is private; package QUEUES is type QUEUE (capacity: POSITIVE) is private; function empty (s: in QUEUE) return BOOLEAN; procedure add (t: in G; s: in out QUEUE); procedure remove (s: in out QUEUE); function oldest (s: in QUEUE) return G; private type QUEUE (capacity: POSITIVE) is -- Пакет использует массив для представления очереди record implementation: array (0 .. capacity) of G; count: NATURAL; end record; end QUEUES;Здесь опять-таки определен не пакет, а шаблон пакета.


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

package INT_QUEUES is new QUEUES (INTEGER); package STR_QUEUES is new QUEUES (STRING);Родовое объявление позволяет достичь компромисса между типизированным и бестиповым подходом. QUEUES - шаблон для модулей, реализующих очереди, элементы которых могут принадлежать всем возможным типам G. При этом для конкретного G сохраняется возможность контроля соответствия типов, исключающего такие безобразные комбинации, как вставка целого числа в очередь строк.

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

Зачастую универсальное определение имеет смысл, только если фактические параметры удовлетворяют некоторым условиям. Эту форму можно назвать ограниченной универсальностью.


Ограниченная универсальность


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

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

generic type G is private; function minimum (x, y: G) return G is begin if x <= y then return x; else return y; end if; end minimum;

Такое объявление функции имеет смысл только для таких типов G, для которых определена операция сравнения "<=". При статическом контроле типов соответствие этому требованию необходимо проверить на этапе компиляции, не дожидаясь выполнения. Нужен способ проверки того, поддерживается ли данная операция для типа G.

В Ada сама операция <= трактуется как родовой параметр. Синтаксически, операция - это функция, которую можно вызывать, используя обычную инфиксную форму, если в объявлении ее имя размещено в двойных кавычках - "<= ". Следующее объявление становится допустимым в Ada, объединив интерфейс и реализацию.

generic type G is private; with function "<=" (a, b: G) return BOOLEAN is <>; function minimum (x, y: G) return G is begin if x <= y then return x; else return y end if; end minimum;

Ключевое слово with вводит родовые параметры, представляющие подпрограммы, аналогичные "<=".

Родовое порождение minimum можно выполнить для любого типа T1, если для него определена функция T1_le с сигнатурой: function (a, b: T1) return BOOLEAN.

function T1_minimum is new minimum (T1, T1_le);

Если функция T1_le действительно называется "<=", точнее, если ее название и сигнатура соответствуют шаблону, то ее включение в список фактических параметров не требуется. Так, поскольку тип INTEGER имеет предопределенную функцию "<=" с правильной сигнатурой, то можно просто объявить:

function int_minimum is new minimum (INTEGER);

Такое использование заданных по умолчанию подпрограмм с соответствующими именами и типами возможно благодаря предложению is <> в объявлении формальной подпрограммы.
Разрешенная и фактически поощряемая в Ada перегрузка операций играет существенную роль, и функция "<=" определена для различных типов.

От обсуждения ограниченной универсальности для подпрограмм легко перейти к пакетам. Предположим, что требуется универсальный пакет для работы с матрицами объектов любого типа G, где над матрицами определены операции суммирования и умножения. Такое определение имеет смысл, только если эти операции определены для типа G и каждая из этих операций имеет нулевой элемент. Эти свойства необходимы для реализации операций над матрицами. Интерфейсная часть пакета может быть написана следующим образом:

generic type G is private; zero: G; unity: G; with function "+"(a, b: G) return G is <>; with function "*"(a, b: G) return G is <>; package MATRICES is type MATRIX (lines, columns: POSITIVE) is private; function "+"(m1, m2: MATRIX) return MATRIX; function "*"(m1, m2: MATRIX) return MATRIX; private type MATRIX (lines, columns: POSITIVE) is array (1 .. lines, 1 .. columns) of G; end MATRICES;Вот типичные родовые порождения:

package INTEGER_MATRICES is new MATRICES (INTEGER, 0, 1); package BOOLEAN_MATRICES is new MATRICES (BOOLEAN, false, true, "or", "and");Для типа INTEGER опущены фактические параметры + и *, поскольку определены соответствующие операции. Однако их пришлось явно указать в случае BOOLEAN. (Параметры, опускаемые по умолчанию, лучше всего помещать в конец списка формальных параметров.)

Интересно рассмотреть реализацию такого пакета:

package body MATRICES is ... Остальные объявления ... function "*"(m1, m2: G) is result: MATRIX (m1'lines, m2'columns); begin if m1'columns /= m2'lines then raise incompatible_sizes; end if; for i in m1'RANGE(1) loop for j in m2'RANGE(2) loop result (i, j):= zero; for k in m1'RANGE(2) loop result (i, j):= result (i, j) + m1 (i, k) * m2 (k, j) end loop; end loop; end loop; return result end "*"; end MATRICES;В этом фрагменте использованы некоторые специфические особенности Ada:



Для параметризованных типов, подобных MATRIX (lines, columns: POSITIVE), объявление переменной должно сопровождаться фактическими параметрами, например mm: MATRIX (100, 75). Далее можно получить их значения, используя нотацию с апострофом: mm'lines в этом случае имеет значение 100.Если a - массив, то a'RANGE(i) обозначает диапазон значений в его i-ом измерении; например, m1'RANGE(1) в приведенном примере - то же самое, что и 1.. m1'lines.Если перемножаются две несовместимые по размерности матрицы, то возбуждается исключение.Приведенные примеры демонстрируют реализацию ограниченной универсальности в Ada. Они также показывают серьезные ограничения этой техники: выразимы только синтаксические ограничения. Программист может потребовать только существования некоторых подпрограмм (<=, +, *) с заданной сигнатурой, но, если эти подпрограммы не удовлетворяют семантическим ограничениям, эти объявления становятся бессмысленными. Функция minimum имеет смысл, только если <= является отношением полного порядка на G. Для родового порождения MATRICES с заданным типом G, следует быть уверенным, что операции + и * имеют не только сигнатуру G x G

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


Ограниченная универсальность: пакеты


Предыдущая дискуссия переносится и на пакеты. Для эмуляции абстракции матриц, которую Ada реализует пакетом MATRICES, можно использовать класс:

class MATRIX feature anchor: RING_ELEMENT is do end implementation: ARRAY2 [like anchor] item (i, j: INTEGER): like anchor is -- Значение элемента с индексами (i, j) do Result := implementation.item (i, j) end put (i, j: INTEGER; v: like anchor) is -- Присвоить значение v элементу с индексами (i, j) do implementation.put (i, j, v) end infix "+" (other: like Current): like Current is -- Матричная сумма текущей матрицы matrix и other local i, j: INTEGER do create Result.make (...) from i := ... until ... loop from j := ... until ... loop Result.put ((item (i, j) + other.item (i, j)), i, j) j := j + 1 end i := i + 1 end end infix "*"(other: like Current): like Current is -- Матричное произведение текущей матрицы и other local ... do ... end end

С типом аргумента put и результата item связана интересная проблема: он должен быть RING_ELEMENT, но соответствующим образом переопределен в классах-потомках. Закрепленное объявление дает решение проблемы, но здесь, на первый взгляд, нет атрибута, который мог бы послужить якорем. Это не должно нас останавливать: следует объявить искусственный якорь, называемый anchor. Его единственное предназначение - быть переопределенным в подходящий тип потомка RING_ELEMENT будущими потомками MATRIX (например, BOOLEAN_RING в BOOLEAN_MATRIX и т. д.). Во избежание потерь памяти в экземплярах anchor объявляется как функция, а не как атрибут. Техника искусственного якоря полезна для сохранения согласованности типов, когда, как в данном случае, нет естественного якоря среди атрибутов класса.

Некоторые детали цикла, также как и тело инфиксной операции *, остались вне рассмотрения, но дополнить их просто. Компоненты put и item, применяемые в реализации, пришли из библиотечного класса ARRAY2, описывающего двумерные массивы.

Для определения эквивалента родового пакета Ada, показанного ранее:


package BOOLEAN_MATRICES is new MATRICES (BOOLEAN, false, true, "or", "and");следует прежде всего объявить соответствующее булево кольцо:

class BOOLEAN_RING_ELEMENT inherit RING_ELEMENT redefine zero, unity end creation put feature -- Initialization put (v: BOOLEAN) is -- Инициализация значением v do item := v end feature -- Access item: BOOLEAN feature -- Basic operations infix "+" (other: like Current): like Current is -- Булево сложение: or do create Result.put (item or other.item) end infix "*"(other: like Current): like Current is -- Булево умножение: and do create Result.put (item and other.item) end zero: like Current is -- Нулевой элемент булева кольца для сложения once create Result.put (False) end unity: like Current is -- Нулевой элемент для булева умножения once create Result.put (True) end endЗаметьте, ноль и единица реализуются однократными функциями.

Тогда для получения родового порождения пакета Ada следует просто определить наследника BOOLEAN_MATRIX от MATRIX, где нужно только переопределить anchor - искусственный якорь; все остальные типы будут следовать автоматически:

class BOOLEAN_MATRIX inherit MATRIX redefine anchor end feature anchor: BOOLEAN_RING_ELEMENT endЭта конструкция достигает эффекта ограниченной универсальности благодаря использованию наследования, подтверждая для пакетов результаты эмуляции, проиллюстрированные ранее для подпрограмм.


Ограниченная универсальность: подпрограммы


Мы можем написать подпрограмму, такую как minimum, указав тип COMPARABLE для ее аргументов. Основываясь на образце Ada, функция была бы объявлена следующим образом:

minimum (one: COMPARABLE; other: like one): like one is -- Минимальное из one и other do ... end

При ОО-разработке каждая подпрограмма появляется в классе и связывается с текущим экземпляром класса. Включив minimum в класс COMPARABLE, аргумент one станет неявным текущим экземпляром. Класс будет выглядеть так:

deferred class COMPARABLE feature infix "<=" (other: like Current): BOOLEAN is -- Текущий объект меньше или равен other? deferred end minimum (other: like Current): like Current is -- Минимальное из двух значений: текущего -- и other do if Current <= other then Result := Current else Result := other end end end

Для вычисления минимума двух элементов необходимо объявить их тип как эффективного потомка COMPARABLE, с заданной реализацией операции сравнения <=, например:

class INTEGER_COMPARABLE inherit COMPARABLE creation put feature -- Initialization put (v: INTEGER) is -- Инициализация значением v. do item := new end feature -- Access item: INTEGER; -- Значение, связанное с текущим объектом feature -- Basic operations infix "<=" (other: like Current): BOOLEAN is -- Текущий объект меньше или равен other? do Result := (item <= other.item) end; end

Для нахождения минимума двух целых теперь можно применять функцию minimum к сущностям ic1 и ic2, чьи типы не INTEGER, а INTEGER_COMPARABLE:

ic3 := ic1.minimum (ic2)

Для использования родовых функций infix <= и minimum придется исключить прямые ссылки на целые, заменив их сущностями INTEGER_COMPARABLE, поскольку этого требует атрибут item и подпрограмма put. Более того, придется вводить подобных потомков COMPARABLE, таких как STRING_COMPARABLE и REAL_COMPARABLE, для каждого типа, требующего своей версии minimum.

Заметьте, механизм закрепленных объявлений является основой обеспечения корректности. Если бы аргумент minimum в COMPARABLE был бы объявлен как COMPARABLE, а не like Current, то следующий вызов был бы синтаксически допустим:


ic1.minimum (c)если c принадлежал бы типу COMPARABLE, но не был бы типом INTEGER_COMPARABLE. Понятно, что такой вызов мог быть некорректным. Все это применимо и к RING_ELEMENT.

Объявление компонентов item и put для всех потомков COMPARABLE, жертвуя при этом прямым использованием простых типов, конечно же, неприятно. При этом приходится идти на потерю производительности: вместо манипулирования целыми или строками приходится использовать объекты обертывающих типов, таких как INTEGER_COMPARABLE. Но, заплатив эту цену - простоту использования и эффективность, мы приобретаем полную эмуляцию ограниченной универсальности средствами наследования. (В заключительной нотации, конечно, ничего платить не требуется.)

Эмуляция ограниченной универсальности (1)

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


Сочетание универсальности и наследования


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

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

Закрепленные объявления решают вторую проблему. (Читатель, знакомый с лекцией 17 курса "Основы объектно-ориентированного программирования", где детально обсуждались проблемы типизации, мог заметить важность проблемы проверки правильности системы, но эти вопросы исчезнут в окончательном варианте, представленном ниже.)

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



УB.1 Искусственные якоря


Искусственный якорь anchor объявлен как атрибут класса MATRIX и потому требует в период выполнения выделения для экземпляров класса дополнительной (небольшой) памяти. Возможно ли избежать этих потерь, объявив якорь однократной функцией, чье тело может быть пустым, так как фактически она никогда не будет вычисляться? (Подсказка: рассмотрите правила типов.)



УB.2 Бинарные деревья и бинарные деревья поиска


Напишите универсальное "бинарное дерево"-класс BINARY_TREE. Бинарное дерево задается информацией в корне дерева и двумя возможными поддеревьями, левым и правым. Затем рассмотрите "бинарное дерево поиска", для всех узлов которого выполняется следующее условие: информация в узле больше или равна информации в корне левого поддерева, но меньше информации в корне правого поддерева. Это означает задание полного порядка на "информациях". Напишите класс BINARY_SEARCH_TREE, реализующий это понятие как потомка BINARY_TREE. Сделайте класс универсальным, насколько это возможно. Клиенты должны использовать класс для произвольных типов, задающих информацию и специфическое отношение порядка.



УB.3 Более просто используемые матрицы


Добавьте в последнюю версию класса MATRIX две функции - для доступа и модификации элементов, которые в противоположность item и put будут позволять клиентам манипулировать матрицами типа MATRIX [G] в терминах элементов типа G, а не типа RING_ELEMENT [G].



Универсальность и (versus) наследование


Последующий материал и его появление в приложении требует некоторых пояснений. Начальным толчком, приведшим в итоге к появлению этой книги, было исследование, проведенное в 1984 году при подготовке курса для студентов "Концепции в языках программирования", в котором я сравнивал "горизонтальный" механизм универсальности с "вертикальным" механизмом наследования, введенным в Simula. Первый механизм модульного расширения рассматривался на примере родовых языков, таких как Ada, Z, LPG. Анализировалось, чем отличаются эти техники, в чем они соревнуются, в чем дополняют друг друга. Это привело к статье с одноименным данному приложению названием [M1986], представленной на конференции OOPSLA, и к главе в первом издании этой книги.

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

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

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



УВ.4 Полная реализация очередей


Расширьте пример с очередью, определив отложенный класс QUEUE, дополнив класс этого приложения (называемый теперь ARRAYED_QUEUE, наследуемый от QUEUE и ARRAY, с подходящимми постусловиями). Добавьте класс LINKED_QUEUE для реализации связного списка (основанный на наследовании от LINKED_LIST и QUEUE).