Изучение объектных структур в данной лекции может служить весьма хорошим примером того, насколько неправильно отделять вопросы реализации от проблем будто бы "высокого" уровня. В процессе рассмотрения новых технических приемов, связанных с вопросами реализации, приходит более глубокое понимание абстрактных понятий. Типичным примером может служить введение ссылочных и развернутых значений, представляющих, на первый взгляд, неприметное техническое решение. В действительности, это ответ на общий вопрос об отношении части и целого, постоянно обсуждаемый в дискуссиях по ОО-анализу.
В некоторой части компьютерной литературы принижается значение реализации и считается, что самое важное - это анализ. Но разработка ПО - это разработка моделей. Хорошая техника реализации часто является одновременно и хорошим средством моделирования. Помимо программных систем ее можно использовать и во многих других областях.
Данная лекция в большей степени посвящена моделированию, нежели реализации в строгом смысле этого термина. В ней показано, как можно использовать объектные структуры для построения реалистичных и полезных операционных описаний различного вида систем.
В процессе выполнения ОО-система создает некоторое число объектов. Организация этих объектов и отношения между ними определяют конструкцию времени выполнения. Рассмотрим свойства объектов.
Прежде всего, необходимо напомнить смысл термина "объект". Полная ясность была внесена в предыдущей лекции в виде строгого определения (Определение и объективное правило, см. лекцию 7):
Определение: объект
Объект - это экземпляр некоторого класса
Во время выполнения программная система, содержащая класс C, может в разных точках, используя процедуры создания или клонирования, создавать экземпляры C, - структуры данных, соответствующие образцу, заданному классом C. Например, экземпляр класса POINT представляет собой структуру данных, состоящую из двух полей, соответствующих атрибутам x и y класса. Экземпляры всех возможных классов составляют множество объектов системы.
Это официальное определение в мире ОО-ПО. Но в повседневном языке термин "объект" имеет гораздо более широкий смысл. Любая программная система связана с определенной внешней системой, которая может содержать "объекты": точки, линии, поверхности и тела в EMPLOYEE в системе расчета зарплаты, экземпляры которого являются
Хорошим следствием дуализма слова "объект" является естественность и мощь ОО-метода, применяемого для direct mapping ), который, как отмечалось, является принципиальным требованием модульного проектирования. Неудивительно, что некоторые классы являются моделями внешних типов объектов проблемной области, а экземпляры классов - моделями реальных объектов. (См. "Прямое отображение", лекция 3)
Но не стоит переоценивать "реальность" слова "объект". В науке и технике существует большой риск в заимствовании слов естественного языка и придания им специального смысла. Термин "объект" настолько перегружен повседневным смыслом, что техническое его использование может стать источником недоразумений. В частности:
STATE, описывающий состояние системы, или класс COMMAND. (См. лекцию 20 и лекцию 21 курса "Основы объектно-ориентированного проектирования")Когда слово "объект" используется в этой книге, то из контекста ясно, в общем или техническом смысле используется этот термин. В тех случаях, когда эту разницу необходимо подчеркнуть, используется уточнение - программный объект или внешний объект.
Программный объект довольно простое существо, если известен класс, которому он принадлежит.
Пусть O - объект. По определению он является экземпляром некоторого класса. Точнее, он является прямым экземпляром (direct instance) только одного класса, например C.
С учетом наследования O будет тогда косвенным экземпляром других классов, - предков C. Это тема дальнейшего обсуждения; в данной дискуссии достаточно понятия прямого экземпляра. Везде, где не может возникнуть недоразумений, слово "прямой" будет опущено. |
Класс C называется порождающим классом ( generating class ) или просто генератором ( generator ) объекта O. Заметьте, C - программный текст, а O - структура данных времени выполнения, появляющаяся в результате работы рассмотренных ниже механизмов создания объектов.
Часть компонентов C является атрибутами. Эти атрибуты полностью определяют форму объекта, представляющего собой просто набор полей, по одному на каждый атрибут.
Рассмотрим класс POINT из предшествующей лекции (Текст класса POINT см. в лекции 7). Исходный текст имеет вид:
class POINT feature x, y: REAL ... Объявления подпрограмм ... end
Подпрограммы опущены, так как форма объектов полностью определяется атрибутами соответствующих классов. Данный класс имеет два атрибута x и y типа REAL, следовательно, его экземпляр - это объект с двумя полями, содержащими значения этого типа:
(рис 8.1) Экземпляр класса POINTВ данной книге объекты - набор полей - изображаются в виде смежных прямоугольников, содержащих соответствующие значения. Внизу курсивом в скобках дается имя класса (в данном случае - На диаграммах, представляющих структуру ОО-системы или части такой системы, классы изображаются в виде эллипсов. Эти соглашения позволяют не путать классы и объекты. |
Оба атрибута класса POINT относятся к типу REAL. Следовательно, соответствующие поля прямого экземпляра POINT содержат действительные числа.
Это пример полей, соответствующих атрибутам одного из "базовых" типов. Формально эти типы определены как классы, а их экземпляры принимают значения из предопределенных диапазонов. К базовым (предопределенным, встроенным)типам относятся:
BOOLEAN, может иметь только два различных экземпляра, соответствующих булевым значениям true и false ;CHARACTER, экземпляры которого представляют символы;INTEGER, экземпляры которого представляют целые числа;REAL и DOUBLE, экземпляры которых представляют действительные числа одинарной и двойной точности.Тип STRING, представляющий конечную последовательность символов, на данном этапе рассматривается как базовый. Далее будет показано, что в действительности он относится к другой категории. ("Строки", см. лекцию 13)
Для каждого базового типа необходимо определить правила записи их значений в исходных текстах. Соглашения просты:
BOOLEAN два различных экземпляра обозначаются как True и False.CHARACTER будет записываться как символ в апострофах: 'A'.STRING обозначается как последовательность символов в двойных апострофах: "Это строка".INTEGER используем обычную десятичную нотацию: 34, -675, +4.REAL или DOUBLE будет применяться как обычная нотация: 3.5 или -0.05, так и экспоненциальное представление: -5.e-2.Рассмотрим класс с атрибутами базовых типов:
class BOOK1 feature title: STRING date, page_count: INTEGER end
Типичный экземпляр класса выглядит так:
(рис 8.2) Объект, представляющий книгуПоскольку в настоящий момент нас в первую очередь интересует структура объектов, то в последующих примерах все компоненты классов будут атрибутами, а подпрограммы отсутствуют.
Это означает, что на данном этапе обсуждения объекты подобны записям или структурам в языках Pascal и C. Принципиальное отличие от этих языков выражается в том, что, благодаря наличию механизмов скрытия информации, клиенты классов не могут непосредственно присваивать значения полям таких объектов. В Pascal и в C с незначительными синтаксическими различиями допустимо объявление записи с последующим присваиванием (Внимание: Недопустимая нотация! Только для обсуждения.):
b1: BOOK1 ... b1.page_count := 355
Здесь во время выполнения полю page_count объекта, присоединенного к b1, присваивается значение 355. Для классов такая возможность не допускается. Предоставлять клиентам классов разрешение менять поля объектов было бы насмешкой над правилом скрытия информации. В этом случае терял бы смысл выборочный экспорт, управляемый автором класса. В ОО-подходе модификация значений полей допустима только с помощью процедур класса, добавляемых в том случае, если автор класса решит предоставить такую возможность своим клиентам. Далее такая процедура будет добавлена в класс BOOK1.
Разрешение присваиваний вида Сами разработчики Java отмечают, что программист может испортить объект при наличии общедоступного поля, так как значение такого поля можно изменить путем непосредственного присваивания. Многие авторы языков вводят такую возможность, а затем предупреждают: "не делайте этого". Логичнее определить метод и нотацию, поддерживающие такие ограничения. |
В ОО-ПО классы без подпрограмм редко имеют практическое значение. Исключением являются ситуации, когда в родительских классах определяется набор атрибутов, а потомки содержат необходимые подпрограммы. Другим примером могут служить классы, представляющие внешние объекты, которые принципиально невозможно модифицировать, например данные от внешних датчиков в системе реального времени. Но на данном этапе такой подход полезен для понимания основных концепций, подпрограммы будут добавлены позже.
Используя указанные выше типы, определим класс WRITER для описания автора книги:
class WRITER feature name, real_name: STRING birth_year, death_year: INTEGER end
(рис 8.3) Объект "писатель"
Чаще всего нам необходимы объекты с полями, представляющими другие объекты. Например, книга имеет автора, который представлен экземпляром класса WRITER.
Можно ввести понятие подобъекта. В новой версии класса BOOK2 его экземпляры содержат поле, являющееся объектом - экземпляром класса WRITER.
(рис 8.4) Два объекта "книга" с подобъектами "писатель"Такое понятие подобъекта, несомненно, полезно, и далее в этой лекции будет показано, как создавать соответствующие классы.
Но это не совсем то, что необходимо. В каждом экземпляре BOOK2 приходится дублировать информацию об одном и том же авторе в виде подобъекта. Причины неприемлемости такого решения:
WRITER повлекло за собой автоматическое обновление этой информации для всех объектов - книг данного автора.Лучшим является решение, представленное на рис.8.5 . Оно основано на новой версии класса, BOOK3.
Каждый экземпляр BOOK3 в поле author содержит ссылку ( reference ) на объект типа WRITER. Нетрудно дать точное определение.
Определение: ссылка
Ссылка - это значение времени выполнения. Она может быть пустой (void) или присоединенной (attached).
Присоединенная ссылка однозначно идентифицирует объект (присоединена к конкретному объекту).
(рис 8.5) Два объекта "книга" со ссылками на один и тот же объект "писатель"
(рис 8.6) Объект, содержащий пустую ссылку (Роман "Кандид" (Candide) опубликован анонимно)Определение ссылки не подразумевает конкретной аппаратно-программной реализации. Если ссылка не пуста, то она идентифицирует объект и может рассматриваться как абстрактное имя объекта.
Концепция ссылки должна, безусловно, иметь аналог при реализации. Программирование на уровне машинного кода использует адресацию, многие языки программирования содержат понятие указателя. Понятие ссылки является более абстрактным. Хотя ссылка, в конечном итоге, может быть представлена адресом, не следует из этого исходить. Ссылка может содержать адрес наряду с другой информацией.
Отличие ссылок от указателей выражается в том, что они типизированы. Они напоминают
Понятие ссылки приводит к концепции идентичности объектов. Каждый объект, созданный в процессе выполнения ОО-системы, уникален и идентифицируется независимо от значений его полей. Возможны две ситуации:
Эти наблюдения свидетельствуют о неоднозначности высказывания " a обозначает тот же объект, что и b ". Можно подразумевать различные объекты с одинаковыми данными (I1) или состояния одного и того же объекта до и после изменения значений полей (I2). Мы будем использовать второе толкование и считать, что значения полей заданного объекта могут изменяться в процессе выполнения, а он остается "тем же самым объектом". В случае (I1) будем говорить о равных (но различных) объектах, точное определение понятия равенства будет дано позже.
В соответствии с определением I2 можно сказать, что поля объекта могут изменяться и это не будет ошибкой. Термин "поле" обозначает одно из значений, составляющих объект, а не соответствующий идентификатор поля - имя одного из атрибутов порождающего класса. Каждому атрибуту класса соответствует поле объекта (1832 для атрибута |
Изучение того, как сделать объекты сохраняемыми (persistent), заставит нас продолжить изучение свойств идентичности объектов. (См. "Идентичность объектов", лекция 13 курса "Основы объектно-ориентированного проектирования")
Класс BOOK1 содержал атрибуты только базовых типов, его вариант BOOK3, содержит атрибут, представляющий ссылку на автора.
class BOOK3 feature title: STRING date, page_count: INTEGER author: WRITER -- Новый атрибут. end
Объявленный тип дополнительного атрибута author это просто имя соответствующего класса: WRITER. Это будет общим правилом: если имеется стандартное объявление класса
class C feature ... end
то объявление некоторой сущности типа C
x: C
обозначает значения, являющиеся ссылками на потенциальные объекты типа C. Такое соглашение, использующее ссылки, обеспечивает большую гибкость и приемлемо в большинстве случаев. Подробное обсуждение этого правила и других возможных решений содержится в последнем разделе данной лекции.
Ничто не препятствует объекту O1 в определенный момент выполнения системы содержать ссылку, присоединенную к самому O1. Такая ссылка на себя может быть косвенной. В ситуации на рис.8.7 объект, имеющий значением поля name: "Almaviva", сам является своим лендлордом (прямая циклическая ссылка). Фигаро любит Сюзанну, которая любит Фигаро (косвенная циклическая ссылка).
(рис 8.7) Прямые и косвенные ссылки на себяТакие циклы в динамических структурах возможны, только если
class PERSON1 feature name: STRING loved_one, landlord: PERSON1 end
содержит прямой цикл ( PERSON1 - клиент PERSON1 ).
Обратное утверждение неверно - присутствие цикла в объявлении класса не означает, что циклы обязательно появятся в структурах времени выполнения. Можно объявить класс
class PERSON2 feature mother, father: PERSON2 end
Класс является собственным клиентом. Однако если он моделирует соответствующие именам атрибутов отношения между людьми, то структуры времени выполнения никогда не будут содержать циклов, поскольку ни один человек не может быть собственным родителем или предком.
На основе предшествующего рассмотрения выясняется в первом приближении структура ОО-системы в процессе выполнения.
(рис 8.8) Возможная структура объектов во время выполненияСистема состоит из нескольких объектов с различными полями. Некоторые поля содержат значения базовых типов, а другие являются пустыми или присоединенными ссылками на другие объекты. Каждый объект является экземпляром некоторого типа, основанного на классе (на рисунке тип указывается под объектом). Некоторые типы представлены единственным экземпляром, но гораздо чаще присутствует несколько экземпляров одного типа. На рис.8.8 тип TYPE1 представлен двумя экземплярами, остальные - единственным. Некоторые объекты содержат поля только ссылочного типа (экземпляр TYPE4 ) или только базовых типов (экземпляр TYPE5 ). Могут присутствовать прямые или косвенные циклические ссылки (верхнее поле экземпляра TYPE2, по часовой стрелке от нижнего экземпляра TYPE1 ).
Подобная структура может показаться слишком запутанной. Впечатление от приведенной иллюстрации, демонстрирующей различные возможности, можно выразить выражением: "блюдо спагетти".
Это впечатление не совсем правильно. Впечатление простоты должен создавать программный текст, но не структура объектов периода выполнения. Текст отражает определенные отношения (такие как "любит", "имеет хозяина"). Конкретную структуру объектов периода выполнения можно назвать экземпляром таких отношений, она фиксирует связи между элементами данного набора объектов. Моделируемые отношения могут быть простыми, в то время как отношения индивидуумов конкретного множества объектов - достаточно сложными. Понятие "любит" очень просто, однако любовные отношения конкретных людей могут быть безнадежно запутаны.
Во время выполнения могут неизбежно возникать структуры, содержащие много объектов и имеющие запутанную структуру ссылок. Хорошая среда разработки должна предоставлять средства анализа объектных структур для тестирования и отладки.
Сложность динамических структур не должна влиять на статическую картину. Необходимо стараться сохранить набор классов и их отношения настолько простыми, насколько это возможно.
Тот факт, что простым моделям могут соответствовать сложные структуры данных, частично отражает мощь наших компьютеров. Короткий исходный текст может описывать огромные вычисления. Простая ОО-система может порождать в процессе выполнения миллионы объектов, связанных большим числом ссылок. Важнейшей целью программной инженерии является сохранение простоты ПО, даже когда экземпляры объектов такой простотой не обладают.
Рассмотренные приемы позволяют продвинуться в понимании возможностей ОО-подхода как средства моделирования. Важно, в частности, прояснить два аспекта: рассмотреть различные миры, связанные с разработкой ПО и отношения между ПО и внешней реальностью.
Из предшествующей дискуссии следует, что когда мы говорим об ОО-разработке, следует различать четыре отдельных мира:
Соотношения между этими мирами представлены на рис.8.9.
(рис 8.9) Формы и их экземплярыИ на программном и на внешнем уровне (нижняя и верхняя части рисунка) важно разграничить общие понятия и их конкретные реализации (классы и абстрактные отношения слева, объекты и отношения экземпляров справа). Данный момент уже обсуждался в дискуссии о сравнительной роли классов и объектов в предыдущей лекции. Применительно к отношениям необходимо отличать абстрактные отношения loved_one от множества связей loved_one, существующих между элементами конкретного множества объектов.
Это различие невыразимо ни в стандартных математических определениях понятия "отношение", ни в программистской терминологии, например в <x, y>, где x и y являются элементами заданных множеств TX и TY. В терминах программирования все x относятся к типу TX, а все y - к типу TY. Будучи пригодными для математиков, эти определения не подходят для
| Это обсуждение будет продолжено в лекции 11, когда будут рассматриваться преобразования между абстрактными и конкретными объектами, и будет дано имя вертикальным стрелкам предыдущего рисунка - функция абстракции. (См. "Функции абстракции", лекция 11) |
Предшествующее обсуждение не содержит ссылок на "реальный мир", - вместо этого используется термин "моделируемая система".
Такое разграничение проводится не всегда. Во многих дискуссиях используется выражение "моделирование реального мира"; аналогичные высказывания содержат и книги по ОО-анализу. Однако говорить о "реальности" применительно к программной системе ошибочно, по крайней мере, по четырем причинам.
Во-первых, реальность отражается в глазах очевидца. Не впадая в профессиональный шовинизм, программист всегда вправе спросить своих заказчиков, почему их системы более реальны, чем его. Возьмите программу, выполняющую математические вычисления - проверку гипотезы четырех красок в теории графов,
Во-вторых, понятие реального мира рушится в нередких ситуациях, когда ПО предназначено для разрешения проблем ПО. Рассмотрим компилятор C, написанный на Pascal. Для него "реальными" объектами являются программы на C. Насколько эти программы более реальны, чем сам компилятор? Это наблюдение применимо и к другим системам, работающим с объектами, существующими только в компьютере. (См. лекцию 6)
Третье соображение обобщает второе. В сегодняшнем информационном мире компьютеры стали частью реальности. На заре появления компьютеров можно было говорить, что создаваемая программная система моделирует реальную систему. Предприятие приобретало компьютеры для автоматизации бизнес процессов. При описании процессов современного банка его ПО является фундаментальной частью
Последний довод наиболее фундаментален. Программная система не является моделью реальности. В лучшем случае это модель модели некоторой части некоторой реальности. Система мониторинга пациента больницы не является моделью больницы, но реализацией конкретной точки зрения на некоторые аспекты работы больницы. Это модель модели некоторой части реальности больницы. Астрономическая программа это не модель вселенной, а всего лишь
Абстрактные типы данных, лежащие в основе ОО-метода, помогают понять, почему не следует придерживаться широко распространенной, но иллюзорной точкой зрения, что мы имеем дело с "реальным миром". Первый шаг к объектной ориентации, выражаемый теорией АТД, состоит в отказе от реальности ради менее грандиозного, но более аппетитного яства, - представляющего множество абстракций, характеризующих операции, доступные клиентам, и их формальные свойства. (На этом построен девиз модельера АТД - не говорите мне, кто вы, скажите, чем вы обладаете.) Мы никогда не претендуем на то, что рассмотрели все возможные операции и свойства: мы выбрали некоторые из них, подходящие для наших целей и отбросили остальные. Моделирование означает отсекание лишнего.
В идеальном случае программная система приходится соответствующей реальности лишь "седьмой водицей на киселе" (cousin twice removed).
Вернемся к более приземленным проблемам и рассмотрим, как программные системы работают с объектами, как создают и используют гибкие структуры данных.
Что не было показано при описании структуры объектов периода выполнения, так это в высшей степени динамичная природа настоящей ОО-модели. Статическая и ориентированная на стеки политика управления объектами характерна для языков уровня Fortran и Pascal соответственно. Противоположной является политика в настоящем ОО-окружении, позволяющая создавать объекты в период выполнения, когда в них возникает потребность. Какому образцу (типу) соответствуют создаваемые объекты, как правило, невозможно предсказать при статической проверке программного текста.
В начальном состоянии, как описано в предыдущей лекции, создается единственный корневой объект. Затем система повторно выполняет операции создания новых объектов, связывает изначально пустые ссылки с этими объектами, делает ранее присоединенные ссылки пустыми или присоединяет их к другим объектам. Динамическая и непредсказуемая природа этих операций обеспечивает гибкий подход и позволяет поддерживать
Следующий раздел посвящен механизмам, необходимым для создания объектов и манипулирования их полями, в частности, ссылками.
Рассмотрим создание экземпляра класса BOOK3. Это возможно только с помощью подпрограммы класса, являющегося клиентом BOOK3, как, например:
class QUOTATION feature
source: BOOK3
page: INTEGER
make_book is
-- Создание объекта BOOK3 и присоединение его к source.
do
... См. ниже ...
end
end
Этот класс описывает цитирование книги в других публикациях. Он содержит два поля: ссылку на цитируемую книгу и число страниц, содержащих ссылки на нее.
Механизм создания экземпляра QUOTATION (скоро он будет рассмотрен) предусматривает инициализацию всех его полей. Правило инициализации по умолчанию определяет, что любое ссылочное поле (в данном примере - поле, соответствующее атрибуту source ) после инициализации должно содержать пустую ссылку. Другими словами, создание объекта типа QUOTATION не сопровождается созданием объекта типа BOOK3.
Ссылка остается пустой, пока над ней не будут выполнены некоторые действия, - таково общее правило. Изменить значение ссылки можно, создав, например, новый объект. В процедуре make_book это делается следующим образом:
make_book is
-- Создание объекта BOOK3 и присоединение его к source.
do
create source
end
Это иллюстрация простейшей формы инструкции создания: create x , где x - атрибут охватывающего (
Сущность x, именованная в инструкции (в данном примере source ), называется целью (target) инструкции создания.
Данная форма известна как "базовая инструкция создания". Другая форма, включающая вызов процедуры класса, скоро появится. Вот точное определение действия базовой инструкции создания:
Результат базовой инструкции создания
Эффект инструкции создания вида create x, где тип цели x является ссылочным типом, основанном на классе C, состоит в выполнении трех следующих действий:
C (набора полей, по одному на каждый атрибут C ). Пусть x (ссылки) к На этапе C1 создается экземпляр C. На этапе C2 устанавливаются предопределенные значения всех полей, зависящие от типа соответствующего атрибута:
Значения по умолчанию при инициализации
Для ссылок значение по умолчанию - пустая ссылка.
Для полей BOOLEAN значение по умолчанию - False.
Для полей CHARACTER значение по умолчанию - символ null.
Для чисел (типов INTEGER, REAL или DOUBLE ) значение по умолчанию - ноль в соответствующем данному типу представлении.
Итак, для цели source типа BOOK3 в соответствии с объявлением класса
class BOOK3 feature title: STRING date, page_count: INTEGER author: WRITER end
результатом инструкции создания create source, выполняемой при вызове процедуры make_book класса QUOTATION, будет объект изображенный на рис.8.10.
(рис 8.10) Созданный и инициализированный объектПосле инициализации значения целочисленных полей равны нулю. Ссылочное поле author и поле title типа STRING, содержат пустые ссылки. Тип STRING, о котором ничего не говорится в правилах инициализации, двойственен, - фактически являясь ссылочным типом, он рассматривается во многих ситуациях как базовый тип. (О строках см. лекцию 13)
Важно проследить за последовательностью происходящих событий. Для рассмотренного выше экземпляра BOOK3 происходит следующее:
QUOTATION. Пусть Q_OBJ - этот экземпляр и имеется сущность a, значение которой ссылка, присоединенная к Q_OBJ.a.make_book приводит к выполнению процедуры make_book с Q_OBJ в качестве цели.Правомерен вопрос - как будет создан сам Q_OBJ (шаг B1)? Это, оставляя проблему, отодвигает ее вглубь. Но к этому моменту мы уже знаем ответ на этот вопрос: все возвращается к первопричине - Большому Взрыву. Для выполнения системы необходимо снабдить ее корневым классом и процедурой этого класса, названной процедурой создания. В начале выполнения автоматически создается один объект - корневой объект - экземпляр корневого класса. Корневой объект является единственным объектом, не создаваемым инструкциями программного текста; он приходит извне, как objectus ex machine ( объект от машины ). Начав с одного, провидением посланного объекта, далее уже программа может создавать объекты нормальным путем через подпрограммы, выполняющие инструкции создания. Первой выполняемой подпрограммой является процедура создания, автоматически применяемая к корневому объекту. Не всегда, но чаще всего она содержит по крайней мере одну инструкцию создания, что в предыдущей лекции называлось началом грандиозного фейерверка, процесса, создающего столько новых объектов, сколько нужно текущему выполнению.
Объекты создаются явным образом. Объявление сущности
b: BOOK3
не влечет за собой создание объекта во время выполнения, это происходит, когда некий элемент системы выполнит операцию
create b
Это может показаться удивительным. Разве объявления b недостаточно для создания объекта? Что хорошего в объявлении, если объект не создается?
Достаточно минуты размышления для понимания того, что разделение объявления и создания объекта является единственно разумным решением.
Первый аргумент - reductio ad absurdum (доведение до абсурда). Предположим, что начата обработка объявления и немедленно создается соответствующий объект. Но это экземпляр класса BOOK3, имеющий атрибут author ссылочного типа WRITER, значит поле author - ссылка, для которой опять нужно создавать объект. Этот объект вновь содержит ссылочные поля, требуется опять делать то же самое и начинается длинный путь рекурсивного создания объектов.
Этот аргумент еще более убедителен для таких классов как PERSON1, содержащих ссылки на себя:
class PERSON1 feature name: STRING loved_one, landlord: PERSON1 end
Появление каждого экземпляра PERSON1 повлечет за собой создание двух других таких объектов (соответствующих loved_one и landlord ) и начнется бесконечный цикл. Такие прямые или косвенные циклические ссылки не экзотика - они часто встречаются и необходимы.
Другой аргумент следует из обсуждения роли объектной технологии как мощного метода моделирования. Если для каждого ссылочного поля будет создаваться новый объект, то не было бы возможности выделить пустые ссылки и множественные ссылки на один и тот же объект. И то, и другое необходимо для реалистичного моделирования систем:
author для обозначения неизвестного автора.loved_one двух персон PERSON1, присоединенные к одному и тому же объекту. Не имело бы смысла создание своего объекта для каждого из этих полей. Все, что требуется, - это операция присваивания (рассмотрена далее в этой лекции) для присоединения ссылки к уже существующему объекту. В еще большей степени это соображение применимо для ссылки на себя (поле landlord верхнего объекта в том же примере).Механизм управления объектами никогда не присоединяет ссылку неявно. Он создает объекты через инструкции создания (или операции клонирования, тоже явные), инициализируя их ссылочные поля пустыми ссылками. Эти поля, в свою очередь, могут стать присоединенными к объектам, только в результате явных операций над этими полями.
В дискуссии о наследовании будет показано, что инструкция создания может использовать синтаксис create {T}x для создания объекта, чей тип T является наследником типа объявленного для x. (Полиморфное создание, см.лекцию 14) |
Все до сих пор рассмотренные инструкции создания основывались на инициализации по умолчанию. В некоторых случаях инициализация, определенная в языке, может нас не устраивать - хотелось бы обеспечить создаваемый объект специфической информацией. В этом предназначение процедур создания.
Для использования инициализации, отличной от предопределенной умолчанием, необходимо класс снабдить одной или несколькими процедурами создания. Такие процедуры должны быть перечислены в предложении, начинающимся ключевым словом creation в начале класса перед первым предложением feature. Схема такова:
indexing ... class C creation p1, p2, ... feature ... Объявления компонент, включая реализацию процедур p1, p2, ... end
Совет, отражающий стиль: в случае класса с единственной процедурой создания - для нее рекомендуется имя make. Для классов с двумя и более процедурами создания желателен префикс make_, за которым следует квалификатор, как в следующем примере POINT. (См. "Правильный выбор имен", лекция 8 курса "Основы объектно-ориентированного проектирования") |
Соответствующая инструкция создания в этих случаях имеет другую форму:
create x.p (...)
где p одна из процедур создания перечисленных в разделе creation, и в круглых скобках (...) перечисляются фактические аргументы p. Результатом является создание объекта с использованием значений по умолчанию, как и ранее, а затем вызов p с заданными аргументами. Такая инструкция является комбинацией инструкции создания и вызова процедуры и называется порождающим вызовом (creation call). (Оригинальная версия класса POINT приведена в лекции 7)
В качестве примера добавим две процедуры создания в класс POINT, что позволит клиентам при создании новой точки указывать ее начальные координаты - декартовы или полярные. Введем процедуры создания: make_cartesian и make_polar. Вот схема:
class POINT1 creation
make_cartesian, make_polar
feature
... Компоненты из предыдущей версии класса:
x, y, ro, theta, translate, scale, ...
feature {NONE} - Этот вариант экспорта рассмотрен ниже.
make_cartesian (a, b: REAL) is
-- Инициализация точки с декартовыми координатами a и b.
do
x := a; y := b
end
make_ polar (r, t: REAL) is
-- Инициализация точки с полярными координатами r и t.
do
x := r * cos (t); y := r * sin (t)
end
end
Для такого класса клиент будет создавать точки инструкциями вида:
create my_point.make_cartesian (0, 1) create my_point.make_polar (1, Pi/2)
В обоих случаях создается точка с одинаковыми координатами в предположении, что константа Pi имеет общепринятый смысл. Вот правило, определяющее эффект порождающего вызова. Первые три пункта правила такие же, как и для базисной формы, приведенной ранее:
Эффект порождающего вызова
Рассмотрим порождающий вызов в форме create x.p(...).
Пусть тип цели x это ссылочный тип, основанный на классе C, p(...) - процедура создания класса C, с заданным списком фактических аргументов. Эффект вызова состоит в выполнении следующих четырех шагов:
C (набора полей, по одному на каждый атрибут C ). Пусть x (ссылки) к p c заданными аргументами и с целевым объектом Для двух процедур создания, объявленных в классе POINT1, предложение feature имело вид feature {NONE}. Это означает, что эти процедуры закрыты для обычных вызовов, но остаются открытыми для порождающих вызовов. Только что представленные два примера порождающих вызовов являются корректными, но нормальные вызовы, например my_point.make_cartesian (0, 1) или my_point.make_polar (1, Pi/2) некорректны, так как процедуры недоступны клиентам со статусом обычных компонентов.
Решение о закрытости процедур означает, что мы не хотим после создания точки дать возможность клиентам прямого доступа к изменению их координат, хотя они могут делать это через другие процедуры класса, например такие, как translate и scale. Конечно, это лишь одна из возможных политик, вполне разумно экспортировать процедуры создания клиентам, придавая им дополнительно статус обычных процедур.
Для процедур создания можно установить выборочный статус порождающего вызова. Для этого достаточно в предложении creation перечислить классы, которым разрешается создавать объекты:
class C creation {A, B, ...}
p1, p2,
...
Этот прием применяется значительно реже, чем задание статуса экспорта этих процедур как обычных компонентов класса в предложении feature. Важно помнить, что статус экспорта порождающего вызова и статус экспорта обычного вызова не зависят друг от друга, они устанавливаются независимо в разных предложениях.
Две формы инструкций создания: create x и create x.p (...), являются взаимно исключающими. Если в классе задано предложение creation, то допускается только порождающие вызовы, базовая форма создания считается в этом случае недопустимой и отвергается компилятором.
Это соглашение кажется на первый взгляд странным, но смысл его становится понятным при рассмотрении требований согласованности объекта. Объект - это не просто набор полей, это реализация АТД, так что поля его будут согласованы только если они удовлетворяют ограничениям, заданным спецификацией АТД. Вот типичный пример. Предположим, что объект задает некоторую личность с двумя полями - год рождения и возраст. Понятно, что согласованность этого объекта не допускает независимых значений этих полей, они связаны вполне определенным соотношением, которое может быть частью спецификации. Инструкция создания обязана всегда производить на свет согласованный объект. Базовая форма этой инструкции применима только в тех частных и довольно редких случаях, когда стратегия умолчания удовлетворяет требованиям согласованности. Во всех остальных случаях в классе требуется определять процедуры создания, что автоматически запрещает использование базовой инструкции создания.
В тех редких случаях, когда инициализация по умолчанию допустима, поскольку удовлетворяет инварианту класса, может появиться желание включить ее в состав процедур создания. Для этого необходимо в список процедур создания включить специальную процедуру, наследуемую от класса ANY, с именем nothing. Как следует из ее имени, эта процедура без аргументов ничего не делает, имея пустое тело. Вот пример подобного включения:
class C creation nothing, some_creation_procedure, some_other_creation_procedure... feature ...
Хотя по-прежнему базовая инструкция создания является некорректной в этом случае, но теперь клиент имеет возможность создать объект порождающим вызовом create x.nothing
В заключение обратите внимание на специальное правило - теперь появилась возможность определить класс, клиенты которого не смогут создавать экземпляры класса. Вот пример того, как этого можно добиться:
class C creation -- Здесь ничего не указано! feature ... Текст класса, как обычно ... end
Класс имеет предложение creation, но пустое. Это означает согласно установленным правилам, что создавать объекты можно только с помощью процедур создания, которых нет, что означает невозможность создания объектов.
Если ограничиться ОО-механизмом, рассмотренным до сих пор, то такая возможность запрета на создание объектов класса кажется надуманной. Знакомство с наследованием придает ей смысл, - иногда желательно использовать класс только в интересах наследования. Эта цель и может быть достигнута таким способом. Заметьте, этого же можно добиться, сделав класс абстрактным (отложенным). Но в этом случае у класса должен быть, по крайней мере, один отложенный метод. Иногда разумно полностью определить методы, но не включить в класс процедуры создания.
В продолжение обсуждения полезно сравнить применяемый подход с несколькими процедурами создания с подходом, используемым в языках C++/Java. В этих языках применяется техника, основанная на перегрузке. Суть ее такова: все процедуры создания, называемые конструкторами, перегружены - они имеют одно и то же имя, совпадающее с именем класса. Конструкторы должны иметь различную сигнатуру (отличаться числом и/или типами аргументов).
Как мы видели при обсуждении перегрузки, сигнатура не является подходящим критерием распознавания. Например: конструкторы make_cartesian и make_polar имеют одинаковую сигнатуру, так что придется вводить искусственный аргумент в один из конструкторов, чтобы стало возможным отличить вызов нужного конструктора.
Наша техника кажется предпочтительнее во всех отношениях. Минимум усилий (никаких процедур создания), если инициализация по умолчанию применима; при желании позволяет предотвратить создание объектов клиентами; вводит столько процедур создания, сколько необходимо, не создавая никаких коллизий; каждая процедура создания имеет собственное имя, облегчая понимание ее предназначения, например make_polar.
Модель периода выполнения определяет важную роль ссылок. Рассмотрим некоторые их свойства, в частности, понятие пустой ( void ) ссылки и связанные с ней проблемы.
Ссылка может находиться в одном из двух состояний - она может быть пустой или присоединенной. Мы уже видели, что изначально ссылка всегда находится в состоянии void и может стать присоединенной благодаря созданию объекта. Вот как выглядит более полная картина, показывающая все возможности перехода между состояниями:
(рис 8.11) Возможные состояния ссылки и переходыПомимо создания, ссылка может изменять состояние в результате присваивания. Проверьте себя, понимаете ли вы разницу между тремя понятиями - объектом, ссылкой и сущностью:
void, либо она присоединена к объекту. Точное определение "присоединения" уже появлялось. Присоединенная ссылка однозначно идентифицирует объект.Если b - сущность ссылочного типа, то ее значением в период выполнения является ссылка, которая может быть присоединена к объекту O. В этом случае говорим, что сущность b присоединена к O.
В большинстве случаев мы ожидаем, что ссылка присоединена к объекту, хотя допустимо иметь и пустые ссылки. Ссылки void играют важную роль в ОО-модели вычислений. В предыдущей лекции подробно разбиралась фундаментальная операция ОО-модели - вызов компонента - применение к экземпляру класса компонента этого класса. Вот как это пишется:
some_entity.some_feature (arg1, ...)
Для корректного выполнения вызова сущность some_entity должна быть присоединена к нужному целевому объекту. Если случится, что some_entity ссылочного типа и имеет значение void, то вызов не может быть обработан, так как необходим целевой объект.
ОО-система никогда не должна в момент выполнения вызывать компонент с целевым объектом void. Результатом подобного вызова будет исключение (exception).(Исключения и их обработка будут изучаться в лекции 12)
Было бы прекрасно, если бы компилятор проверял программный текст и гарантировал, что подобные события не встретятся в период выполнения, точно также как он проверяет отсутствие несовместимости типов, используя соответствующие правила типизации. К сожалению, такая цель недостижима для компиляторов, если только не накладывать жестких ограничений на язык. Так что ответственность за то, чтобы все вызовы имели присоединенный целевой объект, возлагается на разработчика. Конечно, есть простой способ - окружать все вызовы тестом:
if "x не void" then x.f (...) else ... end
Этот прием не применим в качестве универсального требования, хотя и может использоваться, когда из контекста не следует, что целевой объект не будет пуст.
Вопрос о не пустоте ссылок является частью вопроса корректности ПО. Для проверки корректности системы необходимо проверить, что нет вызовов, применимых к void ссылкам, и что все утверждения (изучаемые в последующих лекциях) удовлетворяются в соответствующий момент выполнения. Проверка не пустоты, также как и проверка утверждений, могла бы проводиться специальным автоматом - верификатором, встроенным в компилятор или являющимся независимым средством. В отсутствие такого механизма подобные нарушения приведут к ошибке периода выполнения - исключению. Разработчики могут защитить ПО двумя путями:
Мы уже знакомы с одним из способов изменения значения ссылки x: использование инструкции создания в форме create x, позволяющей создать новый объект и присоединить его к ссылке. Имеются и другие интересные операции, доступные при работе со ссылками.
Классы, появляющиеся в этой лекции, не имели подпрограмм - у них были только атрибуты. Как отмечалось, такие классы почти бесполезны, так как у них нет способа изменить значение атрибутов. Необходимы способы модификации ссылок, не использующие при этом инструкций в духе языков Pascal-C-Java-C++, подобных присваиванию: my_beloved.loved_one := me (напрямую изменяющих у объекта поле loved_one ), что нарушает принцип скрытия информации и синтаксически некорректно в нашей нотации.
Для модификации полей объекта клиент обязан вызвать подпрограмму, специально поставляемую разработчиком класса для этих целей. Давайте включим в класс PERSON1 процедуру, позволяющую модифицировать поле loved_one. Вот результат:
class PERSON2 feature
name: STRING
loved_one, landlord: PERSON2
set_loved (l: PERSON2) is
-- Присоединить поле loved_one текущего объекта к объекту l.
do
loved_one := l
end
end
Процедура set_loved присваивает ссылочному полю loved_one текущего экземпляра PERSON2 значение другой ссылки l. Ссылочное присваивание (левая и правая части являются ссылками) присваивает значение источника (правой части) целевой ссылке (слева).
Эффект ссылочного присваивания очевиден: целевая ссылка становится присоединенной к объекту, к которому присоединен источник - или становится void, если такое значение имеет источник. Предположим, например, что мы начинаем с ситуации, изображенной на рис.8.12 , где поля landlord и loved_one всех изображенных объектов пока пусты.
(рис 8.12) Перед присваиванием ссылкеПредположим, что выполняется вызов процедуры:
a.set_loved (r)
Сущность a присоединена к объекту O1, а сущность r - к O3. В результате выполнения процедуры set_loved выполнится присваивание:
loved_one := l
Здесь в роли текущего объекта выступает объект O1, сущности l и r имеют одинаковое значение - ссылки на объект O3. В результате изменится значение поля loved_one объекта O1 - ссылка присоединится к другому объекту O3, как показано на следующем рисунке:
Если бы r было пустой ссылкой, то такой же в результате присваивания стала бы и ссылка в поле loved_one объекта O1.
(рис 8.13) После присваивания ссылки
Наряду с присваиванием возникает необходимость и в тесте - проверить, присоединены ли две ссылки к одному и тому же объекту. Для этого есть оператор эквивалентности =.
Если x и y - сущности ссылочного типа, то выражение:
x = y
истинно тогда и только тогда, когда обе ссылки пусты или присоединены к одному и тому же объекту. Противоположный оператор "не эквивалентно" записывается как /=.
Выражение:
r = a.loved_one
истинно в ситуации, представленной на рис.8.13 и ложно для ситуации рис.8.12 .
Заметьте, в операциях эквивалентности сравниваются ссылки, а не объекты, к которым они присоединены. Так что если две ссылки присоединены к разным объектам, результатом операции эквивалентности будет false, даже если объекты имеют все поля с одинаковыми значениями. Операции, сравнивающие объекты, а не ссылки, будут введены позднее.
Получить пустую ссылку достаточно просто - при инициализации по умолчанию все ссылки пусты. Однако удобно иметь специальное имя для ссылки, доступной в любом контексте и имеющей значение void. Предопределенный компонент
Void
играет эту роль.
Обычно компонент Void используется в тестах, проверяющих пустоту ссылок:
if x = Void then ...
и для того, чтобы присвоить некоторой ссылке это значение:
x := Void
В результате последнего присваивания происходит отсоединение ссылки от объекта, и она становится пустой. Эта ситуация показана на следующем рисунке:
(рис 8.14) Отсоединение ссылки от объектаПрисваивание Void ссылке не оказывает никакого влияния на объект, ранее присоединенный к ссылке, - разрывается только связь между ссылкой и объектом. Было бы некорректно рассматривать эту операцию как освобождение памяти, так как другие ссылки могут продолжать быть связанными с объектом (на рисунке x может быть отсоединено от объекта O1, но другие ссылки могут быть еще присоединены к нему). Об управлении памятью смотри следующую лекцию. |
Ссылочное присваивание приводит к тому, что две или несколько ссылок присоединяются к одному объекту. Иногда необходима другая форма присваивания, в результате которой мы хотим получить не копию ссылки, а копию объекта. Эта цель достигается при вызове функции клонирования clone.
Если y присоединено к объекту OY, выражение
clone (y)
означает создание нового объекта OX, такого, что он имеет те же поля, что и OY, и все соответствующие поля имеют идентичные значения. Если y равно void, то значение clone (y) также void.
Скопировать присоединенный к y объект и связать копию со ссылкой x позволяет присваивание:
[1] x := clone (y)
Вот иллюстрация этого механизма:
(рис 8.15) Клонирование объекта Наряду со сравнением ссылок необходим механизм, позволяющий сравнивать объекты. Этой цели служит функция equal. Вызов:
equal (x, y)
возвращает значение true, если и только если x и y оба имеют значение void или присоединены к двум объектам с идентичными полями. После выполнения присваивания с клонированием [1], состояние, непосредственно следующее за присваиванием, удовлетворяет equal (x, y).
Возможно, вы удивляетесь, почему у функции clone есть аргумент, а у функции equal - их два. Для ОО-стиля характерен квалифицируемый вызов в форме: y. и x.is_equal (y). Ответ появится в разделе обсуждения, но это будет еще не скоро, так что попытайтесь догадаться сами. |
Функция clone создает новую копию существующего объекта. Иногда целевой объект уже существует, и все, что необходимо, это скопировать значения полей. Процедура copy выполняет эту работу. Она вызывается обычным образом:
x.copy (y)
Сущности x и y должны быть одного и того же типа; эффект от выполнения - копирование полей объекта, присоединенного к y, в соответствующие поля объекта, присоединенного к x.
Как и во всех вызовах компонента, вызов copy требует, чтобы целевой объект x был не пуст. Дополнительно требуется, чтобы и y был не пуст. Эта неспособность иметь дело с пустыми ссылками отличает copy от clone.
Требование не пустоты y настолько важно, что должен существовать способ для его формального выражения. Фактически речь идет о более общей проблеме: как программа может задать предусловия на аргументы, передаваемые клиентом при ее вызове. Такие предусловия, являясь частным случаем общего понятия "утверждение" в деталях будут обсуждаться в последующих лекциях. Аналогично, нам хотелось бы уметь выражать в виде постусловия семантическое свойство, отмеченное выше, - результат выполнения clone удовлетворяет equal. |
Процедура copy может считаться более фундаментальной, чем функция clone в том смысле, что, по меньшей мере, для класса без процедуры создания можно выразить clone в терминах copy следующим образом:
clone (y: SOME_TYPE) is
-- Void если y равно void; иначе дублировать присоединенный к y объект
do
if y /= Void then
create Result --Правильно только в отсутствие процедур создания
Result.copy (y)
end
end
При вызове функции сущность Result автоматически инициализируется в соответствии с общими правилами для атрибутов. Вот почему нет необходимости в ветви else условного оператора: Result инициализируется значением Void, так что результатом функции будет void, если значение y это void.
Формы копирования и сравнения, реализуемые подпрограммами clone, equal и copy, называются поверхностными, поскольку они работают с объектами только на первом уровне, никогда не пытаясь следовать вглубь по ссылкам. Возникает необходимость для глубоких вариантов этих операций, рекурсивно дублирующих полную структуру.
Для понимания разницы рассмотрим пример, показанный на рис.8.16. Предположим, что мы начинаем в начальном состоянии A, где сущность a присоединена к объекту O1.
(рис 8.16) Различные формы присваивания и клонированияРассмотрим простое присваивание ссылки:
b := a
В состоянии B, показанном на рисунке, цель b в результате присваивания присоединена к объекту O1, к которому присоединен источник a. Никаких новых объектов не создается.
Далее рассмотрим операцию клонирования:
c := clone (a)
Эта инструкция, как показывает раздел C нашего рисунка, создает новый объект O4, с полями, идентичными полям объекта O1. Будут скопированы два ссылочных поля, и значения ссылок будут указывать на те же объекты O1 и O3, как и поля оригинального объекта O1. Но, заметьте, не происходит дублирования самого объекта O3, и никакого другого объекта помимо дублирования O1. По этой причине базисная операция clone называется поверхностным клонированием, - она останавливается на первом уровне объектной структуры.
Заметьте, при клонировании исчезли ссылки на себя. Ссылка landlord объекта O1 была присоединена к самому объекту O1. У объекта O4 это поле становится ссылкой на оригинал O1. |
В некоторых ситуациях вы, возможно, захотите пойти дальше и дублировать структуру рекурсивно без введения разделяемых ссылок. Функция глубокого клонирования deep_clone позволяет достичь цели. Процесс создания deep_clone (y) рекурсивно следует за всеми ссылочными полями, содержащимися в объекте, дублируя полную структуру. (Если y это void, то и результат будет также void.) Эта функция будет, конечно же, правильно обрабатывать циклические ссылочные структуры.
Нижняя часть на рисунке - раздел D - иллюстрирует выполнение этой операции:
d := deep_clone (a)
В этом случае не появляются новые разделяемые ссылки. Все объекты, прямо или косвенно доступные объекту O1, будут дублированы, создавая новые объекты O5, O6 и O7. Нет никаких связей между старыми объектами ( O1, O2 и O3 ) и новыми. Объект O5, дублирующий O1, имеет собственные ссылки на себя.
Так же, как необходимы операции глубокого и deep_equal сравнивает две объектные структуры, определяя их структурную идентичность. В примере, показанном на рисунке, deep_equal выполнимо для любой пары из a, b и d. В то же время equal (a, c) истинно, поскольку поля объектов O1 и O4 идентичны, equal (a, d) - ложно. Фактически equal не выполнимо ни для одной пары из d и любого элемента оставшейся тройки. В целом имеют место следующие свойства:
x := clone (y) или вызова x.copy (y), выражение equal (x, y) имеет значение true (в случае присваивания это свойство имеет место независимо от того, имеет ли y значение void ).x := deep_clone (y), выражение deep_equal (x, y) имеет значение true.Эти свойства будут отражены в постусловиях соответствующих подпрограмм.
Изучение глубокого копирования и эквивалентности приводит к механизму, обеспечивающему серьезные практические преимущества ОО-метода, естественно, при условии его доступности в среде разработки.
До сих пор обсуждение не затрагивало вопросов ввода и вывода. Но, конечно, ОО-системе необходимо общаться с внешним миром и другими системами. Такое общение предполагает возможность чтения и записи объектов в различные хранилища - файлы,
| Для простоты в этом разделе будем предполагать, что проблема сводится к чтению и записи файлов. Для этих операций будем использовать термины "возвратить" (retrieval) и "сохранить" (storage), адекватные терминам ввод и вывод (input, output). Изучаемые механизмы должны быть применимыми при использовании других средств коммуникации, например при посылке и получении объектов по сети. |
Для экземпляров таких классов, как POINT или BOOK1 сохранение и возвращение объектов не является какой-либо новинкой. Эти классы, используемые в качестве первых примеров этой лекции, имеют атрибуты таких типов, как INTEGER, REAL и STRING, для которых доступно хорошо понятное внешнее представление. Сохранение или возвращение экземпляра такого класса из файла подобно выполнению операций ввода-вывода записей в языке Паскаль или структур языка С. Для этих хорошо известных технических проблем существуют read и write, которые подобно clone и copy будут доступны для всех классов.
Но такие механизмы не могут нас полностью устраивать, поскольку они не управляют главным элементом объектной структуры - ссылками. Так как ссылки могут быть представлены адресами памяти или чем-то подобным, то и для них можно найти подходящее внешнее представление. Это не самая трудная часть проблемы. Сложнее обстоит дело с передачей смысла самих ссылок. Ссылки присоединены к объектам и бесполезны в отсутствие этих объектов. Так что, как только мы начинаем иметь дело с нетривиальными объектами - объектами, содержащими ссылки, нас перестают устраивать старые механизмы сохранения и возвращения, работающие только со значениями. Механизмы должны вместе с объектом обрабатывать и всех его связников (dependents) в соответствии со следующим определением:
Определение: связники, прямые связники
Прямыми связниками объекта являются объекты, присоединенные к его ссылочным полям, если таковые имеются.
Связниками объекта являются сам объект и (рекурсивно) связники его прямых связников.
Для структуры объектов, показанной на рис.8.17, было бы бессмысленно сохранить в файле или передать по сети только объект O1. Операция должна включать связников O1 - объекты O2 и O3.
(рис 8.17) Три взаимно зависимых объектаВ этом примере любой из трех объектов рассматривает оставшиеся два как своих связников. В примере, показанном на рис.8.18, объект W1 можно сохранить независимо, но сохранение объектов B1 или B2 требует сохранения также и W1.
(рис 8.18) Объекты "Book" и "Writer"Понятие связников неявно присутствует в представлении deep_equal. Вот общее правило:
Принцип Замыкания Сохраняемости (Persistence Closure principle)
Всякий раз, когда механизм сохранения сохраняет объект, он должен сохранять и связников этого объекта. Всякий раз, когда механизм возвращения возвращает объект, он должен возвращать и связников этого объекта, если они еще не были возвращены.
Базисным механизмом, реализующим эти цели, является библиотечный класс STORABLE, включенный в библиотеку Base. Основными компонентами класса STORABLE являются:
store (f: IO_MEDIUM) retrieved (f: IO_MEDIUM): STORABLE
Вызов x.store (f) сохраняет в файле, связанном с f, объект, присоединенный к x, вместе со всеми его связниками. Объект, присоединенный к x, называют головным объектом хранимой структуры. Порождающий класс для x должен быть потомком STORABLE. Это требуется только для класса головного объекта и не распространяется на связников.
Класс IO_MEDIUM это еще один класс библиотеки Base, предназначенный для работы не только с файлами, но и для передачи данных по сети. Очевидно, f не должно быть void, а присоединенный файл или сетевое устройство должны допускать запись.
Вызов возвращает структуру объектов, идентичную структуре, сохраняемой в f предыдущим вызовом store. Компонент - это функция, возвращающая в качестве результата ссылку на головной объект возвращаемой структуры объектов.
Механизм STORABLE это наш первый пример важного свойства сохраняемости (persistence) объектов. Объект сохраняем, если он продолжает существовать по окончании очередной сессии работы системы. Класс STORABLE обеспечивает только частичное решение проблемы, накладывая ряд ограничений:
retrieved воссоздает полную структуру объектов. Это означает невозможность использовать два или более таких вызовов для получения отдельных частей структуры, если только структуры не являются независимыми.В развитие этой темы следует перейти от понятия механизма сохранения к общему понятию ОО-базы данных, подробно рассматриваемому в лекции 13 курса "Основы объектно-ориентированного проектирования". В ней обсуждаются проблемы механизмов сохранения STORABLE и другие проблемы, такие как эволюция схем и идентичность объектов сохранения.
Отмеченные выше ограничения механизма STORABLE нисколько не умаляют его практическую ценность. Хорошо известно, что отсутствие подобного механизма - одно из главных препятствий на пути широкого использования сложных структур данных в традиционном окружении. Без него хранение данных требует значительных программистских усилий: для каждого вида структуры приходится писать несколько взаимосвязанных рекурсивных процедур, реализующих операции ввода-вывода, также как и специальные механизмы обхода
Предопределенный механизм STORABLE позволяет решить все эти проблемы независимо от того, какова структура объектов, ее сложность, учитывая при этом эволюцию ПО.
Типичным приложением механизма STORABLE является свойство SAVE. Рассмотрим интерактивную систему, например текстовый редактор, графический редактор или систему компьютерного проектирования. Во всех случаях пользователю необходимо предоставить команду SAVE, сохраняющую состояние текущей сессии в файле. Хранимая информация должна быть достаточной для продолжения работы, так что она должна включать все важные структуры данных системы. Механизм STORABLE и хороший выбор головного объекта позволяет реализовать свойство SAVE одной командой:
head.store (save_file)
Уже одной этой причины достаточно для рекомендации выбора ОО-окружения в сравнении с другими более традиционными средами разработки.
Обсуждение структуры объектов времени выполнения показало важную роль ссылок. Для завершения картины необходимо выяснить, как работать со значениями, представляющими собой не ссылки на объекты, а непосредственно сами объекты.
До сих пор все значения целочисленных, булевых и других аналогичных типов рассматривались как ссылки на объекты. Однако по двум причинам необходимы сущности, значениями которых являются объекты:
BOOLEAN, INTEGER и др.) обрабатываются аналогично типам, введенным разработчиком (POINT, BOOK и др.). Тем не менее, если используется сущность n типа INTEGER, то в большинстве случаев удобнее полагать, что значение n - целое число, а не ссылка на объект содержащий целое число. Это удобнее отчасти по соображениям эффективности. Понятно, что для размещения целочисленных объектов необходимо больше памяти, а на обработку косвенного доступа к ним - дополнительное время. Кроме того, концептуально целое число и ссылка на целое число - совершенно различные понятия. Этот довод важен, если нашей целью является построение точной модели.O1 подобъекта O2, а не ссылки на внешний объект O2. Причиной такого подхода могут быть повышение эффективности, точное моделирование или и то, и другое.Удовлетворить потребность в составных объектах очень просто. Пусть C - класс, определенный так, как это делалось до сих пор
class C feature ... end
Класс C может использоваться в качестве типа. Любая сущность типа C является ссылкой. По этой причине C называется ссылочным типом (reference type).
Теперь предположим, что нам необходима сущность x, значение которой во время выполнения будет экземпляром C, а не ссылкой на такой экземпляр. Это достигается следующим объявлением x:
x : expanded C
Эта нотация использует новое ключевое слово expanded (развернутый). Нотация expanded C означает, что экземпляры этого типа в точности соответствуют экземплярам C. Единственное отличие от обычного объявления типа состоит в том, что сущности типа C обозначают ссылки, которые могут быть присоединены к экземплярам C, а сущности типа expanded C обозначают непосредственно экземпляры C.
Таким образом, к структуре, определенной в предыдущих разделах, добавлено понятие составного объекта (composite object). Объект O называется составным, если одно или более его полей являются объектами - подобъектами (subobjects) O . Следующий класс является примером описания
class COMPOSITE feature ref: C sub: expanded C end
Класс COMPOSITE имеет два атрибута: ref, обозначающий ссылку, и sub, обозначающий подобъект. Вот как выглядит прямой экземпляр COMPOSITE.
(рис 8.19) Составной объект с одним подобъектомПоле ref является ссылкой, присоединенной к экземпляру C (возможно, пустой ссылкой). Поле sub содержит экземпляр C и не может быть пустым.
Удобно несколько расширить нотацию. Иногда при проектировании класса, например E, хотелось бы установить, что все экземпляры класса должны быть развернутыми. Чтобы сделать это требование явным, следует объявить класс в следующей форме:
expanded class E feature ... Далее все аналогично любому другому классу ... end
Так определенный класс называется развернутым классом. Такое объявление класса никак не отражается на экземплярах класса, они остаются такими же, как если бы класс был объявлен просто class E. Но сущности типа E изменяются - теперь их значения не ссылки, а сами объекты. Как следствие этой новой возможности понятие
Определение: развернутый тип
Тип является развернутым в двух случаях:
Он задан в форме: expanded C
Он задан в форме E, где E - развернутый класс.
Объявление вида
x: expanded E
где E - развернутый класс, не будет ошибкой, поскольку эквивалентно
x: E
Таким образом, имеется два вида типов. Тип, не являющийся развернутым, является ссылочным типом. Эту терминологию можно использовать и для сущностей - ссылочные сущности и развернутые сущности. Аналогично и классы могут быть ссылочными и развернутыми.
Почему нам нужны
Первое применение наиболее очевидно: без
Аргумент производительности, однако, не является ключевым. ОО-конструирование ПО зачастую рассматривается как моделирование. Для отражения реальности необходимо моделировать объект как составной, а не как объект со ссылками. Это концептуальная проблема, а не проблема реализации.
Рассмотрим два объявления атрибутов:
D1. ref: S D2. exp: expanded S
Объявления появляются в классе C, предполагается также, что S это ссылочный класс. Объявление D1 отражает тот факт, что каждый экземпляр класса C "знает о" существовании некоторого экземпляра S (если только ref не является void ). Объявление D2 более требовательное: оно устанавливает тот факт, что каждый экземпляр класса C "содержит" экземпляр S. Даже если не думать о проблемах реализации, следует понимать, что речь идет о двух разных отношениях.
Отношение "содержит", поддерживаемое развернутыми типами, не допускает никакого разделения встроенного объекта, в то время как отношение "знает о" допускает несколько ссылок, присоединенных к объекту.
Вот пример объявления класса:
class WORKSTATION feature k: expanded KEYBOARD c: expanded CPU m: expanded MONITOR n: NETWORK ... end
Рабочая станция имеет клавиатуру, ЦПУ, монитор и подключена к сети. Клавиатура, ЦПУ и монитор являются частью данного компьютера и не могут разделяться двумя или несколькими рабочими станциями. Однако несколько рабочих станций подключены к одной и той же сети. Эти особенности проявляются в определении класса, использующем
(рис 8.20) Отношения между объектами: "знает о" и "содержит"Итак, концепция
Третье важное приложение REAL использовался, чтобы показать, как с помощью инфиксных и префиксных компонентов можно промоделировать понятие вещественного числа как класса. То же самое нетрудно проделать и для других BOOLEAN, CHARACTER, INTEGER, DOUBLE. Но проблема все же остается. Если классы рассматривать как ссылочные, то сущности
r: REAL
будут в период выполнения ссылками на возможные объекты, содержащие значение (в данном случае REAL ). Это неприемлемо: чтобы соответствовать общей практике программирования значение должно быть не ссылкой, а самим вещественным числом. Решение проблемы немедленно следует из обсуждения - класс REAL следует объявить как развернутый. Его объявление должно быть таким:
expanded class REAL feature ... Объявления компонент такие же как и ранее ... end
Все другие
В некоторых областях информатики - базах данных, моделировании, анализе требований - разработана классификация отношений, имеющих место между элементами моделируемой системы. В этих контекстах часто встречается отношение "агрегирования" (aggregation), выражающее тот факт, что каждый объект некоторого типа является агрегатом - содержит в своем составе ноль или более объектов, каждый из которых имеет свой собственный тип. Например: автомобиль является агрегатом, содержащим мотор, кузов и другие детали.
CAR с компонентами ENGINE и expanded BODY. Другой способ основан на том, что агрегирование представляется отношением "развернутый клиент". Говорят, что класс C является развернутым клиентом класса S, если он содержит объявление компонента типа expanded S (или просто S, если S развернут). Одно из преимуществ такого модельного подхода в том, что развернутый клиент - это частный случай общего отношения "быть клиентом", так что можно использовать общие рамки и нотацию, комбинируя зависимости, подобные агрегированию с зависимостями, допускающими разделение. Примером могут служить с одной стороны - отношение между и KEYBOARD, с другой - отношение между и NETWORK.
Используя ОО-подход, можно избежать множественности отношений, используемых в литературе по информационному моделированию, - все покрывается двумя отношениями: клиент (развернутый или нет) и наследование.
Рассмотрим E (в любой форме) и развернутую сущность x типа E.
Так как значение x это всегда объект, то он не может быть void. Так что булево выражение:
x = Void
будет всегда вырабатывать значение false, и вызов в форме x.some_ feature (arg1, ...) никогда не приведет к возбуждению исключения из-за void цели, что могло случиться для ссылочной сущности.
Пусть объект O является значением x. Как и в случае не пустой ссылки, говорят, что x присоединено к O. Итак, для любой сущности, значение которой не void, можем говорить о присоединенном объекте, независимо от типа - ссылочного или развернутого - сущности.
Что можно сказать о создании развернутых объектов? Инструкцию:
create x
можно применить к развернутому x. Для ссылки x эффект достигался за три шага: (C1) создание нового объекта; (C2) инициализация его полей значениями по умолчанию; (C3) присоединение к x. Для развернутого x, шаг C1 неуместен, а шаг C3 бесполезен; так что единственный эффект состоит в инициализации полей значениями по умолчанию.
В общем случае, в случае присутствия
class F feature u: BOOLEAN v: INTEGER w: REAL x: C y: expanded C z: E ... end
Класс E развернут, а класс C нет. Инициализация прямого экземпляра F включает установку поля u в false, v - в 0, w - в 0.0, x - ссылкой void, а экземпляры y и z станут экземплярами классов C и E соответственно, чьи поля будут инициализированы в соответствии со стандартными правилами. Этот процесс инициализации может быть рекурсивно продолжен, поскольку поля экземпляров C и E могут быть в свою очередь развернутыми.
Как можно было понять, использование C иметь атрибут типа expanded D, если класс D имеет атрибут типа expanded C. Это означало бы, что каждый объект C включал бы подобъект D, который бы включал подобъект C и так далее. Сформулируем правило "развернутого клиента", ранее введенное неформально:
Правило Развернутого Клиента
Пусть отношение "развернутый клиент" определяется следующим образом: класс C является развернутым клиентом класса S, если некоторый атрибут C является развернутым типом, основанным на классе S.
Тогда отношение развернутый клиент не может включать никаких циклов.
Другими словами, не может существовать множества классов A, B, C, ... N, где каждый последующий является развернутым клиентом предыдущего, а последний класс N является развернутым клиентом класса A. В частности, класс A не может иметь атрибут типа expanded A, так как это делало бы класс A своим развернутым клиентом.
Заключительное замечание ответит на вопрос, как сочетаются ссылки и подобъекты. Развернутый класс или
(рис 8.21) Подобъект со ссылкой на другой объектПриведенная ситуация предполагает следующие объявления:
Class COMPOSITE1 feature other: SOME_TYPE sub: expanded C end class C feature ref: D x: OTHER_TYPE; y: YET_ANOTHER_TYPE end class D feature ... end
Каждый экземпляр класса COMPOSITE, такой как O_COMP на рис.8.21, имеет подобъект, ( на рисунке) содержащий ссылку ref, которая может быть присоединена к объекту ( OD на рисунке).
Но противоположная ситуация, где ссылка становится присоединенной к объекту, невозможна. Это будет следовать из правил присваивания и передаче аргументов, изучаемых в следующем разделе. Итак, структура времени выполнения никогда не может находиться в ситуации, показанной на рис.8.22, где OE содержит ссылку на , - подобъект O_CMP1, и содержит ссылку на себя.
(рис 8.22) Ссылка на подобъектЭто правило открыто для критики, поскольку оно ограничивает моделирующие возможности подхода. Предыдущая версия нотации книги допускала ссылки на подобъекты. Но эта возможность порождала больше проблем, чем она того стоит:
В этом разделе рассматривается специальная информация, и он может быть пропущен при первом чтении.
Введение
Семантика присваивания, как отмечалось, распространяется еще на одну операцию - передачу аргумента при вызове подпрограмм. Предположим, существует подпрограмма (процедура или функция) в форме:
r (..., x: SOME_TYPE, ...)
Здесь сущность x это один из формальных аргументов r. Рассмотрим теперь некоторый вызов r в любой из двух возможных форм - квалифицированный или неквалифицированный вызов:
r (..., y, ...) t.r (..., y, ...)
Выражение y является фактическим аргументом, передаваемым формальному аргументу x.
Выполнение r при любом из этих вызовов начинается с инициализации формальных аргументов значениями соответствующих фактических аргументов. Для простоты и согласованности правила, определяющие передачу аргументов, те же, что и правила присваивания. Другими словами, инициализация формального аргумента эквивалентна выполнению присваивания:
x := y
Это правило приводит к определению:
Определение: Присоединение
Присоединение y к x является результатом выполнения следующих двух операций:
Присваивания в форме x := y
Инициализации x при вызове подпрограммы, где x - формальный аргумент, а y - фактический аргумент вызова.
В обоих случаях x является целью присоединения, а y - источником.
Одни и те же правила действуют в обоих случаях для определения корректности присоединения (в зависимости от типов цели и источника). При условии корректности одни и те же правила определяют, каков будет эффект присоединения в период выполнения.
При изучении ссылочного присваивания мы уже познакомились с эффектом присоединения. Если источник и цель являются ссылками, то эффект присваивания:
x := y
и соответствующей передачи аргументов состоит в том, что x получает значение ссылки y. Это иллюстрировалось несколькими примерами. Если значением y является void, то операция вместо присоединения сделает и x равным void ; если y присоединен к объекту, то и x будет присоединен к этому же объекту.
Что происходит, когда типы x и y развернуты? Ссылочное присваивание не имеет смысла, а вот поверхностная форма копирования вполне возможна. Так и происходит. Рассмотрим объявления:
x, y: expanded SOME_CLASS
Присваивание x := y будет копировать каждое поле объекта, присоединенного к y, в соответствующие поля объекта, присоединенного к x, создавая тот же эффект, что и выполнение:
x.copy (y)
Копирование также является легальной операцией, эквивалентной в этом случае присваиванию. (В случае ссылок копирование и присваивание тоже легальны, но имеют разный эффект.)
Семантика копирования для INTEGER, то мы ожидаем от присваивания m := n, (или от соответствующей передачи аргументов) копирования значения n в m.
Проведенный анализ применим и к связанной с присваиванием операции эквивалентности. Рассмотрим булевы выражения: x = y и x /= y. Для x и y ссылочных типов, как уже отмечалось, истинность первого выражения (ложность второго) достигается только тогда, когда источник и цель оба имеют значение void или оба присоединены к одному и тому же объекту. Для развернутых x и y, такая семантика неприемлема, - здесь действует другая семантика, основанная на последовательном сравнении значений соответствующих полей, так что в этом случае выражение x = y имеет то же значение, что и equal (x, y).
Разрешается, как мы увидим позже при обсуждении наследования, изменить семантику equal для придания специального смысла эквивалентности экземпляров некоторого класса. Это никак не отразится на операции эквивалентности =, которая по соображениям безопасности и простоты всегда имеет смысл оригинальной функции standard_equal. |
Правило присваивания и сравнения обобщается в следующем замечании.
Присоединение y к x означает копирование объекта x, если x и y принадлежат развернутым типам. Это ссылочное присоединение, если x и y ссылочного типа. Аналогично, тесты: x=y и x/=y означают сравнение объектов для x и y x и y ссылочного типа.
В рассматриваемых до сих пор случаях источник и цель принадлежали одной категории - оба развернутого или ссылочного типа. Что если они из разных категорий?
Вначале рассмотрим ситуацию, когда в присваивании x := y цель x y - ссылочного типа. Единственно приемлемой в этом случае является семантика копирования: копирование полей объекта, присоединенного к y, в поля объекта, присоединенного к x. Все хорошо, если y не void в период выполнения. Если y - void, то результатом будет включение исключения. (Исключения изучаются в лекции 12)
Для развернутого x тест x = Void не является причиной появления исключительной ситуации; он просто дает значение false. Но нет приемлемой семантики для присваивания x := Void, так что всякая подобная попытка приводит к появлению исключения. |
Рассмотрим теперь другой случай присваивания: x := y, где x ссылочного типа, а y - развернутого. Тогда в период выполнения y всегда присоединен к объекту, который мы можем назвать OY, и присоединение также должно присоединить x к объекту. Казалось бы, что можно присоединить x непосредственно к OY. Однако это привело бы к созданию ссылки на подобъект, а подобные ссылки запрещены нашими правилами. Поэтому правильной стратегией является клонирование источника OY и присоединение x к созданной копии. Рассмотрим пример:
class C feature
...
end
class COMPOSITE2 feature
x: C
y: expanded C
reattach is
do x := y end
end
При вызове компонента reattach в результате присваивания x будет присоединен к объекту, являющемуся клоном объекта y.
Следующая таблица обобщает семантику присоединения изученных случаев:
| Тип цели x | Тип источника y | |
|---|---|---|
| Ссылочный | Развернутый | |
| Ссылочный | Ссылочное присоединение | Клонирование: эффект x := clone(y) |
| Развернутый | Копирование: эффект x.copy(y) Ошибка, если y - void | Копирование: эффект x.copy(y) |
Семантика операций, проверяющих эквивалентность ( = и /= ) должна быть совместимой с семантикой присваивания. Наряду с операцией = можно использовать и equal. Какую из этих операций следует применять, зависит от обстоятельств.
x и y - ссылки, их можно тестировать как на ссылочную эквивалентность, так и на объектную эквивалентность при условии, что ссылки не void. Мы определили операцию x = y, как обозначающую ссылочную эквивалентность в этом случае. Функция equal, введенная для проверки объектной эквивалентности, дополнена и применима, когда x или y - void.x и y - x - ссылка, y - x - void, возвращая значение false в этой ситуации, поскольку y не может быть void.Этот анализ дает желаемую интерпретацию равенства = во всех случаях. Для объектного сравнения всегда доступна функция equal, расширенная на случаи, когда один или оба операнда принимают значение void. Следующая таблица подводит итог семантике сравнения:
| Тип цели x | Тип источника y | |
|---|---|---|
| Ссылочный | Развернутый | |
| Ссылочный | Ссылочное сравнение | equal(x,y) объектное сравнение, если x не void, иначе - false |
| Развернутый | equal(x,y) объектное сравнение, если y не void, иначе - false | equal(x,y) объектное сравнение |
Сравнение таблиц 8.1 и 8.2 показывает совместимость присваивания и операций сравнения в упоминавшемся уже смысле. Напомним, в частности, что equal (x, y) будет истинно после выполнения x := clone (y) или x. copy (y).
Обсуждаемые проблемы возникают во всех языках, включающих ссылки и указатели, таких как Pascal, Ada,
В предыдущих разделах отмечалось, что два
Для x и y ссылочного типа при непустом значении y присваивание x := y или соответствующее присоединение в результате вызова приведут к тому, что x и y будут присоединены к одному и тому же объекту.
(рис 8.23) Разделение как результат присоединенияВ результате x и y становятся тесно связанными до тех пор, пока x или y не будет присвоено новое значение. В частности любая операция вида x.f, где f некоторый компонент соответствующего класса, приведет к тому же результату, что и y.f, поскольку воздействует на тот же объект.
Присоединение x к тому же объекту, что и y, известно как назначение динамического псевдонима (dynamic aliasing). Псевдоним является динамическим, поскольку существует только во время выполнения.
Статические псевдонимы закрепляют два имени за одним и тем же программным элементом в исходном тексте, и они всегда обозначают одно и то же значение вне зависимости от событий, происходящих во время выполнения. Этот прием включен в некоторые языки программирования. В Fortran директива означает, что две переменные разделяют содержимое одной и той же области памяти. Директива препроцессора C #define x y определяет, что любое упоминание x в тексте программы эквивалентно y. |
Наличие динамических псевдонимов оказывает более серьезное влияние на операции присваивания с участием сущностей ссылочного типа, нежели с участием сущностей x и y INTEGER присваивание x := y просто устанавливает для x значение y и никакого связывания x и y не происходит. После подобного присваивания с участием ссылочных типов x и y становятся псевдонимами одного объекта.
Неприятным последствием применения псевдонимов (и статических, и динамических) является воздействие операций на сущности, даже не упоминаемые в операциях.
Модель вычислений без псевдонимов обладает приятным свойством: приведенный ниже фрагмент всегда справедлив
[БЕЗ СЮРПРИЗОВ] -- Предположим, что свойство P(y) выполняется x := y C (x) -- P(y) останется выполнимым.
Этот пример подразумевает, что P (y) это частное свойство y, а C (x) некая операция с участием x, но не y. В этом случае никакие действия над x не влияют на значение y.
Для сущностей x и y типа INTEGER:
-- Предположим, что здесь y = 0 x := y x := -1 -- По-прежнему y "= 0.
В этом случае нет никакого способа изменить y путем присваивания значения x. Обратимся теперь к аналогичной ситуации с участием динамических псевдонимов. Пусть x и y экземпляры следующего класса C:
class C feature
boolattr: BOOLEAN
-- Булев атрибут для описания некоторого свойства объекта.
set_true is
-- Установка boolattr в true.
do
boolattr := True
end
... Другие компоненты ...
end
Теперь предположим, что тип y это C, и что y в определенный момент времени выполнения не является пустой ссылкой. Тогда следующий пример уже не обладает свойством "БЕЗ СЮРПРИЗОВ":
[СЮРПРИЗ, СЮРПРИЗ!]
-- Предполагаем, что y.boolattr равно false.
x := y
-- Значение y.boolattr по-прежнему false.
x.set_true
-- Но теперь y.boolattr равно true!
Последняя инструкция данного фрагмента никоим образом не содержит y, однако одним из ее результатов является изменение свойств y.
Отмеченные тревожные последствия операций присваивания с участием ссылок порождают законный вопрос о целесообразности сохранения динамических псевдонимов в нашей модели вычислений.
Ответ - частично теоретический и частично практический:
Поочередно рассмотрим оба указанных аспекта.
Предварительное рассмотрение свидетельствует о том, что сами ссылки и их разделение необходимы во многих случаях. Некоторые стандартные структуры данных содержат
(рис 8.24) Связный циклический списокНа самом деле необходимость в ссылках, присоединении и разделении ссылок возникает и в не слишком сложных ситуациях. Вернемся к одному из вариантов класса описывающего книгу
class BOOK3 feature ... Остальные компоненты ... author: WRITER end
Здесь необходимость разделения ссылок обусловлена тем, что две книги или более могут быть написаны одним и тем же автором. Во многих примерах данной лекции подразумевается разделение, - так в случае PERSON у нескольких персон может быть один лендлорд. Это вопрос потребностей моделирования, а не реализации.
Если b1 и b2 два экземпляра BOOK3 одного автора, то b1.author и b2.author - псевдонимы, то есть ссылки, присоединенные к одному объекту, и использование любой из них в качестве цели вызова даст в точности одинаковый эффект. Рассмотренные в таком свете динамические псевдонимы выглядят скорее не как потенциально опасная возможность программирования, а как факт из реальной жизни. Это цена, которую необходимо заплатить за возможности использования нескольких имен при обращении к одному объекту.
Можно легко найти нарушения приведенного выше свойства "БЕЗ СЮРПРИЗОВ", не обращаясь к области ПО. Пусть для некоторой книги b определены следующие свойства и операции:
NOT_NOBEL (b) обозначает: "автор никогда не получал Нобелевскую премию".NOBELIZE (b) обозначает: "Присудить Нобелевскую премию автору книги b".Теперь предположим, что rb обозначает книгу "Красное и черное", а cp - "Пармская обитель". Последующие действия вполне корректны:
[СЮРПРИЗ В ОСЛО]
-- Предположим, что сейчас выполняется NOT_NOBEL(rb)
NOBELIZE(cp)
-- Теперь свойство NOT_NOBEL(rb) уже несправедливо!
Операция над cp изменяет свойство другой сущности rb, которая не упоминается в инструкции! Последствия могут быть весьма значительными (редкая книга Нобелевского лауреата будет переиздана, ее цена возрастет и т.д.). В данной ситуации, не связанной с ПО, произошло в точности то же, что и в предыдущем программном примере после операции x.set_true, повлиявшей на состояние y без упоминания y.
Таким образом, динамические псевдонимы вовсе не являются результатом гнусных трюков программистов со ссылками и указателями. Это следствие свойственного человеку стремления давать имена вещам ("объектам" в наиболее общем смысле этого слова), а иногда и несколько имен одному предмету. В классической риторике эти явления известны как полионимия (polyonymy), например, использование имен "Кибела" (Cybele), "Деметра" (Demeter) и "Церера" (Ceres) "для одной и той же богини, и антономазия (antonomasia) - возможность ссылаться на объект, косвенно именуя его, как, например, в фразе "прекрасная дочь Агамемнона", обращаясь к прекрасной Елене из Трои.
Теперь накоплено достаточно подтверждений того, что любая система моделирования и разработки ПО должна поддерживать понятие ссылки, а, следовательно, и динамические псевдонимы. Как теперь справиться с неприятными последствиями? Невозможность обеспечить свойство "БЕЗ СЮРПРИЗОВ" показывает, что ссылки и псевдонимы подвергают опасности саму возможность систематического рассмотрения ПО. Это означает, что, изучая исходный текст, нельзя надежно и просто сделать какие либо выводы о свойствах ПО времени выполнения.
Для поиска решения необходимо сначала понять, является ли данная проблема специфической для ОО-метода. Знакомство с другими языками программирования, такими как Pascal, C, PL/I, Ada и Lisp убеждает в том, что и там ведутся подобные дискуссии. Все языки располагают средствами динамического размещения объектов и разрешают объектам содержать ссылки на другие объекты. Существенно различаются лишь уровни абстракции: указатели C и PL/I фактически являются машинными адресами, а Pascal и Ada наряжают указатели в более респектабельные одежды, используя правила типизации.
Что тогда нового в ОО-разработке? Ответ связан не с теоретическими возможностями метода (за исключением важных отличий, связанных со сборкой мусора, ОО-структуры времени выполнения идентичны своим аналогам в Pascal и Ada), а в практике разработки ПО. ОО-разработка подразумевает повторное использование. В частности, любой проект, в котором многочисленные прикладные классы выполняют хитрые манипуляции со ссылками, является примером некорректного использования ОО-подхода. Такие операции должны быть включены в библиотечные классы.
Для любой системы подавляющее большинство объектных структур, требующих нетривиальных операций со ссылками, не зависит от области приложения и представляет хорошо известные и часто используемые структуры: списки всевозможных типов, деревья в различных представлениях, хэш-таблицы и некоторые другие. В хорошей ОО-среде разработки библиотека должна быть легко доступной и предоставлять реализации подобных структур. В качестве иллюстрации в приложении A приведен эскиз библиотеки Base. Классы в таких библиотеках могут содержать множество ссылочных операций. Примером могут служить действия над ссылками, необходимые для вставки и удаления элемента связного списка или узла дерева.
Если же при разработке приложения появится потребность в сложных объектных структурах, не представленных адекватно в имеющихся библиотеках, то это следует рассматривать как потребность в новых классах общего назначения. Их необходимо разработать, потратить необходимое время на их тщательное тестирование и затем включить в соответствующую библиотеку. Такая ситуация в терминах одной из предшествующих лекций является примером перехода с позиций потребителя по отношению к повторному использованию на позиции производителя.
Оставшиеся операции со ссылками в классах, зависимых от конкретного приложения, должны быть ограничены простыми и безопасными операциями. В библиографических заметках упомянута статья Suzuki, развивающая эту идею.
В данной лекции введены некоторые правила и нотация для работы с объектами и соответствующими сущностями. Некоторые из этих соглашений могут вызвать удивление. Поэтому полезно завершить изучение объектов и их свойств рассмотрением спорных вопросов и доводов в пользу выбранных решений. Автор, естественно, надеется, что читатели полностью одобрят его выбор. Основная цель данной дискуссии - добиться полного понимания основополагающих проблем. В этом случае, если кто-то предпочитает другое решение, то сможет выбрать его вполне осознанно.
Для разминки начнем с небольшой проблемы, связанной с нотацией. Это конечно деталь, но из деталей складывается общая картина. Речь идет о наборе соглашений, используемых для графического представления классов и объектов.
В предшествующей лекции отмечалось насколько важно различать понятия класс и объект. Соответственно, должны отличаться и графические обозначения. Классы на диаграммах, представляющих системную архитектуру, представлены в виде эллипсов. Они соединяются стрелками для обозначения отношений между классами, обычными стрелками отмечаются отношения наследования и двойными -
Классы и объекты существуют в различных контекстах. Эллипсы классов являются частью диаграмм, представляющих структуру программной системы. Прямоугольники-объекты используются на моментальных снимках состояния системы в процессе выполнения. Поскольку указанные виды диаграмм имеют совершенно разное назначение, то в бумажном представлении, как в данной книге, они не появляются одновременно в одном контексте. Но для интерактивных CASE-средств ситуация принципиально меняется. В процессе отладки программной системы возникает необходимость отобразить объект, а затем - порождающий класс для изучения его компонент, родителей и других свойств.
Используемые для классов и объектов графические соглашения совместимы со стандартом, установленным методом BON (Nerson и Walden). В методе BON, (Business Object Notation) предназначенном для использования в интерактивных CASE-средствах и в документации, классы отображаются в виде пузырьков, растягиваемых по вертикали, показывающих компоненты класса, инварианты и другие свойства.(О BON см. библиографические заметки и лекцию 9 курса "Основы объектно-ориентированного проектирования")
В развитие нашего соглашения поля
Трудно удержаться от того, чтобы не процитировать следующий ненаучный аргумент, заимствованный из рецензии Ian Graham на книгу по ОО-анализу, использующую другие графические соглашения:
Мне не нравятся классы, изображаемые в виде треугольников с острыми углами. Мне кажется, что это их экземпляры имеют острые углы, так что можно пораниться, уронив их на ногу, а классы безопасны и поэтому у них должны быть скругленные углы.
Важный синтаксический вопрос - должны ли мы установить синтаксическое различие при работе со ссылками и простыми значениями. Как отмечалось, присваивание и эквивалентность имеют различный смысл для ссылок и значений :=, =, /= ). Не опасно ли это? Не предпочтительнее ли использовать различные наборы символов, напоминая тем самым, что они имеют разный смысл?
Два набора символов использовались в языке Simula 67. Немного изменив нотацию для совместимости с настоящей книгой (в языке Simula reference сокращается до ref ), в Simula можно записать объявление сущности ссылочного типа C так:
x: reference C
Ключевое слово reference указывает, что экземпляры x будут ссылками. Рассмотрим объявления:
m, n: INTEGER x, y: reference C
Нотация Simula, используемая для операций с простыми и ссылочными типами, приведена в таблице.
| Операция | Операнды |
Операнды-ссылки |
|---|---|---|
| Присваивание | m := n | x :- y |
| Проверка на равенство | m = n | x == y |
| Проверка на неравенство | m /= n | x =/= y |
Соглашения Simula лишены неоднозначности. Почему бы их не сохранить? К сожалению, эти соглашения могут служить примером благих намерений, приносящих скорее вред, нежели пользу. Проблемы начинаются в прозаической области поиска ошибок. Два набора символов похожи, - это провоцирует синтаксические ошибки, подобные использованию := вместо :-.
Такие ошибки будут обнаружены компилятором. Но хотя ограничения, проверяемые компилятором, предназначены помочь программисту, - здесь это может не сработать. Либо вы знаете разницу между семантикой ссылок и значений, и тогда подсказки компилятора о необходимости проверки, каждый раз, когда вы написали присваивание или равенство, могут показаться раздражающими. Либо вы не понимаете этой разницы, тогда его подсказки немногим могут помочь.
Но самый важный аспект соглашений Simula в том, что он не оставляет выбора: для ссылок нет доступной конструкции, дающей семантику значений. Представляется разумным для сущностей a и b ссылочного типа иметь два множества операций:
a :- b для ссылочных присваиваний и a == b сравнения ссылок;a := b для присваивания путем копирования (эквивалент a := clone (b) или a.copy (b) в нашей нотации) и a = b для сравнения объектов (эквивалент нашего equal (a, b) ).Но за одним исключением, Simula поддерживает только первое множество операций. Если необходимы операции второго множества ( copy или clone, сравнение объектов), придется написать специальные подпрограммы для каждого целевого TEXT, для которого Simula предлагает оба множества операций.
Кстати, при дальнейшем анализе идея предоставления двух множеств операций для всех ссылочных типов не кажется уже столь разумной. Это бы означало, что тривиальная описка - использование := вместо :-, теперь уже не обнаруживалась бы компилятором, а приводила бы к результату, далекому от ожидаемого, например, к клонированию вместо ссылочного присваивания.
Как результат этого анализа, нотация этой книги использует соглашения, отличные от тех, что используются в Simula: одни и те же символы применимы для развернутых и ссылочных типов с различной семантикой. Эффекта семантики значений можно достигнуть для объектов ссылочного типа при использовании предопределенных подпрограмм, применимых для всех типов:
a := clone (b) или a.copy (b) для объектного присваивания;equal (a, b) для сравнения объектов на идентичность всех полей.Эта нотация существенно отличается от нотации, применяемой для их ссылочных двойников, ( := и =, соответственно) что снижает риск появления недоразумений.
Помимо чисто синтаксических аспектов, эта проблема интересна и тем, что она представляет типичный образец компромиссов, возникающих при проектировании языка, когда требуется найти баланс между конфликтующими критериями. Один из критериев, победивших в Simula, - может быть сформулирован следующим образом:
Но другие силы, доминирующие в нашей нотации, требуют:
INTEGER ) присваивание и сравнение значений представляются наиболее употребительными операциями. Для ссылок, в то же время, ссылочное сравнение и присваивание используется чаще, чем клонирование, копирование и сравнение объектов. Поэтому в обоих случаях предпочтительнее использовать := и = для фундаментальных операций.:- и := ), но с различной семантикой.Еще одна причина играет роль в данном случае, хотя она включает механизм, пока еще не изученный. В последующих лекциях мы познакомимся с родовыми или LIST [G], где G, известно как формальный родовой параметр, представляющий произвольный тип. Такой класс может манипулировать сущностями типа G и использовать их в присваиваниях и проверках на равенство. Клиенты, нуждающиеся в использовании такого класса, должны позаботиться о создании типа, служащего в качестве фактического родового параметра. Например, они могут использовать LIST [INTEGER] или LIST [POINT]. Как показывают эти примеры, фактический родовой параметр может быть a и b имеют тип G, то часто полезно использовать присваивания в форме a := b или тесты в форме a = b с намерением получить семантику значений, когда фактический параметр принадлежит развернутому типу, такому как INTEGER, и ссылочную семантику - для ссылочного типа, такого как POINT.
Примером подпрограммы, нуждающейся в таком дуальном поведении, является процедура вставки элемента x в список. Процедура создает новый элемент списка. Если x целое, элемент должен содержать копию значения x. Если x является ссылкой, то элемент списка должен содержать ссылку на объект, присоединенный к x. |
В таких случаях, правила, определенные выше, гарантируют желаемое дуальное поведение, что было бы недостижимо, если бы требовался различный синтаксис для двух видов семантики. С другой стороны, если во всех случаях требуется единая семантика, то и это достижимо: такое поведение может быть только семантикой значений (так как семантика ссылок не имеет смысла для clone (или copy) и equal, а не ( := и = ).
Форма вызова подпрограмм clone и equal является стилевой особенностью, которая может вызвать удивление. На первый взгляд нотация:
clone (x) equal (x, y)
выглядит не слишком объектно-ориентированной. Догматичное следование принципу "ОО-стиля вычислений" из предыдущей лекции предполагает другую форму (См. "Объектно-ориентированный стиль вычислений", лекция 7):
x.twin -- twin это двойник - клон. x.is_equal (y)
В первой версии нотации так и делалось, однако возникла проблема пустых ссылок. Вызов компонента вида x.f (...) не может быть корректно выполнен в случае пустого x во время выполнения. В этом случае вызов инициирует исключение, которое повлечет аварийное завершение всей системы, если в соответствующем классе не предусмотрена обработка исключений. Поскольку во многих случаях x может быть действительно пустой ссылкой, то это означало бы, что каждый вызов
if x = Void then
z := Void
else
z := x.twin
end
Соответственно, реализация вызова is_equal должна выглядеть ( and then является вариантом and. См. "Нестрогие булевы операции", лекция 13):
if ((x = Void) and (y = Void)) or ((x /= Void) and then x.is_equal (y)) then ...
Излишне говорить, что не следует придерживаться этих соглашений. Нам быстро надоест писать подобные витиеватые фрагменты, а когда мы забудем это сделать, то результатом будет ошибка времени выполнения. Окончательный вариант соглашений, сформулированный в данной лекции, замечателен еще и тем, что дает ожидаемые результаты для x, равного void, - clone (x) вернет void, а equal (x, y) вернет true, если и y - void.
Вызов процедуры copy в форме x.copy (y) не создает подобных проблем, поскольку требует непустых x и y. Это следствие семантики процедуры copy, копирующей поля одного объекта в поля другого, и имеющей смысл, только если существуют оба объекта. Как показано далее, такое условие для y фиксируется формальным предусловием copy, заданным в явном виде в документации.
Отметим, что введенная выше функция is_equal существует в библиотеке системы. Причина в том, что часто удобнее определить специфические варианты эквивалентности элементов конкретного класса, перекрыв семантику по умолчанию. Для достижения этого эффекта достаточно переопределить функцию is_equal в соответствующем классе. Функция equal определяется в терминах is_equal (выражением, показанным выше при иллюстрации использования is_equal ), и поэтому следует за всеми переопределениями is_equal.
Когда есть функция clone, то нет необходимости в . Это связано с тем, что функция clone определена как создание объекта с последующим вызовом copy. Поэтому для адаптации clone к специфике класса достаточно переопределить процедуру copy данного класса. (См. также лекция 16)
Последние комментарии частично прояснили вопрос о статусе универсальных операций clone, copy, equal, is_equal, deep_clone, deep_equal.
Эти операции не являются языковыми конструкциями, невзирая на их фундаментальную значимость для практики. Они поставляются классом ANY основной библиотеки Kernel. Этот класс имеет то специальное свойство, что каждый класс, созданный разработчиком, автоматически становится наследником (прямым или косвенным) класса ANY. Вот почему становится возможным переопределить вышеупомянутые компоненты для поддержки специального вида эквивалентности или копирования. (См. "Глобальная структура наследования", лекция 16)
Сейчас нет необходимости в деталях, поскольку мы еще вернемся к этой проблеме при изучении наследования. Но уже теперь полезно знать, что благодаря механизму наследования, мы можем полагаться на библиотечные классы, поддерживающие свойства, доступные всем классам, - и каждый класс может изменить их, приспосабливая к своим, специфическим целям.
void ) или присоединенной к объекту. Проверка условия x = Void позволяет определить текущее состояние ссылки. Корректное выполнение вызова x.f (...) возможно, если x не пустая ссылка.class C ..., то сущность типа C будет обозначать ссылку, которая может быть присоединена к экземпляру C. Если начало объявления выглядит как expanded class D ..., то сущность типа D будет обозначать объект (экземпляр D ) и никогда не может быть пустой ссылкой.BOOLEAN, CHARACTER, INTEGER, REAL, DOUBLE ) определены как развернутые классы.create x создает объект, инициализирует его поля значениями по умолчанию и присоединяет к нему x. Если в классе определены порождающие процедуры создания, то выполнение инструкции вида create x.creatproc (...) приведет, кроме того, к заданной специфической инициализации полей.:= ) и проверка эквивалентности ( = ) являются ссылочными операциями. Для сущностей Понятие идентичности объекта играет важную роль для баз данных, особенно объектно-ориентированных. Смотри лекцию 13 курса "Основы объектно-ориентированного проектирования", посвященную таким базам данных, и библиографию к ней.
Графические обозначения метода BON (Business Object Notation) разработаны Jean-Marc Nerson и Kim Walden [Walden 1995]. James McKim и Richard Bielak детально рассмотрели преимущества альтернативных порождающих процедур [Bielak 1994].
Риски, связанные с нетипизированными указателями и ссылочными операциями, уже долгое время волнуют специалистов в области методологии программирования, порождая намеки на то, что в области данных это аналог ненавистной операции goto в области управления выполнением кода. В удивительно малоизвестной статье Nori Suzuki [Suzuki 1982] обсуждается возможность избежать в рамках строгого подхода с использованием высокоуровневых операций проблем с динамическими псевдонимами (как избавляются от применения goto, используя приемы "структурного программирования"). Хотя по признанию автора выводы неутешительны, данная статья весьма полезна.
Я признателен Ross Scaife из Университета Кентукки за помощь по вопросам риторики. См. его страницу http://www.uky.edu/ArtsSciences/Classics/rhetoric.html.
Напишите классы BOOK and WRITER описывающие книги и их авторов, используя заготовки из данной лекции. Обратите внимание на необходимость включения всех важных подпрограмм, а не только атрибутов.
Напишите класс PERSON включающий простое понятие личности с атрибутами mother, father и sibling (следующий по старшинству брат или сестра, если они есть). Включите подпрограммы возвращающие списки имен родителей, двоюродных братьев и сестер, дядюшек и тетушек, свекра и свекрови, тестя и тещи данного лица. Совет: пишите рекурсивные процедуры, но внимательно избегайте бесконечных рекурсий для отношений, например, двоюродный брат или сестра, являющихся циклическими.
Предположим, вы часто используете сравнение в форме x.is_equal (y), и хотите упростить нотацию, используя преимущества инфиксной записи (применимой здесь, поскольку наша функция имеет один аргумент). Для инфиксного компонента используйте некоторый оператор $$\S,$$ вызов тогда будет записываться в виде $$x \S y$$. Это маленькое упражнение потребует выбора для оператора $$\S,$$ подходящего для данной ситуации символа, совместимого с правилами инфиксных операторов. Конечно, здесь может существовать много возможных ответов, выбор одного из которых частично (но только частично) дело вкуса. (См. "Компоненты-операторы", лекция 7)
Изучение объектных структур в данной лекции может служить весьма хорошим примером того, насколько неправильно отделять вопросы реализации от проблем будто бы "высокого" уровня. В процессе рассмотрения новых технических приемов, связанных с вопросами реализации, приходит более глубокое понимание абстрактных понятий. Типичным примером может служить введение ссылочных и развернутых значений, представляющих, на первый взгляд, неприметное техническое решение. В действительности, это ответ на общий вопрос об отношении части и целого, постоянно обсуждаемый в дискуссиях по ОО-анализу.
В некоторой части компьютерной литературы принижается значение реализации и считается, что самое важное - это анализ. Но разработка ПО - это разработка моделей. Хорошая техника реализации часто является одновременно и хорошим средством моделирования. Помимо программных систем ее можно использовать и во многих других областях.
Данная лекция в большей степени посвящена моделированию, нежели реализации в строгом смысле этого термина. В ней показано, как можно использовать объектные структуры для построения реалистичных и полезных операционных описаний различного вида систем.
В процессе выполнения ОО-система создает некоторое число объектов. Организация этих объектов и отношения между ними определяют конструкцию времени выполнения. Рассмотрим свойства объектов.
Прежде всего, необходимо напомнить смысл термина "объект". Полная ясность была внесена в предыдущей лекции в виде строгого определения (Определение и объективное правило, см. лекцию 7):
Определение: объект
Объект - это экземпляр некоторого класса
Во время выполнения программная система, содержащая класс C, может в разных точках, используя процедуры создания или клонирования, создавать экземпляры C, - структуры данных, соответствующие образцу, заданному классом C. Например, экземпляр класса POINT представляет собой структуру данных, состоящую из двух полей, соответствующих атрибутам x и y класса. Экземпляры всех возможных классов составляют множество объектов системы.
Это официальное определение в мире ОО-ПО. Но в повседневном языке термин "объект" имеет гораздо более широкий смысл. Любая программная система связана с определенной внешней системой, которая может содержать "объекты": точки, линии, поверхности и тела в EMPLOYEE в системе расчета зарплаты, экземпляры которого являются
Хорошим следствием дуализма слова "объект" является естественность и мощь ОО-метода, применяемого для direct mapping ), который, как отмечалось, является принципиальным требованием модульного проектирования. Неудивительно, что некоторые классы являются моделями внешних типов объектов проблемной области, а экземпляры классов - моделями реальных объектов. (См. "Прямое отображение", лекция 3)
Но не стоит переоценивать "реальность" слова "объект". В науке и технике существует большой риск в заимствовании слов естественного языка и придания им специального смысла. Термин "объект" настолько перегружен повседневным смыслом, что техническое его использование может стать источником недоразумений. В частности:
STATE, описывающий состояние системы, или класс COMMAND. (См. лекцию 20 и лекцию 21 курса "Основы объектно-ориентированного проектирования")Когда слово "объект" используется в этой книге, то из контекста ясно, в общем или техническом смысле используется этот термин. В тех случаях, когда эту разницу необходимо подчеркнуть, используется уточнение - программный объект или внешний объект.
Программный объект довольно простое существо, если известен класс, которому он принадлежит.
Пусть O - объект. По определению он является экземпляром некоторого класса. Точнее, он является прямым экземпляром (direct instance) только одного класса, например C.
С учетом наследования O будет тогда косвенным экземпляром других классов, - предков C. Это тема дальнейшего обсуждения; в данной дискуссии достаточно понятия прямого экземпляра. Везде, где не может возникнуть недоразумений, слово "прямой" будет опущено. |
Класс C называется порождающим классом ( generating class ) или просто генератором ( generator ) объекта O. Заметьте, C - программный текст, а O - структура данных времени выполнения, появляющаяся в результате работы рассмотренных ниже механизмов создания объектов.
Часть компонентов C является атрибутами. Эти атрибуты полностью определяют форму объекта, представляющего собой просто набор полей, по одному на каждый атрибут.
Рассмотрим класс POINT из предшествующей лекции (Текст класса POINT см. в лекции 7). Исходный текст имеет вид:
class POINT feature x, y: REAL ... Объявления подпрограмм ... end
Подпрограммы опущены, так как форма объектов полностью определяется атрибутами соответствующих классов. Данный класс имеет два атрибута x и y типа REAL, следовательно, его экземпляр - это объект с двумя полями, содержащими значения этого типа:
(рис 8.1) Экземпляр класса POINTВ данной книге объекты - набор полей - изображаются в виде смежных прямоугольников, содержащих соответствующие значения. Внизу курсивом в скобках дается имя класса (в данном случае - На диаграммах, представляющих структуру ОО-системы или части такой системы, классы изображаются в виде эллипсов. Эти соглашения позволяют не путать классы и объекты. |
Оба атрибута класса POINT относятся к типу REAL. Следовательно, соответствующие поля прямого экземпляра POINT содержат действительные числа.
Это пример полей, соответствующих атрибутам одного из "базовых" типов. Формально эти типы определены как классы, а их экземпляры принимают значения из предопределенных диапазонов. К базовым (предопределенным, встроенным)типам относятся:
BOOLEAN, может иметь только два различных экземпляра, соответствующих булевым значениям true и false ;CHARACTER, экземпляры которого представляют символы;INTEGER, экземпляры которого представляют целые числа;REAL и DOUBLE, экземпляры которых представляют действительные числа одинарной и двойной точности.Тип STRING, представляющий конечную последовательность символов, на данном этапе рассматривается как базовый. Далее будет показано, что в действительности он относится к другой категории. ("Строки", см. лекцию 13)
Для каждого базового типа необходимо определить правила записи их значений в исходных текстах. Соглашения просты:
BOOLEAN два различных экземпляра обозначаются как True и False.CHARACTER будет записываться как символ в апострофах: 'A'.STRING обозначается как последовательность символов в двойных апострофах: "Это строка".INTEGER используем обычную десятичную нотацию: 34, -675, +4.REAL или DOUBLE будет применяться как обычная нотация: 3.5 или -0.05, так и экспоненциальное представление: -5.e-2.Рассмотрим класс с атрибутами базовых типов:
class BOOK1 feature title: STRING date, page_count: INTEGER end
Типичный экземпляр класса выглядит так:
(рис 8.2) Объект, представляющий книгуПоскольку в настоящий момент нас в первую очередь интересует структура объектов, то в последующих примерах все компоненты классов будут атрибутами, а подпрограммы отсутствуют.
Это означает, что на данном этапе обсуждения объекты подобны записям или структурам в языках Pascal и C. Принципиальное отличие от этих языков выражается в том, что, благодаря наличию механизмов скрытия информации, клиенты классов не могут непосредственно присваивать значения полям таких объектов. В Pascal и в C с незначительными синтаксическими различиями допустимо объявление записи с последующим присваиванием (Внимание: Недопустимая нотация! Только для обсуждения.):
b1: BOOK1 ... b1.page_count := 355
Здесь во время выполнения полю page_count объекта, присоединенного к b1, присваивается значение 355. Для классов такая возможность не допускается. Предоставлять клиентам классов разрешение менять поля объектов было бы насмешкой над правилом скрытия информации. В этом случае терял бы смысл выборочный экспорт, управляемый автором класса. В ОО-подходе модификация значений полей допустима только с помощью процедур класса, добавляемых в том случае, если автор класса решит предоставить такую возможность своим клиентам. Далее такая процедура будет добавлена в класс BOOK1.
Разрешение присваиваний вида Сами разработчики Java отмечают, что программист может испортить объект при наличии общедоступного поля, так как значение такого поля можно изменить путем непосредственного присваивания. Многие авторы языков вводят такую возможность, а затем предупреждают: "не делайте этого". Логичнее определить метод и нотацию, поддерживающие такие ограничения. |
В ОО-ПО классы без подпрограмм редко имеют практическое значение. Исключением являются ситуации, когда в родительских классах определяется набор атрибутов, а потомки содержат необходимые подпрограммы. Другим примером могут служить классы, представляющие внешние объекты, которые принципиально невозможно модифицировать, например данные от внешних датчиков в системе реального времени. Но на данном этапе такой подход полезен для понимания основных концепций, подпрограммы будут добавлены позже.
Используя указанные выше типы, определим класс WRITER для описания автора книги:
class WRITER feature name, real_name: STRING birth_year, death_year: INTEGER end
(рис 8.3) Объект "писатель"
Чаще всего нам необходимы объекты с полями, представляющими другие объекты. Например, книга имеет автора, который представлен экземпляром класса WRITER.
Можно ввести понятие подобъекта. В новой версии класса BOOK2 его экземпляры содержат поле, являющееся объектом - экземпляром класса WRITER.
(рис 8.4) Два объекта "книга" с подобъектами "писатель"Такое понятие подобъекта, несомненно, полезно, и далее в этой лекции будет показано, как создавать соответствующие классы.
Но это не совсем то, что необходимо. В каждом экземпляре BOOK2 приходится дублировать информацию об одном и том же авторе в виде подобъекта. Причины неприемлемости такого решения:
WRITER повлекло за собой автоматическое обновление этой информации для всех объектов - книг данного автора.Лучшим является решение, представленное на рис.8.5 . Оно основано на новой версии класса, BOOK3.
Каждый экземпляр BOOK3 в поле author содержит ссылку ( reference ) на объект типа WRITER. Нетрудно дать точное определение.
Определение: ссылка
Ссылка - это значение времени выполнения. Она может быть пустой (void) или присоединенной (attached).
Присоединенная ссылка однозначно идентифицирует объект (присоединена к конкретному объекту).
(рис 8.5) Два объекта "книга" со ссылками на один и тот же объект "писатель"
(рис 8.6) Объект, содержащий пустую ссылку (Роман "Кандид" (Candide) опубликован анонимно)Определение ссылки не подразумевает конкретной аппаратно-программной реализации. Если ссылка не пуста, то она идентифицирует объект и может рассматриваться как абстрактное имя объекта.
Концепция ссылки должна, безусловно, иметь аналог при реализации. Программирование на уровне машинного кода использует адресацию, многие языки программирования содержат понятие указателя. Понятие ссылки является более абстрактным. Хотя ссылка, в конечном итоге, может быть представлена адресом, не следует из этого исходить. Ссылка может содержать адрес наряду с другой информацией.
Отличие ссылок от указателей выражается в том, что они типизированы. Они напоминают
Понятие ссылки приводит к концепции идентичности объектов. Каждый объект, созданный в процессе выполнения ОО-системы, уникален и идентифицируется независимо от значений его полей. Возможны две ситуации:
Эти наблюдения свидетельствуют о неоднозначности высказывания " a обозначает тот же объект, что и b ". Можно подразумевать различные объекты с одинаковыми данными (I1) или состояния одного и того же объекта до и после изменения значений полей (I2). Мы будем использовать второе толкование и считать, что значения полей заданного объекта могут изменяться в процессе выполнения, а он остается "тем же самым объектом". В случае (I1) будем говорить о равных (но различных) объектах, точное определение понятия равенства будет дано позже.
В соответствии с определением I2 можно сказать, что поля объекта могут изменяться и это не будет ошибкой. Термин "поле" обозначает одно из значений, составляющих объект, а не соответствующий идентификатор поля - имя одного из атрибутов порождающего класса. Каждому атрибуту класса соответствует поле объекта (1832 для атрибута |
Изучение того, как сделать объекты сохраняемыми (persistent), заставит нас продолжить изучение свойств идентичности объектов. (См. "Идентичность объектов", лекция 13 курса "Основы объектно-ориентированного проектирования")
Класс BOOK1 содержал атрибуты только базовых типов, его вариант BOOK3, содержит атрибут, представляющий ссылку на автора.
class BOOK3 feature title: STRING date, page_count: INTEGER author: WRITER -- Новый атрибут. end
Объявленный тип дополнительного атрибута author это просто имя соответствующего класса: WRITER. Это будет общим правилом: если имеется стандартное объявление класса
class C feature ... end
то объявление некоторой сущности типа C
x: C
обозначает значения, являющиеся ссылками на потенциальные объекты типа C. Такое соглашение, использующее ссылки, обеспечивает большую гибкость и приемлемо в большинстве случаев. Подробное обсуждение этого правила и других возможных решений содержится в последнем разделе данной лекции.
Ничто не препятствует объекту O1 в определенный момент выполнения системы содержать ссылку, присоединенную к самому O1. Такая ссылка на себя может быть косвенной. В ситуации на рис.8.7 объект, имеющий значением поля name: "Almaviva", сам является своим лендлордом (прямая циклическая ссылка). Фигаро любит Сюзанну, которая любит Фигаро (косвенная циклическая ссылка).
(рис 8.7) Прямые и косвенные ссылки на себяТакие циклы в динамических структурах возможны, только если
class PERSON1 feature name: STRING loved_one, landlord: PERSON1 end
содержит прямой цикл ( PERSON1 - клиент PERSON1 ).
Обратное утверждение неверно - присутствие цикла в объявлении класса не означает, что циклы обязательно появятся в структурах времени выполнения. Можно объявить класс
class PERSON2 feature mother, father: PERSON2 end
Класс является собственным клиентом. Однако если он моделирует соответствующие именам атрибутов отношения между людьми, то структуры времени выполнения никогда не будут содержать циклов, поскольку ни один человек не может быть собственным родителем или предком.
На основе предшествующего рассмотрения выясняется в первом приближении структура ОО-системы в процессе выполнения.
(рис 8.8) Возможная структура объектов во время выполненияСистема состоит из нескольких объектов с различными полями. Некоторые поля содержат значения базовых типов, а другие являются пустыми или присоединенными ссылками на другие объекты. Каждый объект является экземпляром некоторого типа, основанного на классе (на рисунке тип указывается под объектом). Некоторые типы представлены единственным экземпляром, но гораздо чаще присутствует несколько экземпляров одного типа. На рис.8.8 тип TYPE1 представлен двумя экземплярами, остальные - единственным. Некоторые объекты содержат поля только ссылочного типа (экземпляр TYPE4 ) или только базовых типов (экземпляр TYPE5 ). Могут присутствовать прямые или косвенные циклические ссылки (верхнее поле экземпляра TYPE2, по часовой стрелке от нижнего экземпляра TYPE1 ).
Подобная структура может показаться слишком запутанной. Впечатление от приведенной иллюстрации, демонстрирующей различные возможности, можно выразить выражением: "блюдо спагетти".
Это впечатление не совсем правильно. Впечатление простоты должен создавать программный текст, но не структура объектов периода выполнения. Текст отражает определенные отношения (такие как "любит", "имеет хозяина"). Конкретную структуру объектов периода выполнения можно назвать экземпляром таких отношений, она фиксирует связи между элементами данного набора объектов. Моделируемые отношения могут быть простыми, в то время как отношения индивидуумов конкретного множества объектов - достаточно сложными. Понятие "любит" очень просто, однако любовные отношения конкретных людей могут быть безнадежно запутаны.
Во время выполнения могут неизбежно возникать структуры, содержащие много объектов и имеющие запутанную структуру ссылок. Хорошая среда разработки должна предоставлять средства анализа объектных структур для тестирования и отладки.
Сложность динамических структур не должна влиять на статическую картину. Необходимо стараться сохранить набор классов и их отношения настолько простыми, насколько это возможно.
Тот факт, что простым моделям могут соответствовать сложные структуры данных, частично отражает мощь наших компьютеров. Короткий исходный текст может описывать огромные вычисления. Простая ОО-система может порождать в процессе выполнения миллионы объектов, связанных большим числом ссылок. Важнейшей целью программной инженерии является сохранение простоты ПО, даже когда экземпляры объектов такой простотой не обладают.
Рассмотренные приемы позволяют продвинуться в понимании возможностей ОО-подхода как средства моделирования. Важно, в частности, прояснить два аспекта: рассмотреть различные миры, связанные с разработкой ПО и отношения между ПО и внешней реальностью.
Из предшествующей дискуссии следует, что когда мы говорим об ОО-разработке, следует различать четыре отдельных мира:
Соотношения между этими мирами представлены на рис.8.9.
(рис 8.9) Формы и их экземплярыИ на программном и на внешнем уровне (нижняя и верхняя части рисунка) важно разграничить общие понятия и их конкретные реализации (классы и абстрактные отношения слева, объекты и отношения экземпляров справа). Данный момент уже обсуждался в дискуссии о сравнительной роли классов и объектов в предыдущей лекции. Применительно к отношениям необходимо отличать абстрактные отношения loved_one от множества связей loved_one, существующих между элементами конкретного множества объектов.
Это различие невыразимо ни в стандартных математических определениях понятия "отношение", ни в программистской терминологии, например в <x, y>, где x и y являются элементами заданных множеств TX и TY. В терминах программирования все x относятся к типу TX, а все y - к типу TY. Будучи пригодными для математиков, эти определения не подходят для
| Это обсуждение будет продолжено в лекции 11, когда будут рассматриваться преобразования между абстрактными и конкретными объектами, и будет дано имя вертикальным стрелкам предыдущего рисунка - функция абстракции. (См. "Функции абстракции", лекция 11) |
Предшествующее обсуждение не содержит ссылок на "реальный мир", - вместо этого используется термин "моделируемая система".
Такое разграничение проводится не всегда. Во многих дискуссиях используется выражение "моделирование реального мира"; аналогичные высказывания содержат и книги по ОО-анализу. Однако говорить о "реальности" применительно к программной системе ошибочно, по крайней мере, по четырем причинам.
Во-первых, реальность отражается в глазах очевидца. Не впадая в профессиональный шовинизм, программист всегда вправе спросить своих заказчиков, почему их системы более реальны, чем его. Возьмите программу, выполняющую математические вычисления - проверку гипотезы четырех красок в теории графов,
Во-вторых, понятие реального мира рушится в нередких ситуациях, когда ПО предназначено для разрешения проблем ПО. Рассмотрим компилятор C, написанный на Pascal. Для него "реальными" объектами являются программы на C. Насколько эти программы более реальны, чем сам компилятор? Это наблюдение применимо и к другим системам, работающим с объектами, существующими только в компьютере. (См. лекцию 6)
Третье соображение обобщает второе. В сегодняшнем информационном мире компьютеры стали частью реальности. На заре появления компьютеров можно было говорить, что создаваемая программная система моделирует реальную систему. Предприятие приобретало компьютеры для автоматизации бизнес процессов. При описании процессов современного банка его ПО является фундаментальной частью
Последний довод наиболее фундаментален. Программная система не является моделью реальности. В лучшем случае это модель модели некоторой части некоторой реальности. Система мониторинга пациента больницы не является моделью больницы, но реализацией конкретной точки зрения на некоторые аспекты работы больницы. Это модель модели некоторой части реальности больницы. Астрономическая программа это не модель вселенной, а всего лишь
Абстрактные типы данных, лежащие в основе ОО-метода, помогают понять, почему не следует придерживаться широко распространенной, но иллюзорной точкой зрения, что мы имеем дело с "реальным миром". Первый шаг к объектной ориентации, выражаемый теорией АТД, состоит в отказе от реальности ради менее грандиозного, но более аппетитного яства, - представляющего множество абстракций, характеризующих операции, доступные клиентам, и их формальные свойства. (На этом построен девиз модельера АТД - не говорите мне, кто вы, скажите, чем вы обладаете.) Мы никогда не претендуем на то, что рассмотрели все возможные операции и свойства: мы выбрали некоторые из них, подходящие для наших целей и отбросили остальные. Моделирование означает отсекание лишнего.
В идеальном случае программная система приходится соответствующей реальности лишь "седьмой водицей на киселе" (cousin twice removed).
Вернемся к более приземленным проблемам и рассмотрим, как программные системы работают с объектами, как создают и используют гибкие структуры данных.
Что не было показано при описании структуры объектов периода выполнения, так это в высшей степени динамичная природа настоящей ОО-модели. Статическая и ориентированная на стеки политика управления объектами характерна для языков уровня Fortran и Pascal соответственно. Противоположной является политика в настоящем ОО-окружении, позволяющая создавать объекты в период выполнения, когда в них возникает потребность. Какому образцу (типу) соответствуют создаваемые объекты, как правило, невозможно предсказать при статической проверке программного текста.
В начальном состоянии, как описано в предыдущей лекции, создается единственный корневой объект. Затем система повторно выполняет операции создания новых объектов, связывает изначально пустые ссылки с этими объектами, делает ранее присоединенные ссылки пустыми или присоединяет их к другим объектам. Динамическая и непредсказуемая природа этих операций обеспечивает гибкий подход и позволяет поддерживать
Следующий раздел посвящен механизмам, необходимым для создания объектов и манипулирования их полями, в частности, ссылками.
Рассмотрим создание экземпляра класса BOOK3. Это возможно только с помощью подпрограммы класса, являющегося клиентом BOOK3, как, например:
class QUOTATION feature
source: BOOK3
page: INTEGER
make_book is
-- Создание объекта BOOK3 и присоединение его к source.
do
... См. ниже ...
end
end
Этот класс описывает цитирование книги в других публикациях. Он содержит два поля: ссылку на цитируемую книгу и число страниц, содержащих ссылки на нее.
Механизм создания экземпляра QUOTATION (скоро он будет рассмотрен) предусматривает инициализацию всех его полей. Правило инициализации по умолчанию определяет, что любое ссылочное поле (в данном примере - поле, соответствующее атрибуту source ) после инициализации должно содержать пустую ссылку. Другими словами, создание объекта типа QUOTATION не сопровождается созданием объекта типа BOOK3.
Ссылка остается пустой, пока над ней не будут выполнены некоторые действия, - таково общее правило. Изменить значение ссылки можно, создав, например, новый объект. В процедуре make_book это делается следующим образом:
make_book is
-- Создание объекта BOOK3 и присоединение его к source.
do
create source
end
Это иллюстрация простейшей формы инструкции создания: create x , где x - атрибут охватывающего (
Сущность x, именованная в инструкции (в данном примере source ), называется целью (target) инструкции создания.
Данная форма известна как "базовая инструкция создания". Другая форма, включающая вызов процедуры класса, скоро появится. Вот точное определение действия базовой инструкции создания:
Результат базовой инструкции создания
Эффект инструкции создания вида create x, где тип цели x является ссылочным типом, основанном на классе C, состоит в выполнении трех следующих действий:
C (набора полей, по одному на каждый атрибут C ). Пусть x (ссылки) к На этапе C1 создается экземпляр C. На этапе C2 устанавливаются предопределенные значения всех полей, зависящие от типа соответствующего атрибута:
Значения по умолчанию при инициализации
Для ссылок значение по умолчанию - пустая ссылка.
Для полей BOOLEAN значение по умолчанию - False.
Для полей CHARACTER значение по умолчанию - символ null.
Для чисел (типов INTEGER, REAL или DOUBLE ) значение по умолчанию - ноль в соответствующем данному типу представлении.
Итак, для цели source типа BOOK3 в соответствии с объявлением класса
class BOOK3 feature title: STRING date, page_count: INTEGER author: WRITER end
результатом инструкции создания create source, выполняемой при вызове процедуры make_book класса QUOTATION, будет объект изображенный на рис.8.10.
(рис 8.10) Созданный и инициализированный объектПосле инициализации значения целочисленных полей равны нулю. Ссылочное поле author и поле title типа STRING, содержат пустые ссылки. Тип STRING, о котором ничего не говорится в правилах инициализации, двойственен, - фактически являясь ссылочным типом, он рассматривается во многих ситуациях как базовый тип. (О строках см. лекцию 13)
Важно проследить за последовательностью происходящих событий. Для рассмотренного выше экземпляра BOOK3 происходит следующее:
QUOTATION. Пусть Q_OBJ - этот экземпляр и имеется сущность a, значение которой ссылка, присоединенная к Q_OBJ.a.make_book приводит к выполнению процедуры make_book с Q_OBJ в качестве цели.Правомерен вопрос - как будет создан сам Q_OBJ (шаг B1)? Это, оставляя проблему, отодвигает ее вглубь. Но к этому моменту мы уже знаем ответ на этот вопрос: все возвращается к первопричине - Большому Взрыву. Для выполнения системы необходимо снабдить ее корневым классом и процедурой этого класса, названной процедурой создания. В начале выполнения автоматически создается один объект - корневой объект - экземпляр корневого класса. Корневой объект является единственным объектом, не создаваемым инструкциями программного текста; он приходит извне, как objectus ex machine ( объект от машины ). Начав с одного, провидением посланного объекта, далее уже программа может создавать объекты нормальным путем через подпрограммы, выполняющие инструкции создания. Первой выполняемой подпрограммой является процедура создания, автоматически применяемая к корневому объекту. Не всегда, но чаще всего она содержит по крайней мере одну инструкцию создания, что в предыдущей лекции называлось началом грандиозного фейерверка, процесса, создающего столько новых объектов, сколько нужно текущему выполнению.
Объекты создаются явным образом. Объявление сущности
b: BOOK3
не влечет за собой создание объекта во время выполнения, это происходит, когда некий элемент системы выполнит операцию
create b
Это может показаться удивительным. Разве объявления b недостаточно для создания объекта? Что хорошего в объявлении, если объект не создается?
Достаточно минуты размышления для понимания того, что разделение объявления и создания объекта является единственно разумным решением.
Первый аргумент - reductio ad absurdum (доведение до абсурда). Предположим, что начата обработка объявления и немедленно создается соответствующий объект. Но это экземпляр класса BOOK3, имеющий атрибут author ссылочного типа WRITER, значит поле author - ссылка, для которой опять нужно создавать объект. Этот объект вновь содержит ссылочные поля, требуется опять делать то же самое и начинается длинный путь рекурсивного создания объектов.
Этот аргумент еще более убедителен для таких классов как PERSON1, содержащих ссылки на себя:
class PERSON1 feature name: STRING loved_one, landlord: PERSON1 end
Появление каждого экземпляра PERSON1 повлечет за собой создание двух других таких объектов (соответствующих loved_one и landlord ) и начнется бесконечный цикл. Такие прямые или косвенные циклические ссылки не экзотика - они часто встречаются и необходимы.
Другой аргумент следует из обсуждения роли объектной технологии как мощного метода моделирования. Если для каждого ссылочного поля будет создаваться новый объект, то не было бы возможности выделить пустые ссылки и множественные ссылки на один и тот же объект. И то, и другое необходимо для реалистичного моделирования систем:
author для обозначения неизвестного автора.loved_one двух персон PERSON1, присоединенные к одному и тому же объекту. Не имело бы смысла создание своего объекта для каждого из этих полей. Все, что требуется, - это операция присваивания (рассмотрена далее в этой лекции) для присоединения ссылки к уже существующему объекту. В еще большей степени это соображение применимо для ссылки на себя (поле landlord верхнего объекта в том же примере).Механизм управления объектами никогда не присоединяет ссылку неявно. Он создает объекты через инструкции создания (или операции клонирования, тоже явные), инициализируя их ссылочные поля пустыми ссылками. Эти поля, в свою очередь, могут стать присоединенными к объектам, только в результате явных операций над этими полями.
В дискуссии о наследовании будет показано, что инструкция создания может использовать синтаксис create {T}x для создания объекта, чей тип T является наследником типа объявленного для x. (Полиморфное создание, см.лекцию 14) |
Все до сих пор рассмотренные инструкции создания основывались на инициализации по умолчанию. В некоторых случаях инициализация, определенная в языке, может нас не устраивать - хотелось бы обеспечить создаваемый объект специфической информацией. В этом предназначение процедур создания.
Для использования инициализации, отличной от предопределенной умолчанием, необходимо класс снабдить одной или несколькими процедурами создания. Такие процедуры должны быть перечислены в предложении, начинающимся ключевым словом creation в начале класса перед первым предложением feature. Схема такова:
indexing ... class C creation p1, p2, ... feature ... Объявления компонент, включая реализацию процедур p1, p2, ... end
Совет, отражающий стиль: в случае класса с единственной процедурой создания - для нее рекомендуется имя make. Для классов с двумя и более процедурами создания желателен префикс make_, за которым следует квалификатор, как в следующем примере POINT. (См. "Правильный выбор имен", лекция 8 курса "Основы объектно-ориентированного проектирования") |
Соответствующая инструкция создания в этих случаях имеет другую форму:
create x.p (...)
где p одна из процедур создания перечисленных в разделе creation, и в круглых скобках (...) перечисляются фактические аргументы p. Результатом является создание объекта с использованием значений по умолчанию, как и ранее, а затем вызов p с заданными аргументами. Такая инструкция является комбинацией инструкции создания и вызова процедуры и называется порождающим вызовом (creation call). (Оригинальная версия класса POINT приведена в лекции 7)
В качестве примера добавим две процедуры создания в класс POINT, что позволит клиентам при создании новой точки указывать ее начальные координаты - декартовы или полярные. Введем процедуры создания: make_cartesian и make_polar. Вот схема:
class POINT1 creation
make_cartesian, make_polar
feature
... Компоненты из предыдущей версии класса:
x, y, ro, theta, translate, scale, ...
feature {NONE} - Этот вариант экспорта рассмотрен ниже.
make_cartesian (a, b: REAL) is
-- Инициализация точки с декартовыми координатами a и b.
do
x := a; y := b
end
make_ polar (r, t: REAL) is
-- Инициализация точки с полярными координатами r и t.
do
x := r * cos (t); y := r * sin (t)
end
end
Для такого класса клиент будет создавать точки инструкциями вида:
create my_point.make_cartesian (0, 1) create my_point.make_polar (1, Pi/2)
В обоих случаях создается точка с одинаковыми координатами в предположении, что константа Pi имеет общепринятый смысл. Вот правило, определяющее эффект порождающего вызова. Первые три пункта правила такие же, как и для базисной формы, приведенной ранее:
Эффект порождающего вызова
Рассмотрим порождающий вызов в форме create x.p(...).
Пусть тип цели x это ссылочный тип, основанный на классе C, p(...) - процедура создания класса C, с заданным списком фактических аргументов. Эффект вызова состоит в выполнении следующих четырех шагов:
C (набора полей, по одному на каждый атрибут C ). Пусть x (ссылки) к p c заданными аргументами и с целевым объектом Для двух процедур создания, объявленных в классе POINT1, предложение feature имело вид feature {NONE}. Это означает, что эти процедуры закрыты для обычных вызовов, но остаются открытыми для порождающих вызовов. Только что представленные два примера порождающих вызовов являются корректными, но нормальные вызовы, например my_point.make_cartesian (0, 1) или my_point.make_polar (1, Pi/2) некорректны, так как процедуры недоступны клиентам со статусом обычных компонентов.
Решение о закрытости процедур означает, что мы не хотим после создания точки дать возможность клиентам прямого доступа к изменению их координат, хотя они могут делать это через другие процедуры класса, например такие, как translate и scale. Конечно, это лишь одна из возможных политик, вполне разумно экспортировать процедуры создания клиентам, придавая им дополнительно статус обычных процедур.
Для процедур создания можно установить выборочный статус порождающего вызова. Для этого достаточно в предложении creation перечислить классы, которым разрешается создавать объекты:
class C creation {A, B, ...}
p1, p2,
...
Этот прием применяется значительно реже, чем задание статуса экспорта этих процедур как обычных компонентов класса в предложении feature. Важно помнить, что статус экспорта порождающего вызова и статус экспорта обычного вызова не зависят друг от друга, они устанавливаются независимо в разных предложениях.
Две формы инструкций создания: create x и create x.p (...), являются взаимно исключающими. Если в классе задано предложение creation, то допускается только порождающие вызовы, базовая форма создания считается в этом случае недопустимой и отвергается компилятором.
Это соглашение кажется на первый взгляд странным, но смысл его становится понятным при рассмотрении требований согласованности объекта. Объект - это не просто набор полей, это реализация АТД, так что поля его будут согласованы только если они удовлетворяют ограничениям, заданным спецификацией АТД. Вот типичный пример. Предположим, что объект задает некоторую личность с двумя полями - год рождения и возраст. Понятно, что согласованность этого объекта не допускает независимых значений этих полей, они связаны вполне определенным соотношением, которое может быть частью спецификации. Инструкция создания обязана всегда производить на свет согласованный объект. Базовая форма этой инструкции применима только в тех частных и довольно редких случаях, когда стратегия умолчания удовлетворяет требованиям согласованности. Во всех остальных случаях в классе требуется определять процедуры создания, что автоматически запрещает использование базовой инструкции создания.
В тех редких случаях, когда инициализация по умолчанию допустима, поскольку удовлетворяет инварианту класса, может появиться желание включить ее в состав процедур создания. Для этого необходимо в список процедур создания включить специальную процедуру, наследуемую от класса ANY, с именем nothing. Как следует из ее имени, эта процедура без аргументов ничего не делает, имея пустое тело. Вот пример подобного включения:
class C creation nothing, some_creation_procedure, some_other_creation_procedure... feature ...
Хотя по-прежнему базовая инструкция создания является некорректной в этом случае, но теперь клиент имеет возможность создать объект порождающим вызовом create x.nothing
В заключение обратите внимание на специальное правило - теперь появилась возможность определить класс, клиенты которого не смогут создавать экземпляры класса. Вот пример того, как этого можно добиться:
class C creation -- Здесь ничего не указано! feature ... Текст класса, как обычно ... end
Класс имеет предложение creation, но пустое. Это означает согласно установленным правилам, что создавать объекты можно только с помощью процедур создания, которых нет, что означает невозможность создания объектов.
Если ограничиться ОО-механизмом, рассмотренным до сих пор, то такая возможность запрета на создание объектов класса кажется надуманной. Знакомство с наследованием придает ей смысл, - иногда желательно использовать класс только в интересах наследования. Эта цель и может быть достигнута таким способом. Заметьте, этого же можно добиться, сделав класс абстрактным (отложенным). Но в этом случае у класса должен быть, по крайней мере, один отложенный метод. Иногда разумно полностью определить методы, но не включить в класс процедуры создания.
В продолжение обсуждения полезно сравнить применяемый подход с несколькими процедурами создания с подходом, используемым в языках C++/Java. В этих языках применяется техника, основанная на перегрузке. Суть ее такова: все процедуры создания, называемые конструкторами, перегружены - они имеют одно и то же имя, совпадающее с именем класса. Конструкторы должны иметь различную сигнатуру (отличаться числом и/или типами аргументов).
Как мы видели при обсуждении перегрузки, сигнатура не является подходящим критерием распознавания. Например: конструкторы make_cartesian и make_polar имеют одинаковую сигнатуру, так что придется вводить искусственный аргумент в один из конструкторов, чтобы стало возможным отличить вызов нужного конструктора.
Наша техника кажется предпочтительнее во всех отношениях. Минимум усилий (никаких процедур создания), если инициализация по умолчанию применима; при желании позволяет предотвратить создание объектов клиентами; вводит столько процедур создания, сколько необходимо, не создавая никаких коллизий; каждая процедура создания имеет собственное имя, облегчая понимание ее предназначения, например make_polar.
Модель периода выполнения определяет важную роль ссылок. Рассмотрим некоторые их свойства, в частности, понятие пустой ( void ) ссылки и связанные с ней проблемы.
Ссылка может находиться в одном из двух состояний - она может быть пустой или присоединенной. Мы уже видели, что изначально ссылка всегда находится в состоянии void и может стать присоединенной благодаря созданию объекта. Вот как выглядит более полная картина, показывающая все возможности перехода между состояниями:
(рис 8.11) Возможные состояния ссылки и переходыПомимо создания, ссылка может изменять состояние в результате присваивания. Проверьте себя, понимаете ли вы разницу между тремя понятиями - объектом, ссылкой и сущностью:
void, либо она присоединена к объекту. Точное определение "присоединения" уже появлялось. Присоединенная ссылка однозначно идентифицирует объект.Если b - сущность ссылочного типа, то ее значением в период выполнения является ссылка, которая может быть присоединена к объекту O. В этом случае говорим, что сущность b присоединена к O.
В большинстве случаев мы ожидаем, что ссылка присоединена к объекту, хотя допустимо иметь и пустые ссылки. Ссылки void играют важную роль в ОО-модели вычислений. В предыдущей лекции подробно разбиралась фундаментальная операция ОО-модели - вызов компонента - применение к экземпляру класса компонента этого класса. Вот как это пишется:
some_entity.some_feature (arg1, ...)
Для корректного выполнения вызова сущность some_entity должна быть присоединена к нужному целевому объекту. Если случится, что some_entity ссылочного типа и имеет значение void, то вызов не может быть обработан, так как необходим целевой объект.
ОО-система никогда не должна в момент выполнения вызывать компонент с целевым объектом void. Результатом подобного вызова будет исключение (exception).(Исключения и их обработка будут изучаться в лекции 12)
Было бы прекрасно, если бы компилятор проверял программный текст и гарантировал, что подобные события не встретятся в период выполнения, точно также как он проверяет отсутствие несовместимости типов, используя соответствующие правила типизации. К сожалению, такая цель недостижима для компиляторов, если только не накладывать жестких ограничений на язык. Так что ответственность за то, чтобы все вызовы имели присоединенный целевой объект, возлагается на разработчика. Конечно, есть простой способ - окружать все вызовы тестом:
if "x не void" then x.f (...) else ... end
Этот прием не применим в качестве универсального требования, хотя и может использоваться, когда из контекста не следует, что целевой объект не будет пуст.
Вопрос о не пустоте ссылок является частью вопроса корректности ПО. Для проверки корректности системы необходимо проверить, что нет вызовов, применимых к void ссылкам, и что все утверждения (изучаемые в последующих лекциях) удовлетворяются в соответствующий момент выполнения. Проверка не пустоты, также как и проверка утверждений, могла бы проводиться специальным автоматом - верификатором, встроенным в компилятор или являющимся независимым средством. В отсутствие такого механизма подобные нарушения приведут к ошибке периода выполнения - исключению. Разработчики могут защитить ПО двумя путями:
Мы уже знакомы с одним из способов изменения значения ссылки x: использование инструкции создания в форме create x, позволяющей создать новый объект и присоединить его к ссылке. Имеются и другие интересные операции, доступные при работе со ссылками.
Классы, появляющиеся в этой лекции, не имели подпрограмм - у них были только атрибуты. Как отмечалось, такие классы почти бесполезны, так как у них нет способа изменить значение атрибутов. Необходимы способы модификации ссылок, не использующие при этом инструкций в духе языков Pascal-C-Java-C++, подобных присваиванию: my_beloved.loved_one := me (напрямую изменяющих у объекта поле loved_one ), что нарушает принцип скрытия информации и синтаксически некорректно в нашей нотации.
Для модификации полей объекта клиент обязан вызвать подпрограмму, специально поставляемую разработчиком класса для этих целей. Давайте включим в класс PERSON1 процедуру, позволяющую модифицировать поле loved_one. Вот результат:
class PERSON2 feature
name: STRING
loved_one, landlord: PERSON2
set_loved (l: PERSON2) is
-- Присоединить поле loved_one текущего объекта к объекту l.
do
loved_one := l
end
end
Процедура set_loved присваивает ссылочному полю loved_one текущего экземпляра PERSON2 значение другой ссылки l. Ссылочное присваивание (левая и правая части являются ссылками) присваивает значение источника (правой части) целевой ссылке (слева).
Эффект ссылочного присваивания очевиден: целевая ссылка становится присоединенной к объекту, к которому присоединен источник - или становится void, если такое значение имеет источник. Предположим, например, что мы начинаем с ситуации, изображенной на рис.8.12 , где поля landlord и loved_one всех изображенных объектов пока пусты.
(рис 8.12) Перед присваиванием ссылкеПредположим, что выполняется вызов процедуры:
a.set_loved (r)
Сущность a присоединена к объекту O1, а сущность r - к O3. В результате выполнения процедуры set_loved выполнится присваивание:
loved_one := l
Здесь в роли текущего объекта выступает объект O1, сущности l и r имеют одинаковое значение - ссылки на объект O3. В результате изменится значение поля loved_one объекта O1 - ссылка присоединится к другому объекту O3, как показано на следующем рисунке:
Если бы r было пустой ссылкой, то такой же в результате присваивания стала бы и ссылка в поле loved_one объекта O1.
(рис 8.13) После присваивания ссылки
Наряду с присваиванием возникает необходимость и в тесте - проверить, присоединены ли две ссылки к одному и тому же объекту. Для этого есть оператор эквивалентности =.
Если x и y - сущности ссылочного типа, то выражение:
x = y
истинно тогда и только тогда, когда обе ссылки пусты или присоединены к одному и тому же объекту. Противоположный оператор "не эквивалентно" записывается как /=.
Выражение:
r = a.loved_one
истинно в ситуации, представленной на рис.8.13 и ложно для ситуации рис.8.12 .
Заметьте, в операциях эквивалентности сравниваются ссылки, а не объекты, к которым они присоединены. Так что если две ссылки присоединены к разным объектам, результатом операции эквивалентности будет false, даже если объекты имеют все поля с одинаковыми значениями. Операции, сравнивающие объекты, а не ссылки, будут введены позднее.
Получить пустую ссылку достаточно просто - при инициализации по умолчанию все ссылки пусты. Однако удобно иметь специальное имя для ссылки, доступной в любом контексте и имеющей значение void. Предопределенный компонент
Void
играет эту роль.
Обычно компонент Void используется в тестах, проверяющих пустоту ссылок:
if x = Void then ...
и для того, чтобы присвоить некоторой ссылке это значение:
x := Void
В результате последнего присваивания происходит отсоединение ссылки от объекта, и она становится пустой. Эта ситуация показана на следующем рисунке:
(рис 8.14) Отсоединение ссылки от объектаПрисваивание Void ссылке не оказывает никакого влияния на объект, ранее присоединенный к ссылке, - разрывается только связь между ссылкой и объектом. Было бы некорректно рассматривать эту операцию как освобождение памяти, так как другие ссылки могут продолжать быть связанными с объектом (на рисунке x может быть отсоединено от объекта O1, но другие ссылки могут быть еще присоединены к нему). Об управлении памятью смотри следующую лекцию. |
Ссылочное присваивание приводит к тому, что две или несколько ссылок присоединяются к одному объекту. Иногда необходима другая форма присваивания, в результате которой мы хотим получить не копию ссылки, а копию объекта. Эта цель достигается при вызове функции клонирования clone.
Если y присоединено к объекту OY, выражение
clone (y)
означает создание нового объекта OX, такого, что он имеет те же поля, что и OY, и все соответствующие поля имеют идентичные значения. Если y равно void, то значение clone (y) также void.
Скопировать присоединенный к y объект и связать копию со ссылкой x позволяет присваивание:
[1] x := clone (y)
Вот иллюстрация этого механизма:
(рис 8.15) Клонирование объекта Наряду со сравнением ссылок необходим механизм, позволяющий сравнивать объекты. Этой цели служит функция equal. Вызов:
equal (x, y)
возвращает значение true, если и только если x и y оба имеют значение void или присоединены к двум объектам с идентичными полями. После выполнения присваивания с клонированием [1], состояние, непосредственно следующее за присваиванием, удовлетворяет equal (x, y).
Возможно, вы удивляетесь, почему у функции clone есть аргумент, а у функции equal - их два. Для ОО-стиля характерен квалифицируемый вызов в форме: y. и x.is_equal (y). Ответ появится в разделе обсуждения, но это будет еще не скоро, так что попытайтесь догадаться сами. |
Функция clone создает новую копию существующего объекта. Иногда целевой объект уже существует, и все, что необходимо, это скопировать значения полей. Процедура copy выполняет эту работу. Она вызывается обычным образом:
x.copy (y)
Сущности x и y должны быть одного и того же типа; эффект от выполнения - копирование полей объекта, присоединенного к y, в соответствующие поля объекта, присоединенного к x.
Как и во всех вызовах компонента, вызов copy требует, чтобы целевой объект x был не пуст. Дополнительно требуется, чтобы и y был не пуст. Эта неспособность иметь дело с пустыми ссылками отличает copy от clone.
Требование не пустоты y настолько важно, что должен существовать способ для его формального выражения. Фактически речь идет о более общей проблеме: как программа может задать предусловия на аргументы, передаваемые клиентом при ее вызове. Такие предусловия, являясь частным случаем общего понятия "утверждение" в деталях будут обсуждаться в последующих лекциях. Аналогично, нам хотелось бы уметь выражать в виде постусловия семантическое свойство, отмеченное выше, - результат выполнения clone удовлетворяет equal. |
Процедура copy может считаться более фундаментальной, чем функция clone в том смысле, что, по меньшей мере, для класса без процедуры создания можно выразить clone в терминах copy следующим образом:
clone (y: SOME_TYPE) is
-- Void если y равно void; иначе дублировать присоединенный к y объект
do
if y /= Void then
create Result --Правильно только в отсутствие процедур создания
Result.copy (y)
end
end
При вызове функции сущность Result автоматически инициализируется в соответствии с общими правилами для атрибутов. Вот почему нет необходимости в ветви else условного оператора: Result инициализируется значением Void, так что результатом функции будет void, если значение y это void.
Формы копирования и сравнения, реализуемые подпрограммами clone, equal и copy, называются поверхностными, поскольку они работают с объектами только на первом уровне, никогда не пытаясь следовать вглубь по ссылкам. Возникает необходимость для глубоких вариантов этих операций, рекурсивно дублирующих полную структуру.
Для понимания разницы рассмотрим пример, показанный на рис.8.16. Предположим, что мы начинаем в начальном состоянии A, где сущность a присоединена к объекту O1.
(рис 8.16) Различные формы присваивания и клонированияРассмотрим простое присваивание ссылки:
b := a
В состоянии B, показанном на рисунке, цель b в результате присваивания присоединена к объекту O1, к которому присоединен источник a. Никаких новых объектов не создается.
Далее рассмотрим операцию клонирования:
c := clone (a)
Эта инструкция, как показывает раздел C нашего рисунка, создает новый объект O4, с полями, идентичными полям объекта O1. Будут скопированы два ссылочных поля, и значения ссылок будут указывать на те же объекты O1 и O3, как и поля оригинального объекта O1. Но, заметьте, не происходит дублирования самого объекта O3, и никакого другого объекта помимо дублирования O1. По этой причине базисная операция clone называется поверхностным клонированием, - она останавливается на первом уровне объектной структуры.
Заметьте, при клонировании исчезли ссылки на себя. Ссылка landlord объекта O1 была присоединена к самому объекту O1. У объекта O4 это поле становится ссылкой на оригинал O1. |
В некоторых ситуациях вы, возможно, захотите пойти дальше и дублировать структуру рекурсивно без введения разделяемых ссылок. Функция глубокого клонирования deep_clone позволяет достичь цели. Процесс создания deep_clone (y) рекурсивно следует за всеми ссылочными полями, содержащимися в объекте, дублируя полную структуру. (Если y это void, то и результат будет также void.) Эта функция будет, конечно же, правильно обрабатывать циклические ссылочные структуры.
Нижняя часть на рисунке - раздел D - иллюстрирует выполнение этой операции:
d := deep_clone (a)
В этом случае не появляются новые разделяемые ссылки. Все объекты, прямо или косвенно доступные объекту O1, будут дублированы, создавая новые объекты O5, O6 и O7. Нет никаких связей между старыми объектами ( O1, O2 и O3 ) и новыми. Объект O5, дублирующий O1, имеет собственные ссылки на себя.
Так же, как необходимы операции глубокого и deep_equal сравнивает две объектные структуры, определяя их структурную идентичность. В примере, показанном на рисунке, deep_equal выполнимо для любой пары из a, b и d. В то же время equal (a, c) истинно, поскольку поля объектов O1 и O4 идентичны, equal (a, d) - ложно. Фактически equal не выполнимо ни для одной пары из d и любого элемента оставшейся тройки. В целом имеют место следующие свойства:
x := clone (y) или вызова x.copy (y), выражение equal (x, y) имеет значение true (в случае присваивания это свойство имеет место независимо от того, имеет ли y значение void ).x := deep_clone (y), выражение deep_equal (x, y) имеет значение true.Эти свойства будут отражены в постусловиях соответствующих подпрограмм.
Изучение глубокого копирования и эквивалентности приводит к механизму, обеспечивающему серьезные практические преимущества ОО-метода, естественно, при условии его доступности в среде разработки.
До сих пор обсуждение не затрагивало вопросов ввода и вывода. Но, конечно, ОО-системе необходимо общаться с внешним миром и другими системами. Такое общение предполагает возможность чтения и записи объектов в различные хранилища - файлы,
| Для простоты в этом разделе будем предполагать, что проблема сводится к чтению и записи файлов. Для этих операций будем использовать термины "возвратить" (retrieval) и "сохранить" (storage), адекватные терминам ввод и вывод (input, output). Изучаемые механизмы должны быть применимыми при использовании других средств коммуникации, например при посылке и получении объектов по сети. |
Для экземпляров таких классов, как POINT или BOOK1 сохранение и возвращение объектов не является какой-либо новинкой. Эти классы, используемые в качестве первых примеров этой лекции, имеют атрибуты таких типов, как INTEGER, REAL и STRING, для которых доступно хорошо понятное внешнее представление. Сохранение или возвращение экземпляра такого класса из файла подобно выполнению операций ввода-вывода записей в языке Паскаль или структур языка С. Для этих хорошо известных технических проблем существуют read и write, которые подобно clone и copy будут доступны для всех классов.
Но такие механизмы не могут нас полностью устраивать, поскольку они не управляют главным элементом объектной структуры - ссылками. Так как ссылки могут быть представлены адресами памяти или чем-то подобным, то и для них можно найти подходящее внешнее представление. Это не самая трудная часть проблемы. Сложнее обстоит дело с передачей смысла самих ссылок. Ссылки присоединены к объектам и бесполезны в отсутствие этих объектов. Так что, как только мы начинаем иметь дело с нетривиальными объектами - объектами, содержащими ссылки, нас перестают устраивать старые механизмы сохранения и возвращения, работающие только со значениями. Механизмы должны вместе с объектом обрабатывать и всех его связников (dependents) в соответствии со следующим определением:
Определение: связники, прямые связники
Прямыми связниками объекта являются объекты, присоединенные к его ссылочным полям, если таковые имеются.
Связниками объекта являются сам объект и (рекурсивно) связники его прямых связников.
Для структуры объектов, показанной на рис.8.17, было бы бессмысленно сохранить в файле или передать по сети только объект O1. Операция должна включать связников O1 - объекты O2 и O3.
(рис 8.17) Три взаимно зависимых объектаВ этом примере любой из трех объектов рассматривает оставшиеся два как своих связников. В примере, показанном на рис.8.18, объект W1 можно сохранить независимо, но сохранение объектов B1 или B2 требует сохранения также и W1.
(рис 8.18) Объекты "Book" и "Writer"Понятие связников неявно присутствует в представлении deep_equal. Вот общее правило:
Принцип Замыкания Сохраняемости (Persistence Closure principle)
Всякий раз, когда механизм сохранения сохраняет объект, он должен сохранять и связников этого объекта. Всякий раз, когда механизм возвращения возвращает объект, он должен возвращать и связников этого объекта, если они еще не были возвращены.
Базисным механизмом, реализующим эти цели, является библиотечный класс STORABLE, включенный в библиотеку Base. Основными компонентами класса STORABLE являются:
store (f: IO_MEDIUM) retrieved (f: IO_MEDIUM): STORABLE
Вызов x.store (f) сохраняет в файле, связанном с f, объект, присоединенный к x, вместе со всеми его связниками. Объект, присоединенный к x, называют головным объектом хранимой структуры. Порождающий класс для x должен быть потомком STORABLE. Это требуется только для класса головного объекта и не распространяется на связников.
Класс IO_MEDIUM это еще один класс библиотеки Base, предназначенный для работы не только с файлами, но и для передачи данных по сети. Очевидно, f не должно быть void, а присоединенный файл или сетевое устройство должны допускать запись.
Вызов возвращает структуру объектов, идентичную структуре, сохраняемой в f предыдущим вызовом store. Компонент - это функция, возвращающая в качестве результата ссылку на головной объект возвращаемой структуры объектов.
Механизм STORABLE это наш первый пример важного свойства сохраняемости (persistence) объектов. Объект сохраняем, если он продолжает существовать по окончании очередной сессии работы системы. Класс STORABLE обеспечивает только частичное решение проблемы, накладывая ряд ограничений:
retrieved воссоздает полную структуру объектов. Это означает невозможность использовать два или более таких вызовов для получения отдельных частей структуры, если только структуры не являются независимыми.В развитие этой темы следует перейти от понятия механизма сохранения к общему понятию ОО-базы данных, подробно рассматриваемому в лекции 13 курса "Основы объектно-ориентированного проектирования". В ней обсуждаются проблемы механизмов сохранения STORABLE и другие проблемы, такие как эволюция схем и идентичность объектов сохранения.
Отмеченные выше ограничения механизма STORABLE нисколько не умаляют его практическую ценность. Хорошо известно, что отсутствие подобного механизма - одно из главных препятствий на пути широкого использования сложных структур данных в традиционном окружении. Без него хранение данных требует значительных программистских усилий: для каждого вида структуры приходится писать несколько взаимосвязанных рекурсивных процедур, реализующих операции ввода-вывода, также как и специальные механизмы обхода
Предопределенный механизм STORABLE позволяет решить все эти проблемы независимо от того, какова структура объектов, ее сложность, учитывая при этом эволюцию ПО.
Типичным приложением механизма STORABLE является свойство SAVE. Рассмотрим интерактивную систему, например текстовый редактор, графический редактор или систему компьютерного проектирования. Во всех случаях пользователю необходимо предоставить команду SAVE, сохраняющую состояние текущей сессии в файле. Хранимая информация должна быть достаточной для продолжения работы, так что она должна включать все важные структуры данных системы. Механизм STORABLE и хороший выбор головного объекта позволяет реализовать свойство SAVE одной командой:
head.store (save_file)
Уже одной этой причины достаточно для рекомендации выбора ОО-окружения в сравнении с другими более традиционными средами разработки.
Обсуждение структуры объектов времени выполнения показало важную роль ссылок. Для завершения картины необходимо выяснить, как работать со значениями, представляющими собой не ссылки на объекты, а непосредственно сами объекты.
До сих пор все значения целочисленных, булевых и других аналогичных типов рассматривались как ссылки на объекты. Однако по двум причинам необходимы сущности, значениями которых являются объекты:
BOOLEAN, INTEGER и др.) обрабатываются аналогично типам, введенным разработчиком (POINT, BOOK и др.). Тем не менее, если используется сущность n типа INTEGER, то в большинстве случаев удобнее полагать, что значение n - целое число, а не ссылка на объект содержащий целое число. Это удобнее отчасти по соображениям эффективности. Понятно, что для размещения целочисленных объектов необходимо больше памяти, а на обработку косвенного доступа к ним - дополнительное время. Кроме того, концептуально целое число и ссылка на целое число - совершенно различные понятия. Этот довод важен, если нашей целью является построение точной модели.O1 подобъекта O2, а не ссылки на внешний объект O2. Причиной такого подхода могут быть повышение эффективности, точное моделирование или и то, и другое.Удовлетворить потребность в составных объектах очень просто. Пусть C - класс, определенный так, как это делалось до сих пор
class C feature ... end
Класс C может использоваться в качестве типа. Любая сущность типа C является ссылкой. По этой причине C называется ссылочным типом (reference type).
Теперь предположим, что нам необходима сущность x, значение которой во время выполнения будет экземпляром C, а не ссылкой на такой экземпляр. Это достигается следующим объявлением x:
x : expanded C
Эта нотация использует новое ключевое слово expanded (развернутый). Нотация expanded C означает, что экземпляры этого типа в точности соответствуют экземплярам C. Единственное отличие от обычного объявления типа состоит в том, что сущности типа C обозначают ссылки, которые могут быть присоединены к экземплярам C, а сущности типа expanded C обозначают непосредственно экземпляры C.
Таким образом, к структуре, определенной в предыдущих разделах, добавлено понятие составного объекта (composite object). Объект O называется составным, если одно или более его полей являются объектами - подобъектами (subobjects) O . Следующий класс является примером описания
class COMPOSITE feature ref: C sub: expanded C end
Класс COMPOSITE имеет два атрибута: ref, обозначающий ссылку, и sub, обозначающий подобъект. Вот как выглядит прямой экземпляр COMPOSITE.
(рис 8.19) Составной объект с одним подобъектомПоле ref является ссылкой, присоединенной к экземпляру C (возможно, пустой ссылкой). Поле sub содержит экземпляр C и не может быть пустым.
Удобно несколько расширить нотацию. Иногда при проектировании класса, например E, хотелось бы установить, что все экземпляры класса должны быть развернутыми. Чтобы сделать это требование явным, следует объявить класс в следующей форме:
expanded class E feature ... Далее все аналогично любому другому классу ... end
Так определенный класс называется развернутым классом. Такое объявление класса никак не отражается на экземплярах класса, они остаются такими же, как если бы класс был объявлен просто class E. Но сущности типа E изменяются - теперь их значения не ссылки, а сами объекты. Как следствие этой новой возможности понятие
Определение: развернутый тип
Тип является развернутым в двух случаях:
Он задан в форме: expanded C
Он задан в форме E, где E - развернутый класс.
Объявление вида
x: expanded E
где E - развернутый класс, не будет ошибкой, поскольку эквивалентно
x: E
Таким образом, имеется два вида типов. Тип, не являющийся развернутым, является ссылочным типом. Эту терминологию можно использовать и для сущностей - ссылочные сущности и развернутые сущности. Аналогично и классы могут быть ссылочными и развернутыми.
Почему нам нужны
Первое применение наиболее очевидно: без
Аргумент производительности, однако, не является ключевым. ОО-конструирование ПО зачастую рассматривается как моделирование. Для отражения реальности необходимо моделировать объект как составной, а не как объект со ссылками. Это концептуальная проблема, а не проблема реализации.
Рассмотрим два объявления атрибутов:
D1. ref: S D2. exp: expanded S
Объявления появляются в классе C, предполагается также, что S это ссылочный класс. Объявление D1 отражает тот факт, что каждый экземпляр класса C "знает о" существовании некоторого экземпляра S (если только ref не является void ). Объявление D2 более требовательное: оно устанавливает тот факт, что каждый экземпляр класса C "содержит" экземпляр S. Даже если не думать о проблемах реализации, следует понимать, что речь идет о двух разных отношениях.
Отношение "содержит", поддерживаемое развернутыми типами, не допускает никакого разделения встроенного объекта, в то время как отношение "знает о" допускает несколько ссылок, присоединенных к объекту.
Вот пример объявления класса:
class WORKSTATION feature k: expanded KEYBOARD c: expanded CPU m: expanded MONITOR n: NETWORK ... end
Рабочая станция имеет клавиатуру, ЦПУ, монитор и подключена к сети. Клавиатура, ЦПУ и монитор являются частью данного компьютера и не могут разделяться двумя или несколькими рабочими станциями. Однако несколько рабочих станций подключены к одной и той же сети. Эти особенности проявляются в определении класса, использующем
(рис 8.20) Отношения между объектами: "знает о" и "содержит"Итак, концепция
Третье важное приложение REAL использовался, чтобы показать, как с помощью инфиксных и префиксных компонентов можно промоделировать понятие вещественного числа как класса. То же самое нетрудно проделать и для других BOOLEAN, CHARACTER, INTEGER, DOUBLE. Но проблема все же остается. Если классы рассматривать как ссылочные, то сущности
r: REAL
будут в период выполнения ссылками на возможные объекты, содержащие значение (в данном случае REAL ). Это неприемлемо: чтобы соответствовать общей практике программирования значение должно быть не ссылкой, а самим вещественным числом. Решение проблемы немедленно следует из обсуждения - класс REAL следует объявить как развернутый. Его объявление должно быть таким:
expanded class REAL feature ... Объявления компонент такие же как и ранее ... end
Все другие
В некоторых областях информатики - базах данных, моделировании, анализе требований - разработана классификация отношений, имеющих место между элементами моделируемой системы. В этих контекстах часто встречается отношение "агрегирования" (aggregation), выражающее тот факт, что каждый объект некоторого типа является агрегатом - содержит в своем составе ноль или более объектов, каждый из которых имеет свой собственный тип. Например: автомобиль является агрегатом, содержащим мотор, кузов и другие детали.
CAR с компонентами ENGINE и expanded BODY. Другой способ основан на том, что агрегирование представляется отношением "развернутый клиент". Говорят, что класс C является развернутым клиентом класса S, если он содержит объявление компонента типа expanded S (или просто S, если S развернут). Одно из преимуществ такого модельного подхода в том, что развернутый клиент - это частный случай общего отношения "быть клиентом", так что можно использовать общие рамки и нотацию, комбинируя зависимости, подобные агрегированию с зависимостями, допускающими разделение. Примером могут служить с одной стороны - отношение между и KEYBOARD, с другой - отношение между и NETWORK.
Используя ОО-подход, можно избежать множественности отношений, используемых в литературе по информационному моделированию, - все покрывается двумя отношениями: клиент (развернутый или нет) и наследование.
Рассмотрим E (в любой форме) и развернутую сущность x типа E.
Так как значение x это всегда объект, то он не может быть void. Так что булево выражение:
x = Void
будет всегда вырабатывать значение false, и вызов в форме x.some_ feature (arg1, ...) никогда не приведет к возбуждению исключения из-за void цели, что могло случиться для ссылочной сущности.
Пусть объект O является значением x. Как и в случае не пустой ссылки, говорят, что x присоединено к O. Итак, для любой сущности, значение которой не void, можем говорить о присоединенном объекте, независимо от типа - ссылочного или развернутого - сущности.
Что можно сказать о создании развернутых объектов? Инструкцию:
create x
можно применить к развернутому x. Для ссылки x эффект достигался за три шага: (C1) создание нового объекта; (C2) инициализация его полей значениями по умолчанию; (C3) присоединение к x. Для развернутого x, шаг C1 неуместен, а шаг C3 бесполезен; так что единственный эффект состоит в инициализации полей значениями по умолчанию.
В общем случае, в случае присутствия
class F feature u: BOOLEAN v: INTEGER w: REAL x: C y: expanded C z: E ... end
Класс E развернут, а класс C нет. Инициализация прямого экземпляра F включает установку поля u в false, v - в 0, w - в 0.0, x - ссылкой void, а экземпляры y и z станут экземплярами классов C и E соответственно, чьи поля будут инициализированы в соответствии со стандартными правилами. Этот процесс инициализации может быть рекурсивно продолжен, поскольку поля экземпляров C и E могут быть в свою очередь развернутыми.
Как можно было понять, использование C иметь атрибут типа expanded D, если класс D имеет атрибут типа expanded C. Это означало бы, что каждый объект C включал бы подобъект D, который бы включал подобъект C и так далее. Сформулируем правило "развернутого клиента", ранее введенное неформально:
Правило Развернутого Клиента
Пусть отношение "развернутый клиент" определяется следующим образом: класс C является развернутым клиентом класса S, если некоторый атрибут C является развернутым типом, основанным на классе S.
Тогда отношение развернутый клиент не может включать никаких циклов.
Другими словами, не может существовать множества классов A, B, C, ... N, где каждый последующий является развернутым клиентом предыдущего, а последний класс N является развернутым клиентом класса A. В частности, класс A не может иметь атрибут типа expanded A, так как это делало бы класс A своим развернутым клиентом.
Заключительное замечание ответит на вопрос, как сочетаются ссылки и подобъекты. Развернутый класс или
(рис 8.21) Подобъект со ссылкой на другой объектПриведенная ситуация предполагает следующие объявления:
Class COMPOSITE1 feature other: SOME_TYPE sub: expanded C end class C feature ref: D x: OTHER_TYPE; y: YET_ANOTHER_TYPE end class D feature ... end
Каждый экземпляр класса COMPOSITE, такой как O_COMP на рис.8.21, имеет подобъект, ( на рисунке) содержащий ссылку ref, которая может быть присоединена к объекту ( OD на рисунке).
Но противоположная ситуация, где ссылка становится присоединенной к объекту, невозможна. Это будет следовать из правил присваивания и передаче аргументов, изучаемых в следующем разделе. Итак, структура времени выполнения никогда не может находиться в ситуации, показанной на рис.8.22, где OE содержит ссылку на , - подобъект O_CMP1, и содержит ссылку на себя.
(рис 8.22) Ссылка на подобъектЭто правило открыто для критики, поскольку оно ограничивает моделирующие возможности подхода. Предыдущая версия нотации книги допускала ссылки на подобъекты. Но эта возможность порождала больше проблем, чем она того стоит:
В этом разделе рассматривается специальная информация, и он может быть пропущен при первом чтении.
Введение
Семантика присваивания, как отмечалось, распространяется еще на одну операцию - передачу аргумента при вызове подпрограмм. Предположим, существует подпрограмма (процедура или функция) в форме:
r (..., x: SOME_TYPE, ...)
Здесь сущность x это один из формальных аргументов r. Рассмотрим теперь некоторый вызов r в любой из двух возможных форм - квалифицированный или неквалифицированный вызов:
r (..., y, ...) t.r (..., y, ...)
Выражение y является фактическим аргументом, передаваемым формальному аргументу x.
Выполнение r при любом из этих вызовов начинается с инициализации формальных аргументов значениями соответствующих фактических аргументов. Для простоты и согласованности правила, определяющие передачу аргументов, те же, что и правила присваивания. Другими словами, инициализация формального аргумента эквивалентна выполнению присваивания:
x := y
Это правило приводит к определению:
Определение: Присоединение
Присоединение y к x является результатом выполнения следующих двух операций:
Присваивания в форме x := y
Инициализации x при вызове подпрограммы, где x - формальный аргумент, а y - фактический аргумент вызова.
В обоих случаях x является целью присоединения, а y - источником.
Одни и те же правила действуют в обоих случаях для определения корректности присоединения (в зависимости от типов цели и источника). При условии корректности одни и те же правила определяют, каков будет эффект присоединения в период выполнения.
При изучении ссылочного присваивания мы уже познакомились с эффектом присоединения. Если источник и цель являются ссылками, то эффект присваивания:
x := y
и соответствующей передачи аргументов состоит в том, что x получает значение ссылки y. Это иллюстрировалось несколькими примерами. Если значением y является void, то операция вместо присоединения сделает и x равным void ; если y присоединен к объекту, то и x будет присоединен к этому же объекту.
Что происходит, когда типы x и y развернуты? Ссылочное присваивание не имеет смысла, а вот поверхностная форма копирования вполне возможна. Так и происходит. Рассмотрим объявления:
x, y: expanded SOME_CLASS
Присваивание x := y будет копировать каждое поле объекта, присоединенного к y, в соответствующие поля объекта, присоединенного к x, создавая тот же эффект, что и выполнение:
x.copy (y)
Копирование также является легальной операцией, эквивалентной в этом случае присваиванию. (В случае ссылок копирование и присваивание тоже легальны, но имеют разный эффект.)
Семантика копирования для INTEGER, то мы ожидаем от присваивания m := n, (или от соответствующей передачи аргументов) копирования значения n в m.
Проведенный анализ применим и к связанной с присваиванием операции эквивалентности. Рассмотрим булевы выражения: x = y и x /= y. Для x и y ссылочных типов, как уже отмечалось, истинность первого выражения (ложность второго) достигается только тогда, когда источник и цель оба имеют значение void или оба присоединены к одному и тому же объекту. Для развернутых x и y, такая семантика неприемлема, - здесь действует другая семантика, основанная на последовательном сравнении значений соответствующих полей, так что в этом случае выражение x = y имеет то же значение, что и equal (x, y).
Разрешается, как мы увидим позже при обсуждении наследования, изменить семантику equal для придания специального смысла эквивалентности экземпляров некоторого класса. Это никак не отразится на операции эквивалентности =, которая по соображениям безопасности и простоты всегда имеет смысл оригинальной функции standard_equal. |
Правило присваивания и сравнения обобщается в следующем замечании.
Присоединение y к x означает копирование объекта x, если x и y принадлежат развернутым типам. Это ссылочное присоединение, если x и y ссылочного типа. Аналогично, тесты: x=y и x/=y означают сравнение объектов для x и y x и y ссылочного типа.
В рассматриваемых до сих пор случаях источник и цель принадлежали одной категории - оба развернутого или ссылочного типа. Что если они из разных категорий?
Вначале рассмотрим ситуацию, когда в присваивании x := y цель x y - ссылочного типа. Единственно приемлемой в этом случае является семантика копирования: копирование полей объекта, присоединенного к y, в поля объекта, присоединенного к x. Все хорошо, если y не void в период выполнения. Если y - void, то результатом будет включение исключения. (Исключения изучаются в лекции 12)
Для развернутого x тест x = Void не является причиной появления исключительной ситуации; он просто дает значение false. Но нет приемлемой семантики для присваивания x := Void, так что всякая подобная попытка приводит к появлению исключения. |
Рассмотрим теперь другой случай присваивания: x := y, где x ссылочного типа, а y - развернутого. Тогда в период выполнения y всегда присоединен к объекту, который мы можем назвать OY, и присоединение также должно присоединить x к объекту. Казалось бы, что можно присоединить x непосредственно к OY. Однако это привело бы к созданию ссылки на подобъект, а подобные ссылки запрещены нашими правилами. Поэтому правильной стратегией является клонирование источника OY и присоединение x к созданной копии. Рассмотрим пример:
class C feature
...
end
class COMPOSITE2 feature
x: C
y: expanded C
reattach is
do x := y end
end
При вызове компонента reattach в результате присваивания x будет присоединен к объекту, являющемуся клоном объекта y.
Следующая таблица обобщает семантику присоединения изученных случаев:
| Тип цели x | Тип источника y | |
|---|---|---|
| Ссылочный | Развернутый | |
| Ссылочный | Ссылочное присоединение | Клонирование: эффект x := clone(y) |
| Развернутый | Копирование: эффект x.copy(y) Ошибка, если y - void | Копирование: эффект x.copy(y) |
Семантика операций, проверяющих эквивалентность ( = и /= ) должна быть совместимой с семантикой присваивания. Наряду с операцией = можно использовать и equal. Какую из этих операций следует применять, зависит от обстоятельств.
x и y - ссылки, их можно тестировать как на ссылочную эквивалентность, так и на объектную эквивалентность при условии, что ссылки не void. Мы определили операцию x = y, как обозначающую ссылочную эквивалентность в этом случае. Функция equal, введенная для проверки объектной эквивалентности, дополнена и применима, когда x или y - void.x и y - x - ссылка, y - x - void, возвращая значение false в этой ситуации, поскольку y не может быть void.Этот анализ дает желаемую интерпретацию равенства = во всех случаях. Для объектного сравнения всегда доступна функция equal, расширенная на случаи, когда один или оба операнда принимают значение void. Следующая таблица подводит итог семантике сравнения:
| Тип цели x | Тип источника y | |
|---|---|---|
| Ссылочный | Развернутый | |
| Ссылочный | Ссылочное сравнение | equal(x,y) объектное сравнение, если x не void, иначе - false |
| Развернутый | equal(x,y) объектное сравнение, если y не void, иначе - false | equal(x,y) объектное сравнение |
Сравнение таблиц 8.1 и 8.2 показывает совместимость присваивания и операций сравнения в упоминавшемся уже смысле. Напомним, в частности, что equal (x, y) будет истинно после выполнения x := clone (y) или x. copy (y).
Обсуждаемые проблемы возникают во всех языках, включающих ссылки и указатели, таких как Pascal, Ada,
В предыдущих разделах отмечалось, что два
Для x и y ссылочного типа при непустом значении y присваивание x := y или соответствующее присоединение в результате вызова приведут к тому, что x и y будут присоединены к одному и тому же объекту.
(рис 8.23) Разделение как результат присоединенияВ результате x и y становятся тесно связанными до тех пор, пока x или y не будет присвоено новое значение. В частности любая операция вида x.f, где f некоторый компонент соответствующего класса, приведет к тому же результату, что и y.f, поскольку воздействует на тот же объект.
Присоединение x к тому же объекту, что и y, известно как назначение динамического псевдонима (dynamic aliasing). Псевдоним является динамическим, поскольку существует только во время выполнения.
Статические псевдонимы закрепляют два имени за одним и тем же программным элементом в исходном тексте, и они всегда обозначают одно и то же значение вне зависимости от событий, происходящих во время выполнения. Этот прием включен в некоторые языки программирования. В Fortran директива означает, что две переменные разделяют содержимое одной и той же области памяти. Директива препроцессора C #define x y определяет, что любое упоминание x в тексте программы эквивалентно y. |
Наличие динамических псевдонимов оказывает более серьезное влияние на операции присваивания с участием сущностей ссылочного типа, нежели с участием сущностей x и y INTEGER присваивание x := y просто устанавливает для x значение y и никакого связывания x и y не происходит. После подобного присваивания с участием ссылочных типов x и y становятся псевдонимами одного объекта.
Неприятным последствием применения псевдонимов (и статических, и динамических) является воздействие операций на сущности, даже не упоминаемые в операциях.
Модель вычислений без псевдонимов обладает приятным свойством: приведенный ниже фрагмент всегда справедлив
[БЕЗ СЮРПРИЗОВ] -- Предположим, что свойство P(y) выполняется x := y C (x) -- P(y) останется выполнимым.
Этот пример подразумевает, что P (y) это частное свойство y, а C (x) некая операция с участием x, но не y. В этом случае никакие действия над x не влияют на значение y.
Для сущностей x и y типа INTEGER:
-- Предположим, что здесь y = 0 x := y x := -1 -- По-прежнему y "= 0.
В этом случае нет никакого способа изменить y путем присваивания значения x. Обратимся теперь к аналогичной ситуации с участием динамических псевдонимов. Пусть x и y экземпляры следующего класса C:
class C feature
boolattr: BOOLEAN
-- Булев атрибут для описания некоторого свойства объекта.
set_true is
-- Установка boolattr в true.
do
boolattr := True
end
... Другие компоненты ...
end
Теперь предположим, что тип y это C, и что y в определенный момент времени выполнения не является пустой ссылкой. Тогда следующий пример уже не обладает свойством "БЕЗ СЮРПРИЗОВ":
[СЮРПРИЗ, СЮРПРИЗ!]
-- Предполагаем, что y.boolattr равно false.
x := y
-- Значение y.boolattr по-прежнему false.
x.set_true
-- Но теперь y.boolattr равно true!
Последняя инструкция данного фрагмента никоим образом не содержит y, однако одним из ее результатов является изменение свойств y.
Отмеченные тревожные последствия операций присваивания с участием ссылок порождают законный вопрос о целесообразности сохранения динамических псевдонимов в нашей модели вычислений.
Ответ - частично теоретический и частично практический:
Поочередно рассмотрим оба указанных аспекта.
Предварительное рассмотрение свидетельствует о том, что сами ссылки и их разделение необходимы во многих случаях. Некоторые стандартные структуры данных содержат
(рис 8.24) Связный циклический списокНа самом деле необходимость в ссылках, присоединении и разделении ссылок возникает и в не слишком сложных ситуациях. Вернемся к одному из вариантов класса описывающего книгу
class BOOK3 feature ... Остальные компоненты ... author: WRITER end
Здесь необходимость разделения ссылок обусловлена тем, что две книги или более могут быть написаны одним и тем же автором. Во многих примерах данной лекции подразумевается разделение, - так в случае PERSON у нескольких персон может быть один лендлорд. Это вопрос потребностей моделирования, а не реализации.
Если b1 и b2 два экземпляра BOOK3 одного автора, то b1.author и b2.author - псевдонимы, то есть ссылки, присоединенные к одному объекту, и использование любой из них в качестве цели вызова даст в точности одинаковый эффект. Рассмотренные в таком свете динамические псевдонимы выглядят скорее не как потенциально опасная возможность программирования, а как факт из реальной жизни. Это цена, которую необходимо заплатить за возможности использования нескольких имен при обращении к одному объекту.
Можно легко найти нарушения приведенного выше свойства "БЕЗ СЮРПРИЗОВ", не обращаясь к области ПО. Пусть для некоторой книги b определены следующие свойства и операции:
NOT_NOBEL (b) обозначает: "автор никогда не получал Нобелевскую премию".NOBELIZE (b) обозначает: "Присудить Нобелевскую премию автору книги b".Теперь предположим, что rb обозначает книгу "Красное и черное", а cp - "Пармская обитель". Последующие действия вполне корректны:
[СЮРПРИЗ В ОСЛО]
-- Предположим, что сейчас выполняется NOT_NOBEL(rb)
NOBELIZE(cp)
-- Теперь свойство NOT_NOBEL(rb) уже несправедливо!
Операция над cp изменяет свойство другой сущности rb, которая не упоминается в инструкции! Последствия могут быть весьма значительными (редкая книга Нобелевского лауреата будет переиздана, ее цена возрастет и т.д.). В данной ситуации, не связанной с ПО, произошло в точности то же, что и в предыдущем программном примере после операции x.set_true, повлиявшей на состояние y без упоминания y.
Таким образом, динамические псевдонимы вовсе не являются результатом гнусных трюков программистов со ссылками и указателями. Это следствие свойственного человеку стремления давать имена вещам ("объектам" в наиболее общем смысле этого слова), а иногда и несколько имен одному предмету. В классической риторике эти явления известны как полионимия (polyonymy), например, использование имен "Кибела" (Cybele), "Деметра" (Demeter) и "Церера" (Ceres) "для одной и той же богини, и антономазия (antonomasia) - возможность ссылаться на объект, косвенно именуя его, как, например, в фразе "прекрасная дочь Агамемнона", обращаясь к прекрасной Елене из Трои.
Теперь накоплено достаточно подтверждений того, что любая система моделирования и разработки ПО должна поддерживать понятие ссылки, а, следовательно, и динамические псевдонимы. Как теперь справиться с неприятными последствиями? Невозможность обеспечить свойство "БЕЗ СЮРПРИЗОВ" показывает, что ссылки и псевдонимы подвергают опасности саму возможность систематического рассмотрения ПО. Это означает, что, изучая исходный текст, нельзя надежно и просто сделать какие либо выводы о свойствах ПО времени выполнения.
Для поиска решения необходимо сначала понять, является ли данная проблема специфической для ОО-метода. Знакомство с другими языками программирования, такими как Pascal, C, PL/I, Ada и Lisp убеждает в том, что и там ведутся подобные дискуссии. Все языки располагают средствами динамического размещения объектов и разрешают объектам содержать ссылки на другие объекты. Существенно различаются лишь уровни абстракции: указатели C и PL/I фактически являются машинными адресами, а Pascal и Ada наряжают указатели в более респектабельные одежды, используя правила типизации.
Что тогда нового в ОО-разработке? Ответ связан не с теоретическими возможностями метода (за исключением важных отличий, связанных со сборкой мусора, ОО-структуры времени выполнения идентичны своим аналогам в Pascal и Ada), а в практике разработки ПО. ОО-разработка подразумевает повторное использование. В частности, любой проект, в котором многочисленные прикладные классы выполняют хитрые манипуляции со ссылками, является примером некорректного использования ОО-подхода. Такие операции должны быть включены в библиотечные классы.
Для любой системы подавляющее большинство объектных структур, требующих нетривиальных операций со ссылками, не зависит от области приложения и представляет хорошо известные и часто используемые структуры: списки всевозможных типов, деревья в различных представлениях, хэш-таблицы и некоторые другие. В хорошей ОО-среде разработки библиотека должна быть легко доступной и предоставлять реализации подобных структур. В качестве иллюстрации в приложении A приведен эскиз библиотеки Base. Классы в таких библиотеках могут содержать множество ссылочных операций. Примером могут служить действия над ссылками, необходимые для вставки и удаления элемента связного списка или узла дерева.
Если же при разработке приложения появится потребность в сложных объектных структурах, не представленных адекватно в имеющихся библиотеках, то это следует рассматривать как потребность в новых классах общего назначения. Их необходимо разработать, потратить необходимое время на их тщательное тестирование и затем включить в соответствующую библиотеку. Такая ситуация в терминах одной из предшествующих лекций является примером перехода с позиций потребителя по отношению к повторному использованию на позиции производителя.
Оставшиеся операции со ссылками в классах, зависимых от конкретного приложения, должны быть ограничены простыми и безопасными операциями. В библиографических заметках упомянута статья Suzuki, развивающая эту идею.
В данной лекции введены некоторые правила и нотация для работы с объектами и соответствующими сущностями. Некоторые из этих соглашений могут вызвать удивление. Поэтому полезно завершить изучение объектов и их свойств рассмотрением спорных вопросов и доводов в пользу выбранных решений. Автор, естественно, надеется, что читатели полностью одобрят его выбор. Основная цель данной дискуссии - добиться полного понимания основополагающих проблем. В этом случае, если кто-то предпочитает другое решение, то сможет выбрать его вполне осознанно.
Для разминки начнем с небольшой проблемы, связанной с нотацией. Это конечно деталь, но из деталей складывается общая картина. Речь идет о наборе соглашений, используемых для графического представления классов и объектов.
В предшествующей лекции отмечалось насколько важно различать понятия класс и объект. Соответственно, должны отличаться и графические обозначения. Классы на диаграммах, представляющих системную архитектуру, представлены в виде эллипсов. Они соединяются стрелками для обозначения отношений между классами, обычными стрелками отмечаются отношения наследования и двойными -
Классы и объекты существуют в различных контекстах. Эллипсы классов являются частью диаграмм, представляющих структуру программной системы. Прямоугольники-объекты используются на моментальных снимках состояния системы в процессе выполнения. Поскольку указанные виды диаграмм имеют совершенно разное назначение, то в бумажном представлении, как в данной книге, они не появляются одновременно в одном контексте. Но для интерактивных CASE-средств ситуация принципиально меняется. В процессе отладки программной системы возникает необходимость отобразить объект, а затем - порождающий класс для изучения его компонент, родителей и других свойств.
Используемые для классов и объектов графические соглашения совместимы со стандартом, установленным методом BON (Nerson и Walden). В методе BON, (Business Object Notation) предназначенном для использования в интерактивных CASE-средствах и в документации, классы отображаются в виде пузырьков, растягиваемых по вертикали, показывающих компоненты класса, инварианты и другие свойства.(О BON см. библиографические заметки и лекцию 9 курса "Основы объектно-ориентированного проектирования")
В развитие нашего соглашения поля
Трудно удержаться от того, чтобы не процитировать следующий ненаучный аргумент, заимствованный из рецензии Ian Graham на книгу по ОО-анализу, использующую другие графические соглашения:
Мне не нравятся классы, изображаемые в виде треугольников с острыми углами. Мне кажется, что это их экземпляры имеют острые углы, так что можно пораниться, уронив их на ногу, а классы безопасны и поэтому у них должны быть скругленные углы.
Важный синтаксический вопрос - должны ли мы установить синтаксическое различие при работе со ссылками и простыми значениями. Как отмечалось, присваивание и эквивалентность имеют различный смысл для ссылок и значений :=, =, /= ). Не опасно ли это? Не предпочтительнее ли использовать различные наборы символов, напоминая тем самым, что они имеют разный смысл?
Два набора символов использовались в языке Simula 67. Немного изменив нотацию для совместимости с настоящей книгой (в языке Simula reference сокращается до ref ), в Simula можно записать объявление сущности ссылочного типа C так:
x: reference C
Ключевое слово reference указывает, что экземпляры x будут ссылками. Рассмотрим объявления:
m, n: INTEGER x, y: reference C
Нотация Simula, используемая для операций с простыми и ссылочными типами, приведена в таблице.
| Операция | Операнды |
Операнды-ссылки |
|---|---|---|
| Присваивание | m := n | x :- y |
| Проверка на равенство | m = n | x == y |
| Проверка на неравенство | m /= n | x =/= y |
Соглашения Simula лишены неоднозначности. Почему бы их не сохранить? К сожалению, эти соглашения могут служить примером благих намерений, приносящих скорее вред, нежели пользу. Проблемы начинаются в прозаической области поиска ошибок. Два набора символов похожи, - это провоцирует синтаксические ошибки, подобные использованию := вместо :-.
Такие ошибки будут обнаружены компилятором. Но хотя ограничения, проверяемые компилятором, предназначены помочь программисту, - здесь это может не сработать. Либо вы знаете разницу между семантикой ссылок и значений, и тогда подсказки компилятора о необходимости проверки, каждый раз, когда вы написали присваивание или равенство, могут показаться раздражающими. Либо вы не понимаете этой разницы, тогда его подсказки немногим могут помочь.
Но самый важный аспект соглашений Simula в том, что он не оставляет выбора: для ссылок нет доступной конструкции, дающей семантику значений. Представляется разумным для сущностей a и b ссылочного типа иметь два множества операций:
a :- b для ссылочных присваиваний и a == b сравнения ссылок;a := b для присваивания путем копирования (эквивалент a := clone (b) или a.copy (b) в нашей нотации) и a = b для сравнения объектов (эквивалент нашего equal (a, b) ).Но за одним исключением, Simula поддерживает только первое множество операций. Если необходимы операции второго множества ( copy или clone, сравнение объектов), придется написать специальные подпрограммы для каждого целевого TEXT, для которого Simula предлагает оба множества операций.
Кстати, при дальнейшем анализе идея предоставления двух множеств операций для всех ссылочных типов не кажется уже столь разумной. Это бы означало, что тривиальная описка - использование := вместо :-, теперь уже не обнаруживалась бы компилятором, а приводила бы к результату, далекому от ожидаемого, например, к клонированию вместо ссылочного присваивания.
Как результат этого анализа, нотация этой книги использует соглашения, отличные от тех, что используются в Simula: одни и те же символы применимы для развернутых и ссылочных типов с различной семантикой. Эффекта семантики значений можно достигнуть для объектов ссылочного типа при использовании предопределенных подпрограмм, применимых для всех типов:
a := clone (b) или a.copy (b) для объектного присваивания;equal (a, b) для сравнения объектов на идентичность всех полей.Эта нотация существенно отличается от нотации, применяемой для их ссылочных двойников, ( := и =, соответственно) что снижает риск появления недоразумений.
Помимо чисто синтаксических аспектов, эта проблема интересна и тем, что она представляет типичный образец компромиссов, возникающих при проектировании языка, когда требуется найти баланс между конфликтующими критериями. Один из критериев, победивших в Simula, - может быть сформулирован следующим образом:
Но другие силы, доминирующие в нашей нотации, требуют:
INTEGER ) присваивание и сравнение значений представляются наиболее употребительными операциями. Для ссылок, в то же время, ссылочное сравнение и присваивание используется чаще, чем клонирование, копирование и сравнение объектов. Поэтому в обоих случаях предпочтительнее использовать := и = для фундаментальных операций.:- и := ), но с различной семантикой.Еще одна причина играет роль в данном случае, хотя она включает механизм, пока еще не изученный. В последующих лекциях мы познакомимся с родовыми или LIST [G], где G, известно как формальный родовой параметр, представляющий произвольный тип. Такой класс может манипулировать сущностями типа G и использовать их в присваиваниях и проверках на равенство. Клиенты, нуждающиеся в использовании такого класса, должны позаботиться о создании типа, служащего в качестве фактического родового параметра. Например, они могут использовать LIST [INTEGER] или LIST [POINT]. Как показывают эти примеры, фактический родовой параметр может быть a и b имеют тип G, то часто полезно использовать присваивания в форме a := b или тесты в форме a = b с намерением получить семантику значений, когда фактический параметр принадлежит развернутому типу, такому как INTEGER, и ссылочную семантику - для ссылочного типа, такого как POINT.
Примером подпрограммы, нуждающейся в таком дуальном поведении, является процедура вставки элемента x в список. Процедура создает новый элемент списка. Если x целое, элемент должен содержать копию значения x. Если x является ссылкой, то элемент списка должен содержать ссылку на объект, присоединенный к x. |
В таких случаях, правила, определенные выше, гарантируют желаемое дуальное поведение, что было бы недостижимо, если бы требовался различный синтаксис для двух видов семантики. С другой стороны, если во всех случаях требуется единая семантика, то и это достижимо: такое поведение может быть только семантикой значений (так как семантика ссылок не имеет смысла для clone (или copy) и equal, а не ( := и = ).
Форма вызова подпрограмм clone и equal является стилевой особенностью, которая может вызвать удивление. На первый взгляд нотация:
clone (x) equal (x, y)
выглядит не слишком объектно-ориентированной. Догматичное следование принципу "ОО-стиля вычислений" из предыдущей лекции предполагает другую форму (См. "Объектно-ориентированный стиль вычислений", лекция 7):
x.twin -- twin это двойник - клон. x.is_equal (y)
В первой версии нотации так и делалось, однако возникла проблема пустых ссылок. Вызов компонента вида x.f (...) не может быть корректно выполнен в случае пустого x во время выполнения. В этом случае вызов инициирует исключение, которое повлечет аварийное завершение всей системы, если в соответствующем классе не предусмотрена обработка исключений. Поскольку во многих случаях x может быть действительно пустой ссылкой, то это означало бы, что каждый вызов
if x = Void then
z := Void
else
z := x.twin
end
Соответственно, реализация вызова is_equal должна выглядеть ( and then является вариантом and. См. "Нестрогие булевы операции", лекция 13):
if ((x = Void) and (y = Void)) or ((x /= Void) and then x.is_equal (y)) then ...
Излишне говорить, что не следует придерживаться этих соглашений. Нам быстро надоест писать подобные витиеватые фрагменты, а когда мы забудем это сделать, то результатом будет ошибка времени выполнения. Окончательный вариант соглашений, сформулированный в данной лекции, замечателен еще и тем, что дает ожидаемые результаты для x, равного void, - clone (x) вернет void, а equal (x, y) вернет true, если и y - void.
Вызов процедуры copy в форме x.copy (y) не создает подобных проблем, поскольку требует непустых x и y. Это следствие семантики процедуры copy, копирующей поля одного объекта в поля другого, и имеющей смысл, только если существуют оба объекта. Как показано далее, такое условие для y фиксируется формальным предусловием copy, заданным в явном виде в документации.
Отметим, что введенная выше функция is_equal существует в библиотеке системы. Причина в том, что часто удобнее определить специфические варианты эквивалентности элементов конкретного класса, перекрыв семантику по умолчанию. Для достижения этого эффекта достаточно переопределить функцию is_equal в соответствующем классе. Функция equal определяется в терминах is_equal (выражением, показанным выше при иллюстрации использования is_equal ), и поэтому следует за всеми переопределениями is_equal.
Когда есть функция clone, то нет необходимости в . Это связано с тем, что функция clone определена как создание объекта с последующим вызовом copy. Поэтому для адаптации clone к специфике класса достаточно переопределить процедуру copy данного класса. (См. также лекция 16)
Последние комментарии частично прояснили вопрос о статусе универсальных операций clone, copy, equal, is_equal, deep_clone, deep_equal.
Эти операции не являются языковыми конструкциями, невзирая на их фундаментальную значимость для практики. Они поставляются классом ANY основной библиотеки Kernel. Этот класс имеет то специальное свойство, что каждый класс, созданный разработчиком, автоматически становится наследником (прямым или косвенным) класса ANY. Вот почему становится возможным переопределить вышеупомянутые компоненты для поддержки специального вида эквивалентности или копирования. (См. "Глобальная структура наследования", лекция 16)
Сейчас нет необходимости в деталях, поскольку мы еще вернемся к этой проблеме при изучении наследования. Но уже теперь полезно знать, что благодаря механизму наследования, мы можем полагаться на библиотечные классы, поддерживающие свойства, доступные всем классам, - и каждый класс может изменить их, приспосабливая к своим, специфическим целям.
void ) или присоединенной к объекту. Проверка условия x = Void позволяет определить текущее состояние ссылки. Корректное выполнение вызова x.f (...) возможно, если x не пустая ссылка.class C ..., то сущность типа C будет обозначать ссылку, которая может быть присоединена к экземпляру C. Если начало объявления выглядит как expanded class D ..., то сущность типа D будет обозначать объект (экземпляр D ) и никогда не может быть пустой ссылкой.BOOLEAN, CHARACTER, INTEGER, REAL, DOUBLE ) определены как развернутые классы.create x создает объект, инициализирует его поля значениями по умолчанию и присоединяет к нему x. Если в классе определены порождающие процедуры создания, то выполнение инструкции вида create x.creatproc (...) приведет, кроме того, к заданной специфической инициализации полей.:= ) и проверка эквивалентности ( = ) являются ссылочными операциями. Для сущностей Понятие идентичности объекта играет важную роль для баз данных, особенно объектно-ориентированных. Смотри лекцию 13 курса "Основы объектно-ориентированного проектирования", посвященную таким базам данных, и библиографию к ней.
Графические обозначения метода BON (Business Object Notation) разработаны Jean-Marc Nerson и Kim Walden [Walden 1995]. James McKim и Richard Bielak детально рассмотрели преимущества альтернативных порождающих процедур [Bielak 1994].
Риски, связанные с нетипизированными указателями и ссылочными операциями, уже долгое время волнуют специалистов в области методологии программирования, порождая намеки на то, что в области данных это аналог ненавистной операции goto в области управления выполнением кода. В удивительно малоизвестной статье Nori Suzuki [Suzuki 1982] обсуждается возможность избежать в рамках строгого подхода с использованием высокоуровневых операций проблем с динамическими псевдонимами (как избавляются от применения goto, используя приемы "структурного программирования"). Хотя по признанию автора выводы неутешительны, данная статья весьма полезна.
Я признателен Ross Scaife из Университета Кентукки за помощь по вопросам риторики. См. его страницу http://www.uky.edu/ArtsSciences/Classics/rhetoric.html.
Напишите классы BOOK and WRITER описывающие книги и их авторов, используя заготовки из данной лекции. Обратите внимание на необходимость включения всех важных подпрограмм, а не только атрибутов.
Напишите класс PERSON включающий простое понятие личности с атрибутами mother, father и sibling (следующий по старшинству брат или сестра, если они есть). Включите подпрограммы возвращающие списки имен родителей, двоюродных братьев и сестер, дядюшек и тетушек, свекра и свекрови, тестя и тещи данного лица. Совет: пишите рекурсивные процедуры, но внимательно избегайте бесконечных рекурсий для отношений, например, двоюродный брат или сестра, являющихся циклическими.
Предположим, вы часто используете сравнение в форме x.is_equal (y), и хотите упростить нотацию, используя преимущества инфиксной записи (применимой здесь, поскольку наша функция имеет один аргумент). Для инфиксного компонента используйте некоторый оператор $$\S,$$ вызов тогда будет записываться в виде $$x \S y$$. Это маленькое упражнение потребует выбора для оператора $$\S,$$ подходящего для данной ситуации символа, совместимого с правилами инфиксных операторов. Конечно, здесь может существовать много возможных ответов, выбор одного из которых частично (но только частично) дело вкуса. (См. "Компоненты-операторы", лекция 7)
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.