учебники, программирование, основы, введение в,

 

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

Классы, а не объекты - предмет обсуждения
Какова центральная концепция объектной технологии? Необходимо дважды подумать, прежде чем ответить "объект". Объекты полезны, но в них нет ничего нового.
С тех пор, как структуры используются в Cobol, с тех пор, как в Pascal существуют записи, с тех пор как программист написал на C первое определение структуры, человечество располагает объектами.
Объекты важны при описании выполнения ОО-систем. Но базовым понятием объектной технологии является класс. Обратимся вновь к его определению. (Детальное обсуждение объектов содержится в следующей лекции.)
Определение класса
Класс - это абстрактный тип данных, поставляемый с возможно частичной реализацией.
Абстрактные типы данных (АТД) являются математическим понятием, пригодным на этапе подготовки спецификации - в процессе анализа. Понятие класса, предусматривая частичную или полную реализацию, обеспечивает необходимую связь с разработкой ПО на этапах проектирования и программирования. Напомним, класс называется эффективным, если его реализация полна, и отложенным - при частичной реализации.
Аналогично АТД, класс это тип, описывающий множество возможных структур данных, называемых экземплярами (instances) класса. Экземпляры АТД являются абстракциями - элементами математического множества. Экземпляр класса конкретен - это структура данных, размещаемая в памяти компьютера и обрабатываемая программой.
Например, если определить класс STACK, взяв за основу спецификацию АТД из предыдущей лекции и добавив информацию, необходимую для адекватного представления, то экземплярами класса будут структуры данных - конкретные стеки. Другим примером является класс POINT, моделирующий точку на плоскости. Если для представления точки выбрана декартова система координат, то каждый экземпляр POINT представляет собой запись с полями x, y - абсциссой точки и ее ординатой.
Термин "объект" появляется как побочный продукт определения "класса". Объект это просто экземпляр некоторого класса.
Программные тексты, описывающие создаваемую систему, содержат определения классов. Объекты создаются только в процессе выполнения программ.
Настоящая лекция посвящена основным приемам создания программных элементов и объединения их в системы, именно поэтому в центре внимания - классы. В следующей лекции будут рассмотрены структуры периода выполнения, порождаемые ОО-системой, что потребует изучения некоторых особенностей реализации и более детального рассмотрения природы объектов.
Устранение традиционной путаницы
Класс это модель, а объект экземпляр такой модели. Эта особенность настолько очевидна, что обычно не требует дополнительных комментариев. Тем не менее, в определенной категории специальной литературы имеет место весьма небрежное обращение с этими понятиями, - смешиваются понятие отдельного объекта и концепция объектов в целом, которую характеризует класс. У этой путаницы два источника. Один - возникает из-за широкого толкования термина "объект" в естественном языке. Другой источник недоразумений связан с метаклассами, - с ситуациями, когда классы сами выступают в роли объектов. Классическим примером может служить транслятор объектного языка, для которого классы языка являются объектами трансляции.
Некоторые ОО-языки, особенно Smalltalk, для выхода из рассмотренной ситуации используют понятие метакласс (metaclass). Метакласс - это класс, экземпляры которого сами являются классами. В романе "Имя Розы", отрывок из которого приведен в эпиграфе к данной лекции, встречается понятие "знаки знаков". По сути, это и есть неформальное определение метаклассов.
Мы будем избегать введения метаклассов, поскольку создаваемых ими проблем больше, чем тех, которые они решают. В частности, введение метаклассов создает трудности при проведении статической проверки типов, что является необходимым условием разработки надежного ПО. Основные функции метаклассов могут быть гораздо лучше реализованы с помощью других средств:

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

В данной книге не используется самостоятельная концепция метакласса. Присутствие метаклассов в том или ином языке или среде разработки не оправдывает смешение понятий моделей и их экземпляров - классов и объектов.

Роль классов

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

Модули и типы

Средства, используемые при разработке ПО, - языки программирования, проектирования, спецификаций, графические системы обозначений для анализа, - всегда включали в себя как возможность применения модулей, так и систему типов.
Модули - это структурные единицы, из которых состоит программа. Различные виды модулей, такие как подпрограммы и пакеты, рассматривались в одной из предыдущих лекций Независимо от конкретного выбора той или иной модульной структуры, модуль всегда рассматривается как синтаксическая концепция. Отсюда следует, что разбиение на модули влияет лишь на форму записи исходных текстов программ, но не определяет их функциональность. В самом деле, принципиально можно написать программу Ada в виде единственного пакета, или программу Pascal как единую основную программу. Безусловно, такой подход не рекомендуется, и любой компетентный программист будет использовать модульные возможности языка для деления программы на обозримые и управляемые части. Но если взять существующую программу, например на Паскале, то всегда можно собрать воедино все модули и получить работоспособную программу с эквивалентной семантикой. (Присутствие рекурсивных подпрограмм делает этот процесс менее тривиальным, но не оказывает принципиального влияния на данную дискуссию.) Таким образом, деление на модули диктуется принципами управления проектами, а не внутренней необходимостью.
Концепция типов на первый взгляд совершенно иная. Тип является статическим описанием вполне определенных динамических объектов - элементов данных, которые обрабатываются во время выполнения программной системы. Набор типов обычно содержит предопределенные типы, такие как INTEGER или CHARACTER, а также пользовательские типы: записи (структуры), указатели, множества (в Pascal), массивы и другие. Понятие типа является семантической концепцией, и каждый тип непосредственно влияет на выполнение программной системы, так как описывает форму объектов, которые система создает и которыми она манипулирует.

Класс как модуль и как тип

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

Унифицированная система типов

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

  • Всегда желательно иметь простую и универсальную схему, нежели множество частных случаев. Предлагаемая система типов полностью опирается на понятие класса.
  • Описание базовых типов как абстрактных структур данных и далее как классов является простым и естественным. Нетрудно представить, например, определение класса INTEGER с функциональностью включающей арифметические операции, такие как "+", операции сравнения, такие как ""=" и ассоциированные свойства, следующие из соответствующих математических аксиом.
  • Определение базовых типов как классов позволяет использовать все возможности ОО, главным образом наследование и родовые средства. Если базовые типы не будут классами, то придется вводить ряд ограничений и рассматривать частные случаи.

Пример наследования - классы 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: POINT  REAL
translate: POINT ×  REAL ×  REAL  POINT

и одна из аксиом:

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θ, соответственно).
Приведенный пример указывает на необходимость рассмотрения компонентов двух видов:

  • Некоторые компоненты представлены в пространстве и, можно сказать, ассоциируются с некоторой частью информации каждого экземпляра класса. Они называются атрибутами (attributes). Для точки, представленной в декартовых координатах, атрибутами являются x и y, а в полярных координатах в роли атрибутов выступают rho и theta.
  • Другие компоненты представлены во времени, и для доступа к ним требуется описать некоторые вычисления (алгоритмы), применимые далее ко всем экземплярам данного класса. В дальнейшем они называются подпрограммами или методами класса (routines). В декартовом представлении точек - rho и theta это подпрограммы, а x и y выступают в качестве подпрограмм при использования полярных координат.

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

Ниже приведена версия исходного текста класса 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

В начале нашего класса помещено предложение, начинающееся с ключевого слова indexing. Оно содержит единственный пункт, помеченный как description. Предложение indexing не оказывает влияния на выполнение программ и служит для размещения информации, ассоциированной с классом. В общем случае оно содержит ноль или более пунктов вида

index_word: index_value, index_value, ...

где index_word - произвольный идентификатор (элемент индексирования), а каждое значение index_value - произвольный элемент языка (идентификатор, целое число, строка и т.д.) (См. "Заметки об indexing",).
Это дает два преимущества:

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

Приведенный пример содержит единственный индексный элемент - 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 в сопоставлении с другими приемами, такими как инструкции возврата. Хотя это соглашение касается всех языков программирования, оно является особенно важным при ОО-подходе.

Правила стиля

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

Наследование функциональных возможностей общего характера

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

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

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика