Письмо 02 - Полиморфизм

Полиморфизм подразумевает много форм реализации. Полиморфизм, как правило, рассматривают в тесной связи с виртуализацией. В Письме 01 говорилось о виртуализации параметров и подпрограмм. Можно вернуться и посмотреть, что одно и тоже действие Draw принимало некоторую виртуальную фигуру и производило её рисование (первый пример). Далее было показаны преимущества перехода от виртуализации данных к виртуализации подпрограмм. В этом случае для каждой фигуры объявлялась своя собственная реализация действия Draw. Говоря другими словами, действие Draw стало полиморфным, имея отдельную форму (реализацию) для каждой из фигур.

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

  • спрятать фигуру (Hide);
  • сделать изменения (Change_Value);
  • отобразить фигуру (Draw).

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

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

Вопросы, которые возникают при использовании множественного наследования, не имеют простых ответов. Тем не менее, в данном случае решение очевидно, для того, чтобы увидеть его достаточно сформулировать проблему. Два класса должны обладать одним и тем же свойством (методом), но при этом данное свойство (метод) может отсутствовать у их ближайшего суперкласса. Словосочетание «одним и тем же свойством», в данном случае, тождественно термину полиморфизм. То есть, говоря другими словами, два класса должны обладать полиморфным свойством, которое отсутствует у их ближайшего суперкласса. Отсюда и решение проблемы: полиморфизм – явление самостоятельное и глобальное. Оно не только представляет собой часть механизма наследования, но и может существовать вне иерархии классов.
Этот подход в большей степени соответствует реальности. Например, летать могут птицы, насекомые, рептилии и механические аппараты (самолёты, вертолёты, дирижабли и т.п.). Означает ли это, что все они имеют общего летающего предка? Конечно, нет. Плавать могут не только рыбы, но и животные, и птицы, и суда. Свидетельствует ли это о том, что у их общего суперкласса есть это свойство? Снова, нет. Здесь вполне допустимо рассмотреть и тот пример, который приводится в книге Г. Буча [стр. 74]. Он рассматривает два класса растений: цветы и фрукты-овощи. И отмечает, что некоторые цветы имеют плоды, а некоторые фрукты-овощи имеют цветы, и на основании таких рассуждений приходит к выводу о том, что множественное наследование необходимо. Реально же проблема решается элементарно и без множественного наследования, вполне достаточно сказать, что «иметь цветы» и «иметь плоды» это полиморфные свойства растений.

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

Интересный вопрос: почему такая возможность не была реализована ранее? В объектно-процедурных языках типа C++, OO Pascal и им подобных это связано с тем, что связь между объектами в этих языках осуществляется посредством косвенных вызовов, через виртуальные таблицы (см. Письмо 01). Полиморфизм обеспечивается постоянством индекса входа свойства в родственные виртуальные таблицы. Если всё то же свойство «летать» имело индекс входа в виртуальную таблицу равный, скажем, трём, то оно и оставалось таким, для всех подклассов данного суперкласса. Компилятор, встретив вызов свойства «летать» заменял его инструкцией косвенного вызова подпрограммы, адрес которой третьему индексу входа, реализующей данное свойство у данного класса. Правда, следует отметить, что при множественном наследовании, такая однозначность утрачивалась, что требовало от компиляторов несколько большего «интеллекта».

Данная схема связи не позволяла ввести самостоятельность для полиморфизма, так как терялась однозначность между полиморфным свойством (виртуальным методом) и его точкой входа в виртуальную таблицу. С другой стороны, в момент компиляции не всегда можно сказать, какой конкретно класс будет использоваться в некоторой ситуации. В рамках принятой схемы связи посредством вызовов проблема трудноразрешима. Но, отказ от вызовов подпрограмм в пользу передачи сообщений, серьёзно облегчает решение задачи.

Рассматривая полиморфные свойства, надо вернуться к рассмотрению инкапсуляции. Полиморфные свойства – это не только код, но и необходимые данные. Как отмечалось в Письме 01, подпрограммы могут иметь локальные данные, существующие только во время исполнения подпрограммы, локальные данные существующие постоянно, глобальные данные и параметры, передаваемые при вызове. При переходе к объектной технологии глобальные данные, необходимые для организации взаимодействия обслуживающих и интерфейсных подпрограмм, образуют структуру данных класса. Обслуживающие и интерфейсные подпрограммы преобразуются в свойства класса. Но, вот интересно, что происходит с локальными данными, существующими постоянно? Как отмечалось, эти данные нужны подпрограмме. Если подпрограмма превратилась в полиморфное свойство, которое может принадлежать различным классам, то её локальные данные, существующие постоянно, также принадлежат структурам этих классов. При передаче полиморфного свойства между классами, не связанными узами наследования, структура локальных данных, существующих постоянно, может переопределяться или вообще не использоваться какими-то классами. Но, если передача полиморфных свойств происходит в результате наследования, то структура локальных данных, существующих постоянно, полиморфного свойства может быть только расширена! Таким образом, повышая функциональные возможности класса за счёт присоединения новых полиморфных свойств, возможно и изменение структуры данных класса. При отсутствии инкапсуляции введение такого механизма было бы трудноразрешимой задачей.

Есть ещё один интересный аспект, который надо рассмотреть, допустив самостоятельность полиморфизма. На этапе проектирования очень сложно сказать, какое свойство должно быть полиморфным, поскольку невозможно предвидеть все возможные пути развития среды. Но тогда следует предположить, что полиморфным может быть любое свойство класса. И это правильно. Но тогда зачем нужно наследование? Действительно, имея самостоятельный механизм полиморфизма, можно совершенно произвольно конструировать классы. Однако, это не лучший стиль. Наследование отвечает за передачу классообразующих (общих для данных классов) свойств, структуры и поведения класса. Благодаря наследованию достигаются высокие уровни абстракций. Одного полиморфизма для этого мало.

В сочетании с сообщениями (асинхронными вызовами) полиморфизм свойств даёт ещё одну уникальную возможность, а именно допускает возможность развивать класс, насыщая его новыми свойствами не только на стадии проектирования, но и при эксплуатации. Это кардинально меняет подход к проектированию. Теперь совершенно не обязательно изначально пытаться представить себе полный набор свойств класса. Любое свойство может быть добавлено в любое время и в любое место иерархии. Если класс, к которому добавляют свойство, имеет подклассы, то данное свойство будет распространяться и на подклассы, так будто оно было унаследовано ими. Поэтому на первой стадии вполне достаточно реализовать только минимально необходимую функциональность класса. А далее можно будет добавлять новые свойства, когда в них появится потребность. Такая возможность, во-первых, позволяет быстро собрать прототип системы и отработать основные функции, а, во-вторых, любое увеличение функциональности не повлечёт за собой ни переписывания кода, ни перекомпиляции, что очень важно для поддержания работоспособности существующих свойств. Если бы развитие классов шло традиционным путём, то достичь подобной мощности и гибкости было бы сложно, хотя бы потому, что пришлось бы перестраивать виртуальные таблицы. К сожалению, даже эту простую операцию выполнить без перекомпиляции кода класса нельзя.

Итак, основное достоинство полиморфизма заключается в том, что он позволяет различным классам иметь семантически однородные свойства. Благодаря этому становится возможным однотипно взаимодействовать с различными классами.

Сайт Alexus Software Development