Основы объектно-ориентированного программирования - Бертран Мейер
Шрифт:
Интервал:
Закладка:
Результат рассмотренного выше вызова во время выполнения определяется следующим образом:
Эффект вызова компонента f для цели x
Применить компонент f к объекту, присоединенному к x, после инициализации всех формальных аргументов f (если таковые предусмотрены) значениями соответствующих фактических аргументов.
Принцип единственности цели
Чем так замечателен вызов компонента? В конце концов, каждый программист знает, как написать процедуру translate, которая перемещает точку на заданное расстояние. Традиционная форма вызова, доступная с незначительными вариациями во всех языках программирования, будет выглядеть следующим образом:
translate (p1, 4.0, -1.5)
В отличие от ОО-стиля в данном вызове все аргументы равноправны. Объектно-ориентированная форма не столь симметрична, определенный объект (в данном случае точка p1) выбирается в качестве цели, другим аргументам (действительные числа 4.0 и -1.5) отводится вспомогательная роль. Выбор единственного объекта в качестве цели для каждого вызова занимает центральное место в ОО-методе вычислений.
Принцип единственности цели
Каждая операция при ОО-вычислениях связана с определенным объектом - текущим экземпляром на момент выполнения операции
Этот аспект метода часто вызывает наибольшие затруднения у новичков. При разработке объектно-ориентированного ПО никогда не говорят: "Применение данной операции к этим объектам", но "Применение данной операции к данному объекту в данный момент". Если предусмотрены аргументы, то возможно такое дополнение: "Между прочим, я едва не забыл, вам необходимы здесь эти значения в качестве аргументов".
Слияние понятий модуль и тип
Принцип единственности цели является прямым следствием слияния понятий модуля и типа, рассмотренного ранее в качестве отправной точки ОО-декомпозиции. Поскольку каждый модуль является типом, каждая операция в данном модуле рассматривается относительно конкретного экземпляра данного типа (текущего экземпляра). Однако до сих пор детали этого слияния оставались немного загадочными. Как уже было сказано, класс одновременно представляет собой модуль и тип, но как согласовать синтаксическое понятие модуля (объединение родственных функциональных возможностей, формирование части программной системы) с семантическим понятием типа (статическое описание неких возможных объектов времени выполнения). Пример класса POINT дает определенный ответ:
Как функционирует слияние модуль-тип
Функциональные возможности класса POINT, рассматриваемого как модуль, в точности соответствуют операциям доступным для экземпляров класса POINT, рассматриваемого как тип
Эта идентификация операций экземпляров типа и служб (services), предоставляемых модулем, лежит в основе структурной дисциплины, навязываемой ОО-методом.
Роль объекта Current
Теперь настало время с помощью того же примера раскрыть тайну текущего экземпляра и выяснить, что он собой представляет в действительности.
Сама форма вызова показывает, почему текст подпрограммы (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]. (F1) Любой элемент программы может выполняться только как часть вызова подпрограммы.
[x]. (F2) Каждый вызов имеет цель.
Квалифицированные и неквалифицированные вызовы
Выше было отмечено, что ОО-вычисления основаны на вызове компонентов. Как следствие этого положения исходные тексты в действительности содержат гораздо больше вызовов, чем может показаться на первый взгляд. До сих пор рассматривались две формы вызовов:
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. Как уже отмечалось, в чистой ОО-модели единственным механизмом вычислений является вызов компонентов. Следовательно, можно считать, по крайней мере теоретически, что и сложение является вызовом соответствующего компонента.