Объектно-ориентированный стиль вычислений - 7. Статические структуры: классы

Лекция



Это продолжение увлекательной статьи про класс.

...

вызовов функции sqrt. Понятно, что эта функция возвращает квадратный корень действительного числа, но откуда она появилась?

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

class POINT inherit

ARITHMETIC

feature

... Остальная часть кода без изменений ...

end
Эта методика наследования функциональных возможностей общего характера является до некоторой степени спорной. Кто-то может полагать, что принципы ОО-подразумевают включение функций типа sqrt в качестве компонентов класса, которому принадлежит объект, например, REAL . Однако существует ряд операций с действительными числами, не все из которых стоит включать в данный класс. В дискуссии о принципах дизайна мы вернемся к вопросу о полезности "вспомогательных" классов, таких как ARITHMETIC . (См. "Наследование функциональных возможностей", лекция 6 курса "Основы объектно-ориентированного проектирования".)

Объектно-ориентированный стиль вычислений

Обратимся теперь к фундаментальным свойствам класса POINT и попытаемся понять, как устроено типичное тело подпрограммы и составляющие его инструкции. Об этом говорит сайт https://intellect.icu . Далее выясним, каким образом класс и его компоненты могут использоваться другими классами - клиентами данного.

Текущий экземпляр

Обратимся опять к тексту одной из подпрограмм, процедуре 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:

7. Статические структуры: классы

Рис. 7.6. Начало координат

В таких случаях говорят, что сущность 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), предоставляемых модулем, лежит в основе структурной дисциплины, навязываемой ОО-методом.

Роль объекта 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):

Принцип вызова компонента

  • - (F1) Любой элемент программы может выполняться только как часть вызова подпрограммы.
  • - (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. Как уже отмечалось, в чистой ОО-модели единственным механизмом вычислений является вызов компонентов. Следовательно, можно считать, по крайней мере теоретически, что и сложение является вызовом соответствующего компонента.

Для лучшего понимания необходимо обсудить определение типа REAL. Сформулированное ранее объектное правило () подразумевает, что каждый тип основан на каком-то классе. Это в равной мере относится к предопределенным классам, аналогичным 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 и другие) используют так называемые стандартные операции:

  • - префиксные: + - not
  • - инфиксные: + - a / < > <= >= = // \\ ^ and or xor and then or else implies .
Здесь // обозначает целочисленное деление, \\ - остаток при целочисленном делении, ^ - операцию возведения в степень, xor - исключающее "или". В классе BOOLEAN and then и or else являются вариантами and и or (отличия обсуждаются далее), implies обозначает импликацию: выражение a implies b эквивалентно ( not a ) or else b .

Операции, не входящие в число "стандартных", называют свободными операциями. Приведем два примера свободных операций.

  • - Далее в классе ARRAY будет использован инфиксный компонент-операция "@" для функции, возвращающей указанный элемент массива. Обращение к i-ому элементу массива будет выглядеть как a @ i.
  • - В класс POINT вместо функции distance можно ввести компонент-операцию "|-|" и расстояние между точками p1 and p2 будет записываться в виде p1 |-| p2, а не как p1.distance(p2).

Все операции имеют фиксированный приоритет, стандартные операции имеют свой обычный приоритет, а все свободные операции обладают более высоким приоритетом.

Использование компонентов-операций позволяет использовать общепринятую нотацию для выражений и одновременно отвечает требованиям полной унификации системы типов. Реализация арифметических и булевых операций как компонентов класса INTEGER вовсе не должна быть причиной снижения производительности. Концептуально a + x является вызовом компонента, но хороший компилятор может создать в результате обработки такого вызова код не менее эффективный, чем компиляторы C, Pascal, Ada или других языков, в которых "+" это жестко зафиксированная языковая конструкция.

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

Селективный экспорт и скрытие информации

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

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

Неограниченный доступ

По умолчанию все компоненты доступны для всех клиентов. Для класса

class S1 feature

f ...

g ...

...

end

компоненты f, g, ... доступны всем клиентам S1. Это означает, что если в классе C объявлена сущность x класса S1, то вызов

x.f ...

является допустимым, если выполнены все другие условия корректности вызова f.

Ограничение доступа клиентам

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

class S2 feature

f ...

g ...

feature {A, B}

h ...

...

end

Компоненты f и g по-прежнему доступны всем клиентам. Компонент h доступен только для классов A и B, а также их потомков (прямых или косвенных). Это означает, что для некоторого x типа S2 следующий вызов

x.h

является допустимым только в исходных текстах классов A, B или одного из их потомков.

В особом случае, когда необходимо скрыть компонент i от всех клиентов, можно объявить его экспортируемым пустому списку клиентов (Не рекомендуемый стиль (см. ниже S5).):

class S3 feature { }

i ...

end

В этом случае любой вызов x.i(...) недопустим. Единственная возможность обращения к i - неквалифицированный вызов

i (...)

в тексте подпрограммы класса S3 или его потомков. Такой механизм обеспечивает полное скрытие информации.

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

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

Стиль объявления скрытых компонент

Использованный выше стиль объявления скрытой компоненты i не слишком удачен. Это хорошо видно в следующем примере (Не рекомендуемый стиль (см. ниже S5).)

class S4 feature

exported ...

feature {}

secret ...

end

где secret является скрытой компонентой, а exported - общедоступной. Разница в написании feature {} с пустым списком в скобках и feature без всяких скобок едва заметна. Гораздо разумнее вместо пустого использовать список, содержащий единственный класс NONE (Рекомендуемый стиль.)

class S5 feature

... exported ...

feature {NONE}

... secret ...

end

Класс NONE является базовым библиотечным классом и обсуждается далее в связи с наследованием. По определению он не может иметь потомков и нельзя создать его экземпляр. Таким образом, компонент, экспортированный классу NONE, фактически является скрытым. Между объявлениями S4 и S5 нет принципиальной разницы, однако во втором случае исходный текст становится более понятным и удобочитаемым. Именно такой стиль объявления скрытых компонент будет использоваться далее в этой книге.

"Внутренний" экспорт

Рассмотрим объявление класса

indexing

замечание: "Ошибочное объявление (объяснение см. ниже)"

class S6 feature

x: S6

my_routine is do ... print (x.secret) ... end

feature {NONE}

secret: INTEGER

end -- class S6

Наличие в объявлении класса атрибута x типа S6 и вызова x.secret делает его собственным клиентом. Но такой вызов недопустим, так как компонент secret скрыт от всех клиентов! Тот факт, что неавторизованным клиентом является сам класс S6, нечего не меняет - объявленный статус secret делает недопустимым любой вызов вида x.secret. Всякие исключения нарушают простоту сформулированного правила.

Есть простое решение: написать вместо feature {NONE} предложение feature {S6} , экспортируя компоненту самому себе и своим потомкам.

Необходимо отметить, что подобный прием необходим, только если в тексте класса присутствует квалифицированный вызов аналогичный print (x.secret). Очевидно, что неквалифицированный вызов secret в инструкции print (secret) допустим без дополнительных ухищрений. Все компоненты, объявленные в данном классе, могут использоваться в подпрограммах данного класса и его потомков. Только при наличии квалифицированных вызовов приходится экспортировать компонент самому себе.

Собираем все вместе

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

Общая относительность

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

p1.translate (u, v)

Однако возникает следующий вопрос: что в действительности обозначает p1? Ответ опять относителен. Предположим, приведенный вызов присутствует в тексте некоторого класса GRAPHICS, а p1 это атрибут GRAPHICS. Тогда очевидно, что в этом случае p1 фактически означает Current.p1. Но это не ответ на поставленный вопрос, так как неизвестно, что представляет собой объект Current в момент вызова! Другими словами, теперь необходимо установить клиента, вызывающего подпрограмму класса GRAPHICS, в которой используется наш вызов.

Большой Взрыв

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

  • - (F1) Любой элемент программы может выполняться только как часть вызова подпрограммы.
  • - (F2) Каждый вызов имеет цель.

Любой вызов может принимать одну из следующих форм:

  • - неквалифицированная: f (a, b, ...);
  • - квалифицированная: x.g (u, v, ...) .

Аргументы в обоих случаях могут отсутствовать. Вызов размещен в теле подпрограммы r и может выполняться только как часть вызова r. Предположим, что известна цель этого вызова - некий объект OBJ. Тогда можно легко установить цель этого вызова - t. Возможны четыре варианта, первый из которых относится к неквалифицированному вызову, а остальные - к квалифицированному:

  • - (T1) Для неквалифицированного вызова t это просто OBJ.
  • - (T2) Если x это атрибут, то x - поле объекта OBJ-имеет значение, которое, в свою очередь, присоединено к некоторому объекту - он и есть t.
  • - (T3) Если x - функция, то необходимо сначала осуществить ее вызов (неквалифицированный), результат которого и дает t.
  • - (T4) Если x - локальная сущность r, то к моменту вызова предыдущие инструкции вычислят значение x, присоединенное к определенному объекту, который и является объектом t.

Проблема в том, что все четыре ответа опять относительны и могут помочь только в том случае, если известно, чем является текущий экземпляр OBJ. Очевидно, что OBJ это цель текущего вызова! Ситуация как в песенке о том, как у попа была собака (в оригинале: котенок съел козленка, котенка укусил щенок, щенка ударила палка ...) - бесконечная цепь.

Для приведения относительных ответов к абсолютным необходимо выяснить, что происходит тогда, когда все только начинается - в момент Большого Взрыва. Итак, определение:

Определение: выполнение системы

Выполнение ОО-программной системы состоит из следующих двух шагов:

  • - Создание определенного объекта, называемого корневым объектом выполнения.
  • - Применение определенной процедуры, называемой процедурой создания, к данному объекту.

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

Зная, где все началось, несложно проследить судьбу Current в процессе этой цепной реакции. Первым текущим объектом, созданным в момент Большого Взрыва, является корневой объект. Рассмотрим далее некоторый этап выполнения системы. Пусть r-последняя вызванная подпрограмма, а текущим на момент вызова r был объект OBJ. Тогда во время выполнения r объект Current определяется следующим образом:

  • - (C1) Если в r выполняется инструкция, не являющаяся вызовом подпрограммы (например, присваивание), то текущим остается прежний объект.
  • - (C2) Неквалифицированный вызов также оставляет тот же объект текущим.
  • - (C3) Запуск квалифицированного вызова x.f ... делает текущим объект, присоединенный к x. Зная объект OBJ, можно идентифицировать x, используя сформулированные ранее правила T1-T4. После завершения вызова роль текущего возвращается к объекту OBJ.

В случаях C2 и C3 вызов может в свою очередь содержать последующие квалифицированные или неквалифицированные вызовы, и данные правила

продолжение следует...

Продолжение:


Часть 1 7. Статические структуры: классы
Часть 2 Объектно-ориентированный стиль вычислений - 7. Статические структуры: классы
Часть 3 Программа main отсутствует - 7. Статические структуры: классы
Часть 4 Присваивание функции результата - 7. Статические структуры: классы

См.также

  • ООП
  • Класс
  • Объект
  • Унифицированный доступ
  • принцип проектирования по контракту
  • Требования к сборщику мусора
  • Подход C++ к связыванию
  • Использование утверждений в документации: краткая форма класса
  • Предопределение функции в качестве атрибута
  • Преждевременное упорядочение
  • Универсальные классы
  • Функциональная декомпозиция
  • Роль развернутых типов
  • Наследование функциональных возможностей
  • Самодокументирование
  • Заметки об indexing
  • Война вокруг точек с запятой
  • Категории функций

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

Ответы на вопросы для самопроверки пишите в комментариях, мы проверим, или же задавайте свой вопрос по данной теме.

создано: 2020-07-23
обновлено: 2024-11-14
109



Рейтиг 9 of 10. count vote: 2
Вы довольны ?:


Поделиться:

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выплнения задания
  • Возможно примерение функции гаранта на сделку
  • Приорететная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно примерение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии


Оставить комментарий
Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Объектно-ориентированное программирование ООП

Термины: Объектно-ориентированное программирование ООП