Девятая
глава вводит в круг вопросов объектно-ориентированного представления
программных систем. В этой главе рассматриваются: абстрагирование понятий
проблемной области, приводящее к формированию классов; инкапсуляция объектов,
обеспечивающая скрытность их характеристик; модульность как средство упаковки
набора классов; особенности построения иерархической структуры
объектно-ориентированных систем. Последовательно обсуждаются объекты и классы
как основные строительные элементы объектно-ориентированного ПО. Значительное
внимание уделяется описанию отношений между объектами и классами.
Рассмотрение
любой сложной системы требует применения техники декомпозиции — разбиения на
составляющие элементы. Известны две схемы декомпозиции: алгоритмическая
декомпозиция и объектно-ориентированная декомпозиция.
В
основе алгоритмической декомпозиции лежит разбиение по действиям — алгоритмам.
Эта схема представления применяется в обычных ПС.
Объектно-ориентированная
декомпозиция обеспечивает разбиение по автономным лицам — объектам реального
(или виртуального) мира. Эти лица (объекты) — более «крупные» элементы, каждый
из них несет в себе и описания действий, и описания данных.
Объектно-ориентированное
представление ПС основывается на принципах абстрагирования, инкапсуляции,
модульности и иерархической организации. Каждый из этих принципов не нов, но их
совместное применение рассчитано на проведение объектно-ориентированной
декомпозиции. Это определяет модификацию их содержания и механизмов
взаимодействия друг с другом. Обсудим данные принципы [22], [32], [41], [59],
[64], [66].
Аппарат
абстракции — удобный инструмент для борьбы со сложностью реальных систем.
Создавая понятие в интересах какой-либо задачи, мы отвлекаемся (абстрагируемся)
от несущественных характеристик конкретных объектов, определяя только
существенные характеристики. Например, в абстракции «часы» мы выделяем
характеристику «показывать время», отвлекаясь от таких характеристик конкретных
часов, как форма, цвет, материал, цена, изготовитель.
Итак,
абстрагирование сводится к формированию абстракций. Каждая абстракция фиксирует
основные характеристики объекта, которые отличают его от других видов объектов
и обеспечивают ясные понятийные границы.
Абстракция
концентрирует внимание на внешнем представлении объекта, позволяет отделить
основное в поведении объекта.от его реализации. Абстракцию удобно строить путем
выделения обязанностей объекта.
Пример: физический
объект — датчик скорости, устанавливаемый на борту летательного аппарата (ЛА).
Создадим его абстракцию. Для этого сформулируем обязанности датчика:
q
знать проекцию скорости ЛА в заданном направлении;
q
показывать текущую скорость;
q
подвергаться настройке.
Теперь
опишем абстракцию датчика. Описание сформулируем как спецификацию класса на
языке Ada 95 [4]:
Package Класс_ДатчикСкорости is
subtype Скорость is Float range ...
subtype Направление is Natural range ...
type ДатчикСкорости is tagged private;
function НовыйДатчик(нокер: Направление)
return ДатчикСкорости:
function ТекущаяСкорость (the: ДатчикСкорости)
return Скорость;
procedure Настраивать(the: in out ДатчикСкорости;
ДействитСкорость: Скорость);
private — закрытая часть спецификации
--
полное описание типа ДатчикСкорости
end Класс_ДатчикСкорости;
Здесь
Скорость и Направление — вспомогательные подтипы, обеспечивающие задание
операций абстракции (НовыйДатчик, ТекущаяСкорость, Настраивать). Приведенная
абстракция — это только спецификация класса датчика, настоящее его
представление скрыто в приватной части спецификации и теле класса. Класс
ДэтчикСкорости — еще не объект. Собственно датчики — это его экземпляры, и их
нужно создать, прежде чем с ними можно будет работать. Например, можно написать
так:
ДатчикПродольнойСкорости
: ДатчикСкорости;
ДатчикПоперечнойСкорости
: ДатчикСкорости;
ДатчикНормальнойСкорости
: ДатчикСкорости;
Инкапсуляция
и абстракция — взаимодополняющие понятия: абстракция выделяет внешнее поведение
объекта, а инкапсуляция содержит и скрывает реализацию, которая обеспечивает
это поведение. Инкапсуляция достигается с помощью информационной закрытости.
Обычно скрываются структура объектов и реализация их методов.
Инкапсуляция
является процессом разделения элементов абстракции на секции с различной
видимостью. Инкапсуляция служит для отделения интерфейса абстракции от ее
реализации.
Пример: физический
объект регулятор скорости.
Обязанности
регулятора:
q
включаться;
q
выключаться;
q
увеличивать скорость;
q
уменьшать скорость;
q
отображать свое состояние.
Спецификация
класса Регулятор скорости примет вид
with Кяасс_ДатчикСкорости. Класс_Порт;
use Класс_ДатчикСкорости. Класс_Порт;
Package Класс_РегуляторСкорости is
type Режим is (Увеличение, Уменьшение);
subtype Размещение is Natural range ...
type РегуляторСкорости is tagged private;
function НовРегуляторСкорости (номер:
Размещение;
напр: Направление; порт; Порт)
return
РегуляторСкорости;
procedure Включить(the: in out РегуляторСкорости);
procedure Выключить(1пе: in out РегуляторСкорости);
procedure УвеличитьСкорость(1г1е: in out
РегуляторСкорости);
procedure УменьшитьСкорость(the: in out
РегуляторСкорости);
Function OnpocCocтояния(the: РегуляторСкорости)
eturn Режим;
private
type укз_наПорт is access all Порт;
type РегуляторСкорости is tagged record
Номер; Размещение;
Состояние: Режим;
Управление: укз_наПорт;
end record;
end Класс_РегуляторСкорости;
Здесь
вспомогательный тип Режим используется для задания основного типа класса, класс
ДатчикСкорости обеспечивает класс регулятора описанием вспомогательного типа
Направление, класс Порт фиксирует абстракцию порта, через который посылаются
сообщения для регулятора. Три свойства: Номер, Состояние, Управление — формулируют
инкапсулируемое представление основного типа класса РегуляторСкорости. При
попытке клиента получить доступ к этим свойствам фиксируется семантическая
ошибка.
Полное
инкапсулированное представление класса РегуляторСкорости включает описание
реализаций его методов — оно содержится в теле класса. Описание тела для
краткости здесь опущено.
В
языках C++, Object Pascal, Ada 95 абстракции
классов и объектов формируют логическую структуру системы. При производстве
физической структуры эти абстракции помещаются в модули. В больших системах,
где классов сотни, модули помогают управлять сложностью. Модули служат
физическими контейнерами, в которых объявляются классы и объекты логической
разработки.
Модульность
определяет способность системы подвергаться декомпозиции на ряд сильно
связанных и слабо сцепленных модулей.
Общая
цель декомпозиции на модули: уменьшение сроков разработки и стоимости ПС за
счет выделения модулей, которые проектируются и изменяются независимо. Каждая
модульная структура должна быть достаточно простой, чтобы быть полностью
понятой. Изменение реализации модулей должно проводиться без знания реализации
других модулей и без влияния на их поведение.
Определение
классов и объектов выполняется в ходе логической разработки, а определение модулей
— в ходе физической разработки системы. Эти действия сильно взаимосвязаны,
осуществляются итеративно.
В Ada 95 мощным средством обеспечения модульности является
пакет.
Пример: пусть имеется
несколько программ управления полетом летательного аппарата (ЛА) — программа
угловой стабилизации ЛА и программа управления движением центра масс ЛА. Нужно
создать модуль, чье назначение — собрать все эти программы. Возможны два
способа.
1.
Присоединение с помощью указателей контекста:
with Класс_УгловСтабил, Класс_ДвиженЦентраМасс;
use Класс_УгловСтабил,
Класс_ДвиженЦентраМасс;
Package Класс_УпрПолетом is
…
2.
Встраивание программ управления непосредственно в объединенный модуль:
Package Класс_УпрПолетом is
type УгловСтабил is tagged private;
type ДвиженЦентраМасс is tagged private;
-------------------------
Мы
рассмотрели три механизма для борьбы со сложностью:
q
абстракцию (она упрощает представление физического
объекта);
q
инкапсуляцию (закрывает детали внутреннего представления
абстракций);
q
модульность (дает путь группировки логически связанных
абстракций).
Прекрасным
дополнением к этим механизмам является иерархическая организация — формирование
из абстракций иерархической структуры. Определением иерархии в проекте
упрощаются понимание проблем заказчика и их реализация — сложная система
становится обозримой человеком.
Иерархическая
организация задает размещение абстракций на различных уровнях описания системы.
Двумя
важными инструментами иерархической организации в объектно-ориентированных
системах являются:
q
структура из классов («is a»-иерархия);
q
структура из объектов («part of»-иерархия).
Чаще
всего «is а»-иерархическая структура строится с
помощью наследования. Наследование определяет отношение между классами, где
класс разделяет структуру или поведение, определенные в одном другом (единичное
наследование) или в нескольких других (множественное наследование) классах.
Пример: положим, что
программа управления полетом 2-й ступени ракеты-носителя в основном похожа на
программу управления полетом 1-й ступени, но все же отличается от нее.
Определим класс управления полетом 2-й ступени, который инкапсулирует ее
специализированное поведение:
with Класс_УпрПолетом1; use Класс_УпрПолетом1;
Package Класс_УпрПолетом2 is
type Траектория is (Гибкая. Свободная);
type УпрПолетом2 is new УпрПолетом1 with private;
procedure Установиться: in out УпрПолетом2:
тип: Траектория; ориентация : Углы;
параметры: Координаты_Скорость; команды: График);
procedure УсловияОтделенияЗступени (the: УпрПолетом2;
критерий:КритерийОтделения);
function ПрогнозОтделенияЗступени (the: УпрПолетом2)
return БортовоеВремя;
function ИсполнениеКоманд(the: УпрПолетом2)
return Boolean;
private
type УпрПолетом2 is new УпрПолетом1
with
record
типТраектории: Траектория; доОтделения: БортовоеВремя;
выполнениеКоманд: Boolean;
end record;
end Класс_УпрПолетом2;
Видим,
что класс УпрПолетом2 — это «is
а»-разновидность класса УпрПолетом1, который называется родительским классом
или суперклассом.
В
класс УпрПолетом2 добавлены:
q
авспомогательный тип Траектория;
q
три новых свойства (типТраектории, доОтделения,
выполнениеКоманд);
q
три новых метода (УсловияОтделенияЗступени,
ПрогнозОтделенияЗступени, ИсполнениеКоманд).
Кроме
того, в классе УпрПолетом2 переопределен метод суперкласса Установить.
Подразумевается, что в наследство классу УпрПолетом2 достался набор методов и
свойств класса УпрПолетом1. В частности, тип УпрПолетом2 включает поля типа
УпрПолетом1, обеспечивающие прием данных о координатах и скорости
ракеты-носителя, ее угловой ориентации и графике выдаваемых команд, а также о
критерии отделения следующей ступени.
Классы
УпрПолетом1и УпрПолетом2 образуют наследственную иерархическую организацию. В
ней общая часть структуры и поведения сосредоточены в верхнем, наиболее общем
классе (суперклассе). Суперкласс соответствует общей абстракции, а подкласс —
специализированной абстракции, в которой элементы суперкласса дополняются,
изменяются и даже скрываются. Поэтому наследование часто называют отношением обобщение-специализация.
Иерархию
наследования можно продолжить. Например, используя класс УпрПолетом2, можно
объявить еще более специализированный подкласс —
УпрПолетомКосмическогоАппарата.
Другая
разновидность иерархической организации — «part of»-иерархическая
структура — базируется на отношении агрегации. Агрегация не является понятием,
уникальным для объектно-ориентированных систем. Например, любой язык
программирования, разрешающий структуры типа «запись», поддерживает агрегацию.
И все же агрегация особенно полезна в сочетании с наследованием:
1)
агрегация обеспечивает физическую группировку логически
связанной структуры;
2)
наследование позволяет легко и многократно использовать эти
общие группы в других абстракциях.
Приведем
пример класса ИзмерительСУ (измеритель системы управления ЛА):
with Класс_НастройкаДатчиков. Класс_Датчик;
use Класс_НастройкаДатчиков, Класс_Датчик;
Package Класс_ИзмерительСУ is
type ИзмерительСУ is tagged private;
-- описание методов
private
type укз_наДатчик is access all Датчик'Class;
type ИзмерительСУ is record
датчики: array(1..30) of
укз_наДатчик;
процедураНастройки: НастройкаДатчиков;
end record;
end Класс_ИзмерительСУ;
Очевидно,
что объекты типа ИзмерительСУ являются агрегатами, состоящими из массива
датчиков и процедуры настройки. Наша абстракция ИзмерительСУ позволяет
использовать в системе управления различные датчики. Изменение датчика не
меняет индивидуальности измерителя в целом. Ведь датчики вводятся в агрегат с
помощью указателей, а не величин. Таким образом, объект типа ИзмерительСУ и
объекты типа Датчик имеют относительную независимость. Например, время жизни
измерителя и датчиков независимо друг от друга. Напротив, объект типа
НастройкаДатчиков физически включается в объект типа ИзмерительСУ и независимо
существовать не может. Отсюда вывод — разрушая объект типа ИзмерительСУ, мы, в
свою очередь, разрушаем экземпляр НастройкиДатчиков.
Интересно
сравнить элементы иерархий наследования и агрегации с точки зрения уровня
сложности. При наследовании нижний элемент иерархии (подкласс) имеет больший
уровень сложности (большие возможности), при агрегации — наоборот (агрегат
ИзмерительСУ обладает большими возможностями, чем его элементы — датчики и
процедура настройки).
Рассмотрим
более пристально объекты — конкретные сущности, которые существуют во времени и
пространстве.
Объект — это конкретное
представление абстракции. Объект обладает индивидуальностью, состоянием и
поведением. Структура и поведение подобных объектов определены в их общем
классе. Термины «экземпляр класса» и «объект» взаимозаменяемы. На рис. 9.1
приведен пример объекта по имени Стул, имеющего определенный набор свойств и
операций.
Индивидуальность — это
характеристика объекта, которая отличает его от всех других объектов.
Состояние объекта характеризуется
перечнем всех свойств объекта и текущими значениями каждого из этих свойств
(рис. 9.1).
Рис. 9.1. Представление
объекта с именем Стул
Объекты
не существуют изолированно друг от друга. Они подвергаются воздействию или сами
воздействуют на другие объекты.
Поведение характеризует
то, как объект воздействует на другие объекты (или подвергается воздействию) в
терминах изменений его состояния и передачи сообщений. Поведение объекта
является функцией как его состояния, так и выполняемых им операций (Купить,
Продать, Взвесить, Переместить, Покрасить). Говорят, что состояние объекта
представляет суммарный результат его поведения.
Операция
обозначает обслуживание, которое объект предлагает своим клиентам. Возможны
пять видов операций клиента над объектом:
1)
модификатор (изменяет состояние объекта);
2)
селектор (дает доступ к состоянию, но не изменяет его);
3)
итератор (доступ к содержанию объекта по частям, в строго
определенном порядке);
4)
конструктор (создает объект и инициализирует его
состояние);
5)
деструктор (разрушает объект и освобождает занимаемую им
память). Примеры операций приведены в табл. 9.1.
Таблица 9.1. Разновидности
операций
Вид операции |
Пример операции |
Модификатор
Селектор Итератор Конструктор Деструктор |
Пополнеть (кг) КакойВес
( ) : integer ПоказатьАссортиментТоваров
( ) : string СоздатьРобот
(параметры) УничтожитьРобот
( ) |
В
чистых объектно-ориентированных языках программирования операции могут
объявляться только как методы — элементы классов, экземплярами которых являются
объекты. Гибридные языки (C++, Ada 95) позволяют
писать операции как свободные подпрограммы (вне классов). Соответствующие
примеры показаны на рис. 9.2.
Рис. 9.2. Методы и
свободные подпрограммы
В
общем случае все методы и свободные подпрограммы, ассоциированные с конкретным
объектом, образуют его протокол. Таким
образом, протокол определяет оболочку допустимого поведения объекта и поэтому
заключает в себе цельное (статическое и динамическое) представление объекта.
Большой
протокол полезно разделять на логические группировки поведения. Эти
группировки, разделяющие пространство поведения объекта, обозначают роли, которые может играть объект.
Принцип выделения ролей иллюстрирует рис. 9.3.
С
точки зрения внешней среды важное значение имеет такое понятие, как обязанности
объекта. Обязанности означают
обязательства объекта обеспечить определенное поведение. Обязанностями объекта
являются все виды обслуживания, которые он предлагает клиентам. В мире объект
играет определенные роли, выполняя свои обязанности.
Рис. 9.3. Пространство
поведения объекта
В
заключение отметим: наличие у объекта внутреннего состояния означает, что
порядок выполнения им операций очень важен. Иначе говоря, объект может
представляться как независимый автомат. По аналогии с автоматами можно выделять
активные и пассивные объекты (рис. 9.4).
Рис.9.4. Активные и пассивные объекты
Активный
объект имеет собственный канал (поток) управления, пассивный — нет. Активный
объект автономен, он может проявлять свое поведение без воздействия со стороны
других объектов. Пассивный объект, наоборот, может изменять свое состояние
только под воздействием других объектов.
В
поле зрения разработчика ПО находятся не объекты-одиночки, а взаимодействующие
объекты, ведь именно взаимодействие объектов реализует поведение системы. У Г.
Буча есть отличная цитата из Галла: «Самолет — это набор элементов, каждый из
которых по своей природе стремится упасть на землю, но ценой совместных
непрерывных усилий преодолевает эту тенденцию» [22]. Отношения между парой
объектов основываются на взаимной информации о разрешенных операциях и
ожидаемом поведении. Особо интересны два вида отношений между объектами: связи
и агрегация.
Связь
— это физическое или понятийное соединение между объектами. Объект сотрудничает
с другими объектами через соединяющие их связи. Связь обозначает соединение, с
помощью которого:
q
объект-клиент вызывает операции объекта-поставщика;
q
один объект перемещает данные к другому объекту.
Можно
сказать, что связи являются рельсами между станциями-объектами, по которым
ездят «трамвайчики сообщений».
Связи
между объектами показаны на рис. 9.5 с помощью соединительных линий. Связи
представляют возможные пути для передачи сообщений. Сами сообщения показаны
стрелками, отмечающими их направления, и помечены именами вызываемых операций.
Рис. 9.5. Связи между
объектами
Как
участник связи объект может играть одну из трех ролей:
q
актер — объект, который может воздействовать на другие
объекты, но никогда не подвержен воздействию других объектов;
q
сервер — объект, который никогда не воздействует на другие
объекты, он только используется другими объектами;
q
агент — объект, который может как воздействовать на другие
объекты, так и использоваться ими. Агент создается для выполнения работы от
имени актера или другого агента.
На
рис. 9.5 Том — это актер, Мери, Колонки — серверы, Музыкальный центр — агент.
Приведем
пример. Допустим, что нужно обеспечить следующий график разворота первой
ступени ракеты по углу тангажа, представленный на рис. 9.6.
Запишем
абстракцию графика разворота:
with Класс_ДатчикУглаТангажа;
use Класс_ДатчикУглаТангажа;
Package Класс_ГрафикРазворота is
subtype Секунда is Natural range ...
type ГрафикРазворота is tagged private;
procedure Очистить (the: in out ГрафикРазворота);
procedure Связать (the: In out ГрафикРазворота;
teta: Угол: si: Секунда: s2: Секунда);
function УголНаМомент (the: ГрафикРазворота;
s: Секунда) return Угол;
private
…
end Класс_ГрафикРазворота;
Рис. 9.6. График разворота
первой ступени ракеты
Для
решения задачи надо обеспечить сотрудничество трех объектов: экземпляра класса
ГрафикРазворота, РегулятораУгла и КонтроллераУгла.
Описание
класса КонтроллерУгла может иметь следующий вид:
with Класс_ГрафикРазворота.
Класс_РегуляторУгла;
use Класс_ГрафикРазворота. Класс_РегуляторУгла;
Package Класс_КонтроллерУгла is
type укз_наГрафик is access all ГрафикРазворота;
type КонтроллерУгла is tagged private;
procedure Обрабатывать (the: in out КонтроллерУгла;
уг: укз_наГрафик);
function Запланировано (the: КонтроллерУгла;
уг:
укз_наГрафик) return Секунда;
private
type КонтроллерУгла is tagged record
регулятор:
РегуляторУгла := НовРегуляторУгла
(1.1.10);
…
end Класс_КонтроллерУгла:
ПРИМЕЧАНИЕ
Операция Запланировано позволяет клиентам запросить у
экземпляра Контроллера-Угла время обработки следующего графика.
И
наконец, описание класса РегуляторУгла представим в следующей форме:
with Класс_ДатчикУгла. Класс_Порт;
use Класс_ДатчикУгла. Класс_Порт;
Package Класс_РегуляторУгла is
type Режим is (Увеличение. Уменьшение);
subtype Размещение is Natural range ...
type РегуляторУгла is tagged private;
function НовРегуляторУгла (номер:
Размещение;
напр: Направление: порт: Порт)
return РегуляторУгла;
procedure Включить(the: in out РегуляторУгла);
procedure Выключить(the: in out РегуляторУгла);
procedure УвеличитьУгол(№е: in out
РегуляторУгла);
procedure УменьшитьУгол(the: in out
РегуляторУгла);
function ОпросСостояния(the: РегуляторУгла)
return Режим:
private
type укз_наПорт is access all Порт;
type РегуляторУгла is tagged record
Номер:
Размещение;
Состояние:
Режим;
Управление:
укз_наПорт;
end record;
end Класс_РегуляторУгла;
Теперь,
когда сделаны необходимые приготовления, объявим нужные экземпляры классов, то
есть объекты:
РабочийГрафик:
aliased ГрафикРазворота;
РабочийКонтроллер:
aliased Контроллеругла;
Далее
мы должны определить конкретные параметры графика разворота
Связать
(РабочийГрафик. 30. 60. 90);
а
затем предложить объекту-контроллеру выполнить этот график:
Обрабатывать
(РабочийКонтроллер. РабочийГрафикАссеss);
Рассмотрим
отношение между объектом РабочийГрафик и объектом РабочийКонтроллер.
РабочийКонтроллер — это агент, отвечающий за выполнение графика разворота и
поэтому использующий объект РабочийГрафик как сервер. В данном отношении объект
РабочийКонтроллер использует объект РабочийГрафик как аргумент в одной из своих
операций.
Рассмотрим
два объекта, А и В, между которыми имеется связь. Для того чтобы объект А мог
послать сообщение в объект В, надо, чтобы В был виден для А.
В
примере из предыдущего подраздела объект РабочийКонтроллер должен видеть объект
РабочийГрафик (чтобы иметь возможность использовать его как аргумент в операции
Обрабатывать).
Различают
четыре формы видимости между объектами.
1.
Объект-поставщик (сервер) глобален для клиента.
2.
Объект-поставщик (сервер) является параметром операции клиента.
3.
Объект-поставщик (сервер) является частью объекта-клиента.
4.
Объект-поставщик (сервер) является локально объявленным объектом в операции
клиента.
На
этапе анализа вопросы видимости обычно опускают. На этапах проектирования и
реализации вопросы видимости по связям обязательно должны рассматриваться.
Связи
обозначают равноправные (клиент-серверные) отношения между объектами. Агрегация
обозначает отношения объектов в иерархии «целое/часть». Агрегация обеспечивает
возможность перемещения от целого (агрегата) к его частям (свойствам).
В
примере из подраздела «Связи» объект РабочийКонтроллер имеет свойство
регулятор, чьим классом является РегуляторУгла. Поэтому объект
РабочийКонтроллер является агрегатом (целым), а экземпляр РегулятораУгла —
одной из его частей. Из РабочегоКонтроллера всегда можно попасть в его
регулятор. Обратный же переход (из части в целое) обеспечивается не всегда.
Агрегация
может обозначать, а может и не обозначать физическое включение части в целое.
На рис. 9.7 приведен пример физического включения (композиции) частей
(Двигателя, Сидений, Колес) в агрегат Автомобиль. В этом случае говорят, что
части включены в агрегат по величине.
Рис. 9.7. Физическое
включение частей в агрегат
На
рис. 9.8 приведен пример нефизического включения частей (Студента,
Преподавателя) в агрегат Вуз. Очевидно, что Студент и Преподаватель являются
элементами Вуза, но они не входят в него физически. В этом случае говорят, что
части включены в агрегат по ссылке.
Рис. 9.8. Нефизическое
включение частей в агрегат
Итак,
между объектами существуют два вида отношений — связи и агрегация. Какое из них
выбрать?
При
выборе вида отношения должны учитываться следующие факторы:
q
связи обеспечивают низкое сцепление между объектами;
q
агрегация инкапсулирует части как секреты целого.
Понятия
объекта и класса тесно связаны. Тем не менее существует важное различие между
этими понятиями. Класс — это абстракция существенных характеристик объекта.
Класс — описание множества объектов,
которые разделяют одинаковые свойства, операции, отношения и семантику (смысл).
Любой объект — просто экземпляр класса.
Как
показано на рис. 9.9, различают внутреннее представление класса (реализацию) и
внешнее представление класса (интерфейс).
Интерфейс
объявляет возможности (услуги) класса, но скрывает его структуру и поведение.
Иными словами, интерфейс демонстрирует внешнему миру абстракцию класса, его
внешний облик. Интерфейс в основном состоит из объявлений всех операций,
применимых к экземплярам класса. Он может также включать объявления типов,
переменных, констант и исключений, необходимых для полноты данной абстракции.
Рис. 9.9. Структуре представления
класса
Интерфейс
может быть разделен на 3 части:
1)
публичную (public), объявления которой доступны всем клиентам;
2)
защищенную (protected), объявления которой доступны только самому классу, его
подклассам и друзьям;
3)
приватную (private), объявления которой доступны только самому классу и его
друзьям.
Другом
класса называют класс, который имеет доступ ко всем частям этого класса
(публичной, защищенной и приватной). Иными словами, от друга у класса нет
секретов.
ПРИМЕЧАНИЕ
Другом класса может быть и свободная подпрограмма.
Реализация
класса описывает секреты поведения класса. Она включает реализации всех
операций, определенных в интерфейсе класса.
Классы,
подобно объектам, не существуют в изоляции. Напротив, с отдельной проблемной
областью связывают ключевые абстракции, отношения между которыми формируют
структуру из классов системы.
Всего
существует четыре основных вида отношений между классами:
q
ассоциация (фиксирует структурные отношения — связи между
экземплярами классов);
q
зависимость (отображает влияние одного класса на другой
класс);
q
обобщение-специализация («is а»-отношение);
q
целое-часть («part of»-отношение).
Для
покрытия основных отношений большинство объектно-ориентированных языков
программирования поддерживает следующие отношения:
1)
ассоциация;
2)
наследование;
3)
агрегация;
4)
зависимость;
5)
конкретизация;
6)
метакласс;
7)
реализация.
Ассоциации
обеспечивают взаимодействия объектов, принадлежащих разным классам. Они
являются клеем, соединяющим воедино все элементы программной системы. Благодаря
ассоциациям мы получаем работающую систему. Без ассоциаций система превращается
в набор изолированных классов-одиночек.
Наследование
— наиболее популярная разновидность отношения обобщение-специализация. Альтернативой наследованию считается
делегирование. При делегировании объекты делегируют
свое поведение родственным объектам. При этом классы становятся не нужны.
Агрегация
обеспечивает отношения целое-часть, объявляемые
для экземпляров классов.
Зависимость
часто представляется в виде частной формы — использования,
которое фиксирует отношение между клиентом, запрашивающим услугу, и
сервером, предоставляющим эту услугу.
Конкретизация
выражает другую разновидность отношения обобщение-специализация.
Применяется в таких языках, как Ada 95, C++,
Эйфель.
Отношения
метаклассов поддерживаются в языках SmallTalk и CLOS. Метакласс — это класс классов, понятие, позволяющее
обращаться с классами как с объектами.
Реализация
определяет отношение, при котором класс-приемник обеспечивает свою собственную
реализацию интерфейса другого класса-источника. Иными словами, здесь идет речь
о наследовании интерфейса. Семантически реализация — это «скрещивание»
отношений зависимости и обобщения-специализации.
Ассоциация
обозначает семантическое соединение классов.
Пример: в системе
обслуживания читателей имеются две ключевые абстракции — Книга и Библиотека.
Класс Книга играет роль элемента, хранимого в библиотеке. Класс Библиотека
играет роль хранилища для книг.
Рис. 9.10. Ассоциация
Отношение
ассоциации между классами изображено на рис. 9.10. Очевидно, что ассоциация
предполагает двухсторонние отношения:
q
для данного экземпляра Книги выделяется экземпляр
Библиотеки, обеспечивающий ее хранение;
q
для данного экземпляра Библиотеки выделяются все хранимые
Книги.
Здесь
показана ассоциация один-ко-многим. Каждый
экземпляр Книги имеет указатель на экземпляр Библиотеки. Каждый экземпляр
Библиотеки имеет набор указателей на несколько экземпляров Книги.
Ассоциация
обозначает только семантическую связь. Она не указывает направление и точную
реализацию отношения. Ассоциация пригодна для анализа проблемы, когда нам
требуется лишь идентифицировать связи. С помощью создания ассоциаций мы
приводим к пониманию участников семантических связей, их ролей, мощности
(количества элементов).
Ассоциация
один-ко-многим, введенная в примере,
означает, что для каждого экземпляра класса Библиотека есть 0 или более
экземпляров класса Книга, а для каждого экземпляра класса Книга есть один
экземпляр Библиотеки. Эту множественность обозначает мощность ассоциации. Мощность ассоциации бывает одного из трех
типов:
q
один-к-одному;
q
один-ко-многим;
q
многие-ко-многим.
Примеры
ассоциаций с различными типами мощности приведены на рис. 9.11, они имеют
следующий смысл:
q
у европейской жены один муж, а у европейского мужа одна
жена;
q
у восточной жены один муж, а у восточного мужа сколько
угодно жен;
q
у заказа один клиент, а у клиента сколько угодно заказов;
q человек может посещать сколько угодно зданий, а в здании может находиться сколько угодно людей.
Рис. 9.11. Ассоциации с
различными типами мощности
Наследование
— это отношение, при котором один класс разделяет структуру и поведение,
определенные в одном другом (простое наследование) или во многих других
(множественное наследование) классах.
Между
п классами наследование определяет
иерархию «является» («is а»), при которой подкласс наследует от одного или нескольких
более общих суперклассов. Говорят, что подкласс является специализацией его суперкласса (за счет дополнения или
переопределения существующей структуры или поведения).
Пример: дана система для
записи параметров полета в «черный ящик», установленный в самолете. Организуем
систему в виде иерархии классов, построенной на базе наследования. Абстракция
«верхнего» класса иерархии имеет вид
with ...;...
use ...; ...
Package Класс_ПараметрыПолета is
type ПараметрыПолета is tagged private;
function Инициировать return ПараметрыПолета;
procedure Записывать (the: in out ПараметрыПолета);
function ТекущВремя (the: ПараметрыПолета)
return БортовоеВремя;
private
type ПараметрыПолета is tagged record
Имя: integer;
ОтметкаВремени:
БортовоеВремя;
end record;
end Класс_ПараметрыПолета;
Запись
параметров кабины самолета может обеспечиваться следующим классом:
with Класс_ПараметрыПолета; ...
use Класс_ПараметрыПолета; ...
Package Класс_Кабина is
type Кабина is new ПараметрыПолета with private;
function Инициировать (Д:Давление;
К:Кислород;
Т:Температура) return Кабина;
procedure Записывать (the: in out Кабина);
function ПерепадДавления (the: Кабина) return Давление;
private
type Кабина is new ПараметрыПолета
with record
параметр1: Давление;
параметр2: Кислород;
параметр3: Температура
end record;
end Класс_Кабина;
Этот
класс наследует структуру и поведение класса ПараметрыПолета, но наращивает его
структуру (вводит три новых элемента данных), переопределяет его поведение
(процедура Записывать) и дополняет его поведение (функция ПерепадДавления).
Иерархическая
структура классов системы для записи параметров полета, находящихся в отношении
наследования, показана на рис. 9.12.
Рис. 9.12. Иерархия
простого наследования
Здесь
ПараметрыПолета — базовый (корневой) суперкласс, подклассами которого являются
Экипаж, ПараметрыДвижения, Приборы, Кабина. В свою очередь, класс
ПараметрыДвижения является суперклассом для его подклассов Координаты,
Скорость, Ориентация.
Полиморфизм
— возможность с помощью одного имени обозначать операции из различных классов
(но относящихся к общему суперклассу). Вызов обслуживания по полиморфному имени
приводит к исполнению одной из некоторого набора операций.
Рассмотрим
различные реализации процедуры Записывать. Для класса ПараметрыПолета
реализация имеет вид
procedure Записывать (the: in out ПараметрыПолета) is
begin
--
записывать имя параметра
--
записывать отметку времени
end Записывать;
В
классе Кабина предусмотрена другая реализация процедуры:
procedure Записывать (the: in out Кабина) is
begin
Записывать
(ПараметрыПолета (the)); -- вызов метода
-- суперкласса
--
записывать значение давления
--
записывать процентное содержание кислорода
--
записывать значение температуры
end Записывать;
Предположим,
что мы имеем по экземпляру каждого из этих двух классов:
Вполете:
ПараметрыПолета:= Инициировать;
Вкабине:
Кабина:= Инициировать (768. 21. 20);
Предположим
также, что имеется свободная процедура:
procedure СохранятьНовДанные
(d: in out
ПараметрыПолета'class; t: БортовоеВремя) is
begin
if ТекущВремя(d) >= t then
Записывать (d): --
диспетчирование с помощью тега
end if;
end СохранятьНовДанные;
Что
случится при выполнении следующих операторов?
q
СохранятьНовДанные (Вполете, БортовоеВремя (60));
q
СохранятьНовДанные (Вкабине, БортовоеВремя (120));
Каждый
из операторов вызывает операцию Записывать нужного класса. В первом случае диспетчеризация
приведет к операции Записывать из класса ПараметрыПолета. Во втором случае
будет выполняться операция из класса Кабина. Как видим, в свободной процедуре
переменная d может обозначать объекты разных
классов, значит, здесь записан вызов полиморфной операции.
Отношения
агрегации между классами аналогичны отношениям агрегации между объектами.
Повторим
пример с описанием класса КонтроллерУгла:
with Класс_ГрафикРазворота.
Класс_РегуляторУгла;
use Класс_ГрафикРазворота,
Класс_РегуляторУгла;
Package Класс_КонтроллерУгла is
type укз_наГрафик is access all ГрафикРазворота;
type Контроллеругла is tagged private:
procedure Обрабатывать (the: in out Контроллеругла;
yr: укз_наГрафик);
function Запланировано (the: КонтроллерУгла;
уr: укз_наГрафик) return Секунда;
private
type КонтроллерУгла is tagged record
регулятор: РегуляторУгла;
…
end Класс_КонтроллерУгла;
Видим,
что класс КонтроллерУгла является агрегатом, а экземпляр класса РегуляторУгла —
это одна из его частей. Агрегация здесь определена как включение по величине.
Это — пример физического включения, означающий, что объект регулятор не
существует независимо от включающего его экземпляра КонтроллераУгла. Время
жизни этих двух объектов неразрывно связано.
Графическая
иллюстрация отношения агрегации по величине (композиции) представлена на рис.
9.13.
Рис. 9.13. Отношение
агрегации по величине (композиция)
Возможен
косвенный тип агрегации — включением по ссылке. Если мы запишем в приватной
части класса КонтроллерУгла:
…
private
type укз_наРегуляторУгла is access all РегуляторУгла;
type КонтроллерУгла is tagged record
регулятор: укз_наРегуляторУгла;
…
end Класс_КонтроллерУгла;
то
регулятор как часть контроллера будет доступен косвенно.
Теперь
сцепление объектов уменьшено. Экземпляры каждого класса создаются и
уничтожаются независимо.
Еще
два примера агрегации по ссылке и по величине (композиции) приведены на рис.
9.14. Здесь показаны класс-агрегат Дом и класс-агрегат Окно, причем указаны
роли и множественность частей агрегата (соответствующие пометки имеют линии
отношений).
Как
показано на рис. 9.15, возможны и другие формы представления агрегации по
величине — композиции. Композицию можно отобразить графическим вложением
символов частей в символ агрегата (левая часть рис. 9.15). Вложенные части
демонстрируют свою множественность (мощность, кратность) в правом верхнем углу
своего символа. Если метка множественности опущена, по умолчанию считают, что
ее значение «много». Вложенный элемент может иметь роль в агрегате.
Используется синтаксис
роль
: имяКласса.
Рис. 9.14. Агрегация
классов
Рис. 9.15. Формы
представления композиции
Эта
роль соответствует той роли, которую играет часть в неявном (в этой нотации)
отношении композиции между частью и целым (агрегатом).
Отметим,
что, как представлено в правой части рис. 9.15, в сущности, свойства (атрибуты)
класса находятся в отношении композиции между всем классом и его
элементами-свойствами. Тем не менее в общем случае свойства должны иметь
примитивные значения (числа, строки, даты), а не ссылаться на другие классы,
так как в «атрибутной» нотации не видны другие отношения классов-частей. Кроме
того, свойства классов не могут находиться в совместном использовании
несколькими классами.
Зависимость
— это отношение, которое показывает, что изменение в одном классе (независимом)
может влиять на другой класс (зависимый), который использует его. Графически
зависимость изображается как пунктирная стрелка, направленная на класс, от
которого зависят. С помощью зависимости уточняют, какая абстракция является
клиентом, а какая — поставщиком определенной услуги. Пунктирная стрелка
зависимости направлена от клиента к поставщику.
Наиболее
часто зависимости показывают, что один класс использует другой класс как
аргумент в сигнатуре своей операции. В предыдущем примере (на языке Ada 95) класс ГрафикРазворота появляется как аргумент в
методах Обрабатывать и Запланировано класса КонтроллерУгла. Поэтому, как
показано на рис. 9.16, КонтроллерУгла зависит от класса ГрафикРазворота.
Рис. 9.16. Отношение зависимости
Г.
Буч определяет конкретизацию как процесс наполнения шаблона (родового или
параметризованного класса). Целью является получение класса, от которого
возможно создание экземпляров [22].
Родовой
класс служит заготовкой, шаблоном, параметры которого могут наполняться
(настраиваться) другими классами, типами, объектами, операциями. Он может быть
родоначальником большого количества обычных (конкретных) классов. Возможности
настройки родового класса представляются списком формальных родовых параметров.
Эти параметры в процессе настройки должны заменяться фактическими родовыми
параметрами. Процесс настройки родового класса называют конкретизацией.
В
разных языках программирования родовые классы оформляются по-разному.
Воспользуемся возможностями языка Ada 95, в котором
впервые была реализована идея настройки-параметризации. Здесь формальные
родовые параметры записываются между словом generic и заголовком пакета, размещающего класс.
Пример: представим
родовой (параметризированный) класс Очередь:
generic
type Элемент is private;
package Класс_Очередь is
type Очередь is limited tagged private;
…
procedure Добавить (В_0чередь: in out Очередь;
элт: Элемент );
…
private
…
end Класс_0чередь;
У
этого класса один формальный родовой параметр — тип Элемент. Вместо этого
параметра можно подставить почти любой тип данных.
Произведем
настройку, то есть объявим два конкретизированных класса —
Оче-редьЦелыхЭлементов и ОчередьЛилипутов:
package Класс_ОчередьЦелыхЭлементов is new Класс_0чередь
(Элемент => Integer);
package Класс_ОчередьЛилипутов is new Класс_0чередь
(Элемент => Лилипут);
В
первом случае мы настраивали класс на конкретный тип Integer (фактический родовой параметр), во втором случае — на конкретный
тип Лилипут.
Классы
ОчередьЦелыхЭлементов и ОчередьЛилипутов можно использовать как обычные классы.
Они содержат все средства родового класса, но только эти средства настроены на
использование конкретного типа, заданного при конкретизации.
Графическая
иллюстрация отношений конкретизации приведена на рис. 9.17. Отметим, что
отношение конкретизации отображается с помощью подписанной стрелки отношения
зависимости. Это логично, поскольку конкретизированный класс зависит от
родового класса (класса-шаблона).
Рис. 9.17. Отношения
конкретизации родового класса