![]() |
||||||||
|---|---|---|---|---|---|---|---|---|
Статические структуры: классы Классы, а не объекты - предмет обсуждения
В данной книге не используется самостоятельная концепция метакласса. Присутствие метаклассов в том или ином языке или среде разработки не оправдывает смешение понятий моделей и их экземпляров - классов и объектов.
|
||||||||
Пример наследования - классы INTEGER, REAL и DOUBLE могут быть наследниками двух более общих классов; NUMERIC, в котором определены основные арифметические операции ("+", "-", "*"); COMPARABLE, представляющий операции сравнения ("<" и другие). В качестве примера использования универсализации можно рассмотреть родовой класс MATRIX, родовой параметр которого определяет тип элементов матрицы. Экземпляры класса MATRIX [INTEGER] будут целочисленными матрицами, а экземпляры MATRIX [REAL] будут в качестве элементов содержать действительные числа. В качестве комплексного примера одновременного использования наследования и родовых классов можно использовать класс MATRIX [NUMERIC], экземпляры которого могут содержать элементы типа INTEGER или REAL или любого нового типа T, определенного разработчиком как наследника класса NUMERIC. |
При условии хорошей реализации нет необходимости опасаться каких-либо негативных последствий решения определять все типы как классы. Ничто не мешает предоставить компилятору специальную информацию о базовых классах. В этом случае порождаемый код для операций со значениями классов INTEGER и BOOLEAN может быть столь же эффективным, как если бы они были встроенными типами данного языка.
Построение непротиворечивой и универсальной системы типов требует комплексного применения ряда важных ОО-методик, которые будут рассмотрены позже. К их числу относятся расширяемые классы, гарантирующие корректное представление простых значений; инфиксные и префиксные операции, обеспечивающие возможность использования привычного синтаксиса (a < b или -a вместо неуклюжих конструкций a.less_than (b) или a.negated); ограниченная универсализация, необходимая для описания классов, адаптируемых к типам со специфическими операциями. Например, класс MATRIX может представлять целочисленные матрицы, а также матрицы, элементами которых являются числа других типов.
Что представляет собой класс можно выяснить, изучая простой, но типичный пример, который демонстрирует фундаментальные свойства, применимые практически ко всем классам.
Пример использует представление точки в двумерной графической системе:
Для определения типа POINT как абстрактного типа данных потребуется четыре функции-запроса: x, y, ρ, θ. (В текстах подпрограмм для двух последних функций будут использоваться имена rho и theta). Функция x возвращает абсциссу точки (горизонтальную координату), y - ординату (вертикальную координату), ρ - расстояние от начала координат, θ - полярный угол, отсчитываемый от горизонтальной оси. Значения x и y являются декартовыми, а ρ и θ - полярными координатами точки. Другой полезной функцией является distance, возвращающая расстояние между двумя точками.
Далее спецификация АТД будет содержать такие команды, как translate (перемещение точки на заданное расстояние по горизонтали и вертикали), rotate (поворот на определенный угол вокруг начала координат) и scale (уменьшение или увеличение расстояния до начала координат в заданное число раз).
Нетрудно написать полную спецификацию АТД, включающую указанные функции и некоторые ассоциированные аксиомы. Далее в качестве примера приведены две из перечисленных функций:
x: POINTREAL
translate: POINT × REAL × REALPOINT
и одна из аксиом:
x (translate (p1, a, b)) = x (p1) + a
утверждающая, что для произвольной точки p1 и действительных значений a и b при трансляции точки на <a, b> абсцисса увеличивается на a.
Читатель, если пожелает, может самостоятельно завершить спецификацию АТД. В дальнейшей дискуссии подразумевается, что вы понимаете, как устроен данный АТД, вне зависимости от того, написали ли вы его полную формализацию или нет. Сосредоточим внимание на реализации АТД - классе.
Любой абстрактный тип данных и POINT в частности характеризуется набором функций, описывающих операции применимые к экземплярам АТД. В классе, реализующем АТД, функции становятся компонентами (features) - операциями, применимыми к экземплярам класса.
Вбыло показано, что в АТД существуют функции трех видов: запросы (queries), команды (commands) и конструкторы (creators). Для компонентов классов необходима дополнительная классификация, основанная на том, каким образом реализован данный компонент - в пространстве или во времени (by space or by time). (См. "Категории функций",
Пример координат точки отчетливо демонстрирует эту разницу. Для точек доступны два общепринятых представления - в декартовых или полярных координатах. Если для представления выбрана декартова система координат, то каждый экземпляр класса содержит два поля представляющих координаты x и y соответствующей точки:
Если p1 является такой точкой, то получение значений x и y сведется просто к просмотру соответствующих полей данной структуры. Однако определение значений ρ и θ требует вычисления выражения √(x2 + y2) для ρ и arctg (y/x) для θ (при условии ненулевого x).
Использование полярной системы координат приводит к противоположной ситуации. Теперь ρ и θдоступны просто как значения полей, а определение x и y возможно после простых вычислений (ρ cosθ, ρ sinθ, соответственно).
Приведенный пример указывает на необходимость рассмотрения компонентов двух видов:
Вторая категория - подпрограммы - нуждается в дальнейшей дополнительной классификации. Часть подпрограмм возвращает результат, и их называют функциями (functions). В приведенном примере функциями являются x и y в представлении в полярных координатах, в то время как rho и theta - функции в декартовых координатах, все они возврвщают результат типа REAL. Подпрограммы, не возвращающие результат, соответствуют командам в спецификации АТД и называются процедурами (procedures). Например, класс POINT содержит процедуры translate, rotate и scale.
Не следует путать понятие "функция", обозначающее в классах программу, возвращающую результат, с использованным ранее толкованием функции как математического описания операций АТД. Эта досадная путаница понятий обусловлена устоявшейся терминологией в математике и программировании. |
Надана рассмотренная выше классификация, представленная в виде дерева:
Эта классификация является внешней, основанной на том, каким образом данный компонент выглядит для использующего его клиента.
Можно предложить другую, внутреннюю классификацию, использующую в качестве основного критерия способ реализации компонента в классе:
На первый взгляд один из аспектов приведенной выше классификации может вызывать беспокойство. Во многих случаях необходимо иметь возможность работать с объектом, например с точкой p1, не заботясь о том, какое внутреннее представление используется для p1 - декартово, полярное или иное. Необходимо ли для этого отличать атрибуты от функций?
Ответ зависит от того, с какой точки зрения рассматривать данную проблему - разработчика, автора данного класса POINT или клиента, создавшего класс, использующий POINT. Для разработчика разница между атрибутами и функциями принципиально важна и имеет смысл. Ему необходимо принимать решения о том, какие компоненты будут реализованы как данные в памяти и какие будут доступны в результате вычислений. Но, заставлять клиента осознавать эту разницу, было бы серьезной ошибкой. Клиент должен обращаться к значениям x или ρ для точки p1, не заботясь и не имея информации о том, как реализованы соответствующие запросы.
Решение проблемы дает принцип унифицированного доступа (Uniform Access principle), введенный в дискуссии о модульности . Принцип декларирует, что клиент должен иметь возможность доступа к свойствам объекта, используя одинаковую нотацию, вне зависимости от того, как это свойство реализовано - в памяти или как результат вычислений (в пространстве или во времени, в виде атрибута или подпрограммы). Этому важному принципу необходимо следовать при разработке нотации для обращения к компонентам класса. Так выражение, обозначающее значение компонента x объекта p1 будет всегда записываться в виде:
p1.x
вне зависимости от того, осуществляется ли доступ к полю данных объекта или выполняется подпрограмма.
При использовании такой нотации неопределенность может возникать только для запросов без аргументов, которые могут быть реализованы и как функции и как атрибуты. Команда должна быть процедурой, запрос с аргументами должен быть функцией, так как атрибуты не могут иметь аргументов. |
Принцип унифицированного доступа необходим для гарантирования автономности компонентов ПО. Он защищает право создателя класса свободно экспериментировать с различными способами реализации, не создавая помех клиентам. (СМ. "Использование утверждений для документирования: краткая форма класса",)
Pascal, C и Ada нарушают этот принцип, предоставляя различную нотацию для вызова функций и для доступа к атрибутам. Это объяснимо для таких не ОО-языков (хотя еще в 1966 г. синтаксис Algol W предшественника Pascal удовлетворял этому принципу). Более новые языки, такие как C++ и Java, также не следуют этому принципу. Отход от этого принципа может служить причиной того, что изменения внесенные во внутренние представления (например переход от полярной системы координат к декартовой или иные) повлекут за собой неработоспособность многих клиентских классов. Это является одной из причин нестабильности программных разработок. |
Принцип унифицированного доступа является источником определенных требований и к подготовке документации. Последовательное применение этого принципа должно гарантировать, например, что в официальной документации не содержится сведений о том, является ли данный запрос без аргументов функцией или атрибутом. Это одно из требований к стандартной методике документирования классов, известной как краткая форма класса.
Ниже приведена версия исходного текста класса POINT. Фрагменты, начинающиеся с двух тире "--", представляют собой комментарии, продолжающиеся до конца строки. Комментарии содержат пояснения, облегчающие понимание текста, и не влияют на семантику класса.
indexing
description: "Точка на плоскости"
class POINT feature
x, y: REAL
-- Абсцисса и ордината
rho: REAL is
-- Расстояние до начала координат (0, 0)
do
Result := sqrt (x^2 + y^2)
end
theta: REAL is
-- Полярный угол
do
-- Предлагается реализовать в качестве упражнения (упр. У7.3)
end
distance (p: POINT): REAL is
-- Расстояние до точки p
do
Result := sqrt ((x - p.x)^2 + (y- p.y)^2)
end
translate (a, b: REAL) is
-- Перемещение на a по горизонтали, b по вертикали
do
x := x + a
y := y + b
end
scale (factor: REAL) is
-- Изменение расстояния до начала координат в factor раз
do
x := factor * x
y := factor * y
end
rotate (p: POINT; angle: REAL) is
-- Поворот вокруг p на угол angle
do
-- Предлагается реализовать в качестве упражнения (упр. У7.3)
end
end
Некоторые аспекты приведенного текста неочевидны и требуют дополнительных разъяснений.
Класс в основном состоит из предложения, перечисляющего различные компоненты, вводимого ключевым словом feature. Кроме того, присутствует предложение indexing дающее общее описание (description), полезное для понимания функциональности класса, но никак не влияющее на семантику исполнения. Позднее будут рассмотрены три дополнительных предложения: inherit - для наследования; creation - при необходимости использования специального конструктора; invariant - для объявления инвариантов класса. Будет рассмотрена также возможность включения в класс двух или более предложений feature.
Класс POINT демонстрирует ряд приемов, которые будут использованы в последующих примерах. Необходимо оговорить основные соглашения.
Компоненты x и y объявлены как относящиеся к типу REAL без ассоциированного алгоритма, следовательно, они являются атрибутами. Все остальные компоненты содержат конструкции вида
is
do
... Инструкции ...
end
которые описывают алгоритм, что является признаком подпрограмм. Подпрограммы rho, theta и distanceвозвращают результат типа REAL во всех трех случаях, что отражено в объявлениях вида
rho: REAL is ...
Это определяет их как функции. Две другие подпрограммы, translate и scale, не возвращают результата (объявление не завершается конструкцией: T, где T некоторый тип) и, соответственно, являются процедурами.
Поскольку x и y являются атрибутами, а rho и theta функциями, данный конкретный класс использует для представления точки декартову систему координат.
Тело подпрограммы (предложение do) представляет собой последовательность инструкций. Можно разделять последовательные инструкции и объявления точкой с запятой в традициях Algol-Pascal, но это не обязательно. Далее с целью упрощения точка с запятой будет опускаться между элементами на отдельных строках, но всегда будет использоваться как разделитель нескольких инструкций или объявлений в одной строке. (См. "Война вокруг точек с запятой",курса "Основы объектно-ориентированного проектирования")
В подпрограммах класса POINT все инструкции являются присваиваниями значений. В данной нотации для обозначения присваивания используется символ ":=" также следуя соглашениям, принятым в Algol и Pascal. Этот символ нельзя перепутать с символом равенства "=", применяемым, как и в математике, в операциях сравнения.
Другое соглашение о нотации касается использования комментария к заголовку подпрограммы. Уже отмечалось, что комментарии начинаются с двух последовательных тире "--". Они могут размещаться в любом месте, где, по мнению автора, дополнительные разъяснения могут принести пользу. Особую роль играет комментарий к заголовку (header comment). В соответствии с общим стилевым правилом он должен помещаться в начале каждой подпрограммы после ключевого слова is с отступом как в примере класса POINT. Комментарий к заголовку должен кратко отражать назначение подпрограммы.
Атрибуты также сопровождаются комментариями, следующими непосредственно за их объявлением и имеющими тот же отступ, что и комментарии к заголовку подпрограмм. Иллюстрацией могут служить объявления x и y.
В начале нашего класса помещено предложение, начинающееся с ключевого слова indexing. Оно содержит единственный пункт, помеченный как description. Предложение indexing не оказывает влияния на выполнение программ и служит для размещения информации, ассоциированной с классом. В общем случае оно содержит ноль или более пунктов вида
index_word: index_value, index_value, ...
где index_word - произвольный идентификатор (элемент индексирования), а каждое значение index_value - произвольный элемент языка (идентификатор, целое число, строка и т.д.) (См. "Заметки об indexing",).
Это дает два преимущества:
Приведенный пример содержит единственный индексный элемент - description, значение которого - строка, описывающая назначение класса. Все примеры классов в данной книге будут также содержать элемент description. Настоятельно рекомендуется следовать этому примеру и начинать исходный текст любого класса с предложения indexing, дающего краткую характеристику класса по аналогии с тем, как каждая подпрограмма начинается с комментария к заголовку.
Предложения indexing и комментарии к заголовку являются иллюстрацией правильного применения принципа самодокументирования (Self-Documentation principle): везде, где это возможно, документация модуля должна размещаться непосредственно в самом модуле. (См. "Самодокументирование",)
Для понимания текстов функций rho, theta и distance в классе POINT необходимо еще одно соглашение.
Любой язык программирования, поддерживающий функции (подпрограммы, возвращающие результат) должен предусматривать нотацию, позволяющую установить в теле функции значение, возвращаемое в результате ее вызова. В качестве значения, возвращаемого функцией, в данной книге будет использоваться предопределенная сущность (entity) Result. (Полное определение сущности будет дано в конце этой лекции.)
Например, тело функции rho содержит следующее присваивание
Result := sqrt (x^2 + y^2)
Result - зарезервированное слово, которое может присутствовать только в теле функций. В функции, возвращающей результат типа T, Result рассматривается наряду с другими сущностями и ему может быть присвоено значение с помощью инструкций присваивания, как это показано выше.
При любом вызове функции в качестве результата будет возвращаться последнее присвоенное Result значение. Оно всегда определено благодаря правилам языка (они будут детально рассмотрены позже), требующим обязательной инициализации Result в начале каждой подпрограммы путем присваивания значения, предопределенного типом T. Для типа данных REAL инициализирующее значение равно нулю и следующая функция:
non_negative_value (x: REAL): REAL is
-- Возвращает значение аргумента при x>0; ноль при x<=0
do
if x > 0.0 then
Result := x
end
end
будет всегда возвращать вполне определенное значение (как указано в комментарии к заголовку), несмотря на то, что условная инструкция не содержит части else.
Дискуссия в конце данной лекции обсуждает логику использования соглашения Result в сопоставлении с другими приемами, такими как инструкции возврата. Хотя это соглашение касается всех языков программирования, оно является особенно важным при ОО-подходе.
Исходные тексты классов в данной книге строго подчиняются основным правилам стиля. Они регламентируют отступы, шрифты, выбор имен классов и их компонент, использование нижнего и верхнего регистров.
Далее этим правилам будет уделяться серьезное внимание, а их подробному обсуждению полностью посвящена лекция 8 курса "Основы объектно-ориентированного проектирования". Правила стиля не следует рассматривать как "косметическое" средство. Разработка качественного ПО требует последовательности и внимания ко всем деталям, - к форме в той же степени, что и к содержанию. Задача повторного использования делает соблюдение этих правил еще более важным, поскольку предполагается, что исходные тексты ждет долгая жизнь, в течение которой многие люди будут в них разбираться и развивать их.
Следует правильно применять правила стиля с самого начала написания исходного текста класса. Так, никогда не следует начинать подпрограмму, не задав комментарий к заголовку. Это не займет много времени и это время нельзя считать потерянным. Фактически достигается существенная экономия времени при дальнейшей работе с этим классом его автором или другими программистами, возможно, через полчаса, скорее, через пять лет. Использование одинаковых отступов, грамотное написание комментариев и выбор идентификаторов, применение адекватных лексических соглашений (пробел перед каждой открывающей скобкой, но не после нее и т. д.) не слишком усложнят задачу, но сделают более совершенным результат многомесячного труда над громадой исходных текстов. Внимание к деталям, безусловно, не достаточное, но необходимое условие разработки качественного ПО.
Элементарные правила стиля совершенно понятны из приведенного примера класса. Поскольку целью настоящего раздела является изучение базовых механизмов объектной технологии, то детальному описанию правил стиля будет посвящена одна из последующих лекций (курса "Основы объектно-ориентированного проектирования").
Другим аспектом класса POINT, требующим разъяснений, является присутствие в функциях rho и distance вызовов функции sqrt. Понятно, что эта функция возвращает квадратный корень действительного числа, но откуда она появилась?
Поскольку загромождать универсальный язык специализированными арифметическими операциями нецелесообразно, наилучшим решением будет определение подобных операций как компонентов некоторого специализированного класса, который называется, например, ARITHMETIC. Далее любой класс, в котором необходимо использовать указанные возможности, нужно просто объявить потомком этого специализированного класса. Для этого достаточно переписать класс POINT следующим образом
class POINT inherit
ARITHMETIC
feature
... Остальная часть кода без изменений ...
end
Эта методика наследования функциональных возможностей общего характера является до некоторой степени спорной. Кто-то может полагать, что принципы ОО-подразумевают включение функций типа sqrt в качестве компонентов класса, которому принадлежит объект, например, REAL. Однако существует ряд операций с действительными числами, не все из которых стоит включать в данный класс. В дискуссии о принципах дизайна мы вернемся к вопросу о полезности "вспомогательных" классов, таких как ARITHMETIC. (См. "Наследование функциональных возможностей",курса "Основы объектно-ориентированного проектирования".) |
Обратимся теперь к фундаментальным свойствам класса POINT и попытаемся понять, как устроено типичное тело подпрограммы и составляющие его инструкции. Далее выясним, каким образом класс и его компоненты могут использоваться другими классами - клиентами данного.
Обратимся опять к тексту одной из подпрограмм, процедуре translate:
translate (a, b: REAL) is
-- Перемещение на a по горизонтали, b по вертикали
do
x:= x + a
y:= y + b
end
На первый взгляд этот текст совершенно понятен - для перемещения точки на расстояние a по горизонтали и b по вертикали значение a прибавляется к x, а b к y. При более внимательном рассмотрении все становится не столь очевидным. Из приведенного текста непонятно, о какой точке идет речь. Какому объекту принадлежат x и y, к которым прибавляются a и b? Этот вопрос связан с одним из наиболее характерных аспектов ОО-стиля разработки. Прежде чем получить ответ, следует разобраться в некоторых промежуточных деталях.
Текст класса описывает свойства и поведение объектов определенного типа, в данном случае точек. Это достигается путем описания свойств и поведения типичного экземпляра такого типа. Можно было бы назвать этот экземпляр "точкой на улице" по примеру того, как газеты представляют мнение "человека с улицы". Мы будем использовать более формальное имя - текущий экземпляр класса.
Иногда возникает необходимость явного обращения к текущему экземпляру. Зарезервированное слово
Current
обеспечивает эту возможность. В тексте класса Current обозначает текущий экземпляр этого класса. Потребность в использовании Current может возникнуть, если попытаться переписать функцию distance таким образом, чтобы осуществлялась проверка, не совпадает ли аргумент p с текущей точкой; в этом случае результат равнялся бы нулю без последующих вычислений. Эта версия distance будет выглядеть следующим образом:
distance (p: POINT): REAL is
-- Расстояние до точки p
do
if p /= Current then
Result := sqrt ((x - p.x)^2 + (y- p.y)^2)
end
end
Здесь /= операция неравенства. В соответствии с сформулированным ранее правилом инициализации условная инструкция не нуждается в части else, поскольку результат равен нулю при p = Current.
Тем не менее, в большинстве случаев текущий экземпляр подразумевается, и нет необходимости обращаться к Current по имени. Так ссылка на x в теле translate и других подпрограмм обозначает "значение x текущего экземпляра" без дополнительного уточнения.
Конечно, по-прежнему остается загадкой, кто же он - "Current"? Ответ придет позже при изучении вызовов подпрограмм, пока же при рассмотрении текста достаточно полагать, что все операции можно рассматривать только относительно некоторого неявно определенного объекта - текущего экземпляра.
Игнорируя ряд моментов, связанных с загадкой идентификации Current, можно считать выясненным, как определять простые классы. Теперь необходимо обсудить применение этих определений, - как они используются в других классах. При последовательном ОО-подходе каждый программный элемент является частью некоторого класса, поэтому использовать эти определения будут другие классы.
Существуют лишь две возможности использования класса, например, POINT. Первый способ - наследование будет детально рассмотрен позднее. Для реализации второй возможности необходимо создать класс, являющийся клиентом (client) класса POINT. (Наследованию посвящены лекции 14-16.)
Чтобы стать клиентом класса S, простейший и наиболее общий путь - объявить сущность типа S.
Определение: клиент, поставщик
Пусть S некоторый класс. Класс C называется клиентом (client) S, если содержит объявление сущности a: S. Класс S называется поставщиком (supplier) C.
В этом определении a может быть атрибутом или функцией класса C, или локальной сущностью, или аргументом подпрограммы в классе C.
Например, наличие в классе POINT объявлений x, y, rho, theta и distance делает этот класс клиентом класса REAL. Напротив, другие классы могут стать клиентами POINT. Например:
class GRAPHICS feature
p1: POINT
...
some_routine is
-- Выполнение неких действий с p1.
do
... Создание экземпляра POINT и присоединение его к p1 ...
p1.translate (4.0, -1.5) --**
...
end
...
end
Перед выполнением инструкции помеченной "--**" атрибут p1 принимает значение, соответствующее конкретному экземпляру класса POINT. Предположим, что этот объект представляет точку, совпадающую с началом координат x = 0, y = 0:
В таких случаях говорят, что сущность p1 присоединена (attached) к данному объекту (объект связан с сущностью). На данном этапе можно не беспокоиться о том, как был создан и инициализирован объект (строка "... Создание экземпляра POINT ..." до конца не раскрыта). В следующей лекции эти вопросы будут подробно обсуждаться как часть объектной модели. Пока достаточно знать, что объект существует и связан с сущностью p1 (она присоединена к объекту).
Отмеченная звездочками инструкция
p1.translate (4.0, -1.5)
заслуживает внимательного изучения, поскольку представляет собой первый пример использования базового механизма ОО-вычислений (basic mechanism of object-oriented computation). Это обращение к компоненту или вызов компонента (feature call). В процессе выполнения кода ОО-системы все вычисления реализуются путем вызова соответствующих компонентов определенных объектов.
Приведенный конкретный пример означает вызов компонента translate класса POINT применительно к объекту p1 с аргументами 4.0 и -1.5, соответствующими a и b в объявлении translate в указанном классе. В общем случае допустимы две основные формы записи вызова компонента.
x.f
x.f (u, v, ...)
Здесь x называется целью (target) вызова и может быть сущностью или выражением, которые во время выполнения присоединены к конкретному объекту. Цель x, как любая сущность или выражение, имеет определенный тип, заданный классом C, следовательно, f должен быть одним из компонентов класса C. Точнее говоря, в первом случае f должен быть атрибутом или подпрограммой без аргументов, а во втором - подпрограммой с аргументами. Значения u, v, ... называются фактическими аргументами (actual arguments) вызова и они должны быть выражениями, число и тип которых должны в точности соответствовать числу и типу формальных аргументов (formal arguments) объявленных для f в классе C.
Кроме того, компонент f должен быть доступен (экспортирован) клиенту, содержащему данный вызов. Ограничению прав доступа посвящен следующий раздел , пока по умолчанию все компоненты доступны всем клиентам.
Результат рассмотренного выше вызова во время выполнения определяется следующим образом:
Эффект вызова компонента f для цели x
Применить компонент f к объекту, присоединенному к x, после инициализации всех формальных аргументов f (если таковые предусмотрены) значениями соответствующих фактических аргументов.
Чем так замечателен вызов компонента? В конце концов, каждый программист знает, как написать процедуру translate, которая перемещает точку на заданное расстояние. Традиционная форма вызова, доступная с незначительными вариациями во всех языках программирования, будет выглядеть следующим образом:
translate (p1, 4.0, -1.5)
В отличие от ОО-стиля в данном вызове все аргументы равноправны. Объектно-ориентированная форма не столь симметрична, определенный объект (в данном случае точка p1) выбирается в качестве цели, другим аргументам (действительные числа 4.0 и -1.5) отводится вспомогательная роль. Выбор единственного объекта в качестве цели для каждого вызова занимает центральное место в ОО-методе вычислений.
Принцип единственности цели
Каждая операция при ОО-вычислениях связана с определенным объектом - текущим экземпляром на момент выполнения операции
Этот аспект метода часто вызывает наибольшие затруднения у новичков. При разработке объектно-ориентированного ПО никогда не говорят: "Применение данной операции к этим объектам", но "Применение данной операции к данному объекту в данный момент". Если предусмотрены аргументы, то возможно такое дополнение: "Между прочим, я едва не забыл, вам необходимы здесь эти значения в качестве аргументов".
Принцип единственности цели является прямым следствием слияния понятий модуля и типа, рассмотренного ранее в качестве отправной точки ОО-декомпозиции. Поскольку каждый модуль является типом, каждая операция в данном модуле рассматривается относительно конкретного экземпляра данного типа (текущего экземпляра). Однако до сих пор детали этого слияния оставались немного загадочными. Как уже было сказано, класс одновременно представляет собой модуль и тип, но как согласовать синтаксическое понятие модуля (объединение родственных функциональных возможностей, формирование части программной системы) с семантическим понятием типа (статическое описание неких возможных объектов времени выполнения). Пример класса POINT дает определенный ответ:
Как функционирует слияние модуль-тип
Функциональные возможности класса POINT, рассматриваемого как модуль, в точности соответствуют операциям доступным для экземпляров класса POINT, рассматриваемого как тип
Эта идентификация операций экземпляров типа и служб (services), предоставляемых модулем, лежит в основе структурной дисциплины, навязываемой ОО-методом.
Теперь настало время с помощью того же примера раскрыть тайну текущего экземпляра и выяснить, что он собой представляет в действительности.
Сама форма вызова показывает, почему текст подпрограммы (translate в классе POINT) не нуждается в дополнительной идентификации объекта Current. Поскольку любой вызов подпрограммы связан с определенной целью, которая явно обозначена при вызове, то при выполнении вызова имя каждого компонента в тексте подпрограммы (например, x в тексте translate) будет присоединено к той же цели. Таким образом, при выполнении вызова
p1.translate (4.0, -1.5)
каждое вхождение x в тело translate, как в следующей инструкции
x := x + a
означает: "x объекта p1".
Из этих соображений следует точный смысл понятия Current, как цели текущего вызова. Так в течение всего времени выполнения приведенного выше вызова Current будет обозначать объект, присоединенный к p1. При другом вызове Current будет обозначать цель нового вызова. Можно сформулировать следующий принцип вызова компонет (Feature Call principle):
Принцип вызова компонента
Выше было отмечено, что ОО-вычисления основаны на вызове компонентов. Как следствие этого положения исходные тексты в действительности содержат гораздо больше вызовов, чем может показаться на первый взгляд. До сих пор рассматривались две формы вызовов:
x.f
x.f (u, v, ...)
Подобные вызовы используют так называемую точечную нотацию и их называют квалифицированными (qualified), так как точно указана цель вызова, идентификатор которой расположен перед точкой.
Однако другие вызовы могут быть неквалифицированны, поскольку их цель не указана. В качестве примера предположим, что необходимо в класс POINT добавить процедуру transform, которая будет комбинацией процедур translate и scale точки. Текст такой процедуры может обращаться к процедурам translate и scale:
transform (a, b, factor: REAL) is
-- Сместиться на a по горизонтали, на b по вертикали,
-- затем изменить расстояние до начала координат в factor раз.
do
translate (a, b)
scale (factor)
end
Тело процедуры содержит вызовы translate и scale. В отличие от предыдущих примеров здесь не указана точная цель и не применяется точечная нотация. Такие вызовы называют неквалифицированными (unqualified).
Неквалифицированные вызовы не нарушают пункта F2 принципа вызова компонент, так как тоже имеют цель. В данном случае целью является текущий экземпляр. Когда процедура transform вызывается по отношению к определенной цели, вызовы translate и scale имеют ту же цель. Фактически приведенный выше код эквивалентен следующему
do
Current.translate (a, b)
Current.scale (factor)
Можно переписать любой вызов как квалифицированный, указав Current в качестве цели (строго говоря, это справедливо только для экспортированных компонент). Форма неквалифицированного вызова конечно проще и вполне понятна.
Приведенные неквалифицированные вызовы являются вызовами процедур. Аналогичные соображения можно распространить и на атрибуты, хотя наличие вызовов в этом случае возможно менее очевидно. Ранее было отмечено, что в теле процедуры translate присутствие x в выражении x + a означает поле x текущего экземпляра. Можно истолковать это иначе - как вызов компонента x и выражение в полной форме примет вид Current.x+a.
В общем случае любые инструкции или выражения вида:
f
или:
f (u, v, ...)
фактически являются неквалифицированными вызовами и могут быть переписаны в форме квалифицированных вызовов:
Current.f
Current.f (u, v, ...)
хотя неквалифицированная форма является более удобной. Если подобная нотация используется как инструкция, то f представляет процедуру (без параметров в первом случае или с соответствующим числом параметров определенного типа - во втором). В выражениях f может быть функцией или атрибутом (в первом варианте записи).
Рассмотрение выражения:
x + a
приводит к важному понятию компонента-операции (operator feature). Это понятие может восприниматься как чисто косметическое, имеющее только синтаксическую значимость, и реально не вносящее ничего нового в ОО-метод. Но именно такие синтаксические свойства способны существенно облегчить жизнь разработчика, если они существуют, и сделать ее убогой, если их нет. Компоненты-операции являются хорошим примером успешного использования ОО-парадигмы в давно известных областях.
Для реализации этой идеи нужно догадаться, что выражение x + a содержит не один вызов (компонента x), а два. В вычислениях, не использующих объектный подход, + рассматривается как операция сложения двух значений x и a типа REAL. Как уже отмечалось, в чистой ОО-модели единственным механизмом вычислений является вызов компонентов. Следовательно, можно считать, по крайней мере теоретически, что и сложение является вызовом соответствующего компонента.
Для лучшего понимания необходимо обсудить определение типа REAL. Сформулированное ранее объектное правило (лекция 7) подразумевает, что каждый тип основан на каком-то классе. Это в равной мере относится к предопределенным классам, аналогичным REAL, и к классам, определенным разработчиком, таким как POINT. Предположим, что необходимо описать REAL как класс. Нетрудно определить набор существенных компонентов: арифметические операции (сложение, вычитание, изменение знака...), операции сравнения (меньше чем, больше чем...). Итак, первый набросок будет выглядеть так:
indexing
description: "Действительные числа (не окончательная версия!)"
class REAL feature
plus (other: REAL): REAL is
-- Сумма текущего значения и other
do
...
end
minus (other: REAL) REAL is
-- Разность между текущим значением и other
do
...
end
negated: REAL is
-- Текущее значение, взятое с противоположным знаком
do
...
end
less_than (other: REAL): BOOLEAN is
-- Текущее значение меньше чем other?
do
...
end
... Другие компоненты ...
end
При использовании такого описания класса уже нельзя более записывать арифметическое выражение в виде: x + a. Вместо этого надо использовать следующий вызов:
x.plus (a)
По аналогии, вместо привычного -x следует теперь писать x.negated.
Можно попытаться оправдать такой отход от привычной математической нотации стремлением к последовательной реализации ОО-модели и призвать в качестве примера Lisp для обоснования возможности отхода от стандартной нотации в сообществе разработчиков ПО. Но такой аргумент нельзя считать убедительным: использование Lisp было всегда весьма ограниченным. Отход от нотации, существующей уже много столетий и знакомой всем с начальной школы, чрезвычайно опасен. Тем более что в этой нотации нет ничего неправильного.
Простой синтаксический прием позволяет сохранить последовательность подхода (требование унификации вычислительного механизма, основанного на вызове компонент) и обеспечивает совместимость с традиционной нотацией. Достаточно рассматривать выражение вида
x + a
как вызов дополнительного компонента класса REAL. Для реализации такого подхода необходимо переписать компоненту plus таким образом, чтобы для ее вызовов использовать знак операции, а не точечную нотацию. Вот описание класса, реализующее эту цель:
indexing
description: "Real numbers"
class REAL feature
infix "+" (other: REAL): REAL is
-- Сумма текущего значения и other
do
...
end
infix "-" (other: REAL) REAL is
-- Разность между текущим значением и other
do
...
end
prefix "-": REAL is
-- Текущее значение, взятое с противоположным знаком
do
...
end
infix "<" (other: REAL): BOOLEAN is
-- Текущее значение меньше чем other?
do
...
end
... Other features ...
end
Введены два новых ключевых слова - infix и prefix. Единственное синтаксическое новшество заключается в том, что имена компонент не являются идентификаторами (такими как distance или plus), а записываются в одной из двух форм (В следующей лекции будет показано, как определить "развернутый класс". См. "Роль развернутых типов".)
infix "§"
prefix "§"
где § заменяется конкретным знаком операции (+, -, *, <, <= и др.). Компонент может иметь имя в инфиксной форме только если является функцией с одним аргументом, примерами могут служить plus, minus и less_than в первоначальной версии класса REAL. Префиксная форма может использоваться только для функций без аргументов или атрибутов.
Инфиксные и префиксные компоненты, называемые далее компоненты-операции (operator features), используются аналогично именованным компонентам (identifier features). Существуют лишь два синтаксических различия. Для имен компонентов-операций при их объявлении используются формы infix "§" или prefix "§", а не идентификаторы. Вызов компонентов-операций в случае инфиксных компонент имеет вид:
u § v
для префиксных:
§ u
Компоненты-операции поддерживают только квалифицированные вызовы. Неквалифицированный вызов plus (y) в подпрограмме первой версии класса REAL во второй версии должен быть записан в виде Current + y. Для именованных компонентов аналогичная нотация Current.plus (y) допустима, но обычно не используется.
Кроме указанных отличий во всем остальном компоненты-операции полностью синтаксически эквиваленты именованным компонентам, в частности могут наследоваться обычным образом. Не только базовые классы аналогичные REAL, но и любые другие, могут использовать компоненты-операции, например для функции сложения двух векторов в классе VECTOR вполне допустимо использовать инфиксную компоненту "+".
Операции, используемые в компонентах-операциях, должны подчиняться следующим правилам. Знак операции - последовательность из одного или более отображаемых символов, не содержащая пробелов и переводов строки, причем первым символом может быть только один из ниже перечисленных:
+ - a / < > = \ ^ @ # | &
Ограничения, налагаемые на первый символ, облегчают распознавание инфиксных и префиксных операций.
Кроме того, для совместимости с традиционной нотацией для булевых выражений следующие ключевые слова используются для обозначения операций:
not and or xor and then or else implies
Базовые классы (INTEGER и другие) используют так называемые стандартные операции:
Здесь // обозначает целочисленное деление, \\ - остаток при целочисленном делении, ^ - операцию возведения в степень, xor - исключающее "или". В классе BOOLEAN and then и or else являются вариантами and и or (отличия обсуждаются далее), implies обозначает импликацию: выражение a implies b эквивалентно ( not a ) or else b . |
Операции, не входящие в число "стандартных", называют свободными операциями. Приведем два примера свободных операций.
Все операции имеют фиксированный приоритет, стандартные операции имеют свой обычный приоритет, а все свободные операции обладают более высоким приоритетом.
Использование компонентов-операций позволяет использовать общепринятую нотацию для выражений и одновременно отвечает требованиям полной унификации системы типов. Реализация арифметических и булевых операций как компонентов класса INTEGER вовсе не должна быть причиной снижения производительности. Концептуально a + x является вызовом компонента, но хороший компилятор может создать в результате обработки такого вызова код не менее эффективный, чем компиляторы C, Pascal, Ada или других языков, в которых "+" это жестко зафиксированная языковая конструкция.
В большинстве случаев мы можем забыть о том, что использование операций в выражениях фактически является вызовом процедур, поскольку конечный эффект будет таким же, как и при традиционном подходе. В то же время приятно сознавать, что и в этом случае не допущено отхода от принципов ОО-подхода.