Книга: Основы объектно-ориентированного программирования

Обсуждение

Давайте проанализируем следствия некоторых решений, принятых в этой лекции.

Переименование

Любой язык, поддерживающий множественное наследование, должен как-то решать проблему конфликта имен. Коль скоро мы не можем и не должны требовать от разработчиков внесения изменений в исходные классы, есть всего два решения, помимо тех, что были описаны выше:

[x]. требовать от клиентов устранения всех неоднозначностей;

[x]. выбирать некую интерпретацию по умолчанию.

В соответствии с первым подходом, класс C, наследующий компонент f от A и B, будет нормально откомпилирован, возможно, с выдачей предупреждения. Ничего страшного не произойдет, пока в тексте клиента C не обнаружится нечто подобное:

x: C
... x.f ...

Клиенту придется квалифицировать ссылку на f, используя нотацию, например, такую: x.f | A, либо x.f | B, чтобы указать подразумеваемый класс.

Это решение противоречит, однако, одному из принципов, важность которого мы подчеркивали в этой лекции: структура наследования класса касается лишь самого класса и его предков, но не клиентов, за исключением случаев полиморфного применения компонентов. Пользуясь f из C, я не должен знать о том, введена эта функция классом C либо получена им от A или B.

Согласно второй стратегии, запись x.f корректна. Выбор одного из вариантов делается средствами языка. Критерием выбора является, например, порядок, в котором C перечисляет своих родителей. Для обращения к другим вариантам может существовать особая форма записи.

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

Эти проблемы решает смена имен. Одним из ее преимуществ является возможность создания клиентского интерфейса с "понятными" именами компонентов.

ОО-разработка и перегрузка

Анализ роли имен, сделанный в этой лекции, позволяет вернуться к вопросу о внутриклассовой перегрузке (in-class name overloading).

Напомню, что в таких языках, как Ada 83 и Ada 95, перегрузка разрешена - можно давать одно имя разным компонентам одного синтаксического модуля. Например, в одном пакете возможны определения:

infix "+" (a, b: VECTOR) is...
infix "+" (a, b: MATRIX) is...

Языки Java и C++ позволяют делать то же самое в пределах класса.

Ранее мы называли эту возможность синтаксической перегрузкой. Это - статический механизм. Для однозначного разрешения вызова, например, x + y, достаточно посмотреть на тип аргументов x и y, который очевиден из текста программы.

В объектной технологии применяется и более мощный механизм семантической (или динамической) перегрузки. Так, если классы VECTOR и MATRIX наследуют от общего предка NUMERIC компонент

infix "+" (a: T) is...

и каждый из них переопределяет его нужным образом, то понять, о какой операции + идет речь в выражении x + y, можно только динамически во время выполнения программы. Семантическая перегрузка - действительно интересный механизм, позволяющий использовать единое имя в тексте различных классов для представления разных вариантов по сути одной и той же операции, такой, как сложение в NUMERIC. Правила для утверждений, рассматриваемые в следующей лекции, уточнят эту ситуацию, требуя, чтобы переобъявления компонента сохраняли его фундаментальную семантику.

Сохраняется ли роль синтаксической перегрузки в объектной технологии? Трудно найти разумные аргументы в ее поддержку. Можно понять, почему язык Ada 83, не имеющий классов, ее использовал. Но в ОО-языке выбор одного имени для обозначения разных операций - это прямой путь к созданию беспорядка.

Проблема состоит еще и в том, что синтаксическая форма перегрузки вступает в конфликт с семантической, в активе которой - полиморфизм и динамическое связывание. Рассмотрим вызов x.f (a). Если он следует за полиморфными операторами присваивания x := y и a := b, то при сохранении имен его результат будет в точности тем же, что и для y.f (b), даже если типы b и y отличны от типов a и x. Но при перегрузке это свойство не сохраняется! Теперь f может быть перегруженным именем двух разных компонентов: одного - типа a, другого - типа b. Чему отдать предпочтение: синтаксической перегрузке или динамическому связыванию? Хуже того, базовый класс типа y может переопределять один или оба перегруженных компонента. И таким комбинациям, как и причинам ошибок, нет числа.

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

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

Типичный пример, иногда приводимый в подтверждение полезности перегрузки, связан с компонентами класса STRING. Чтобы к одной строке, при отсутствии перегрузки, добавить другую строку или отдельный символ, используются разные имена компонентов: s1.add_string (s2) и s1.add_character ('A'), или в инфиксной записи s := s1++ s2 и s := s1 + 'A'. При перегрузке обе операции можно назвать одинаково. Так ли это необходимо? Объекты типов CHARACTER и STRING наделены совершенно разными свойствами. Добавление символа всегда увеличивает длину строки на 1. Сцепление строк может оставить длину неизменной (если вторая строка пуста) или увеличить ее произвольным образом. Применение разных имен кажется не только разумным, но и желательным, особенно потому, что приведенные выше примеры ошибок действительно вполне возможны.

Предположим, даже, что решено использовать перегрузку, но и в этом случае придется подумать о более точном критерии, позволяющем выбирать нужный компонент. Общепринятый критерий синтаксической перегрузки различает компоненты по их сигнатуре, что не исключает неоднозначности. Типичный пример - процедуры создания точек в полярной или декартовой системе координат: make_cartesian и make_polar. Сигнатуры обеих процедур одинаковы, - они имеют два аргумента типа REAL, однако, работают совершенно по-разному. Перегрузку здесь использовать нельзя. Для отражения того факта, что оба компонента и в самом деле различны, им следует дать разные имена.

Реализацию процедур создания ("конструкторов") в Java и C++ нельзя описывать без иронии. Так, вы не вправе давать конструкторам разные имена, а вынуждены полагаться на перегрузку. Пытаясь решить эту проблему, я не нашел ничего лучше, чем ввести искусственный третий параметр.

В итоге (внутриклассовая) синтаксическая перегрузка в ОО-среде создает немало проблем, не давая видимых преимуществ. (Тем же, кто использует Java, C++ или Ada 95, можно посоветовать полностью отказаться от перегрузки, прибегая к ней лишь при создании конструкторов, то есть тогда, когда язык не оставляет другого выбора.) Стараясь умело применять объектный подход, придерживайтесь простого правила: каждый компонент имеет имя, каждое имя означает только один компонент.

Оглавление книги


Генерация: 0.086. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз