Бертран Мейер - Основы объектно-ориентированного программирования
Часто для усиления скрытия информации в языках с инкапсуляцией предлагается объявлять пакет, состоящий из двух частей, интерфейса (interface) и реализации (implementation)(См. лекция 11 и лекция 5 курса "Основы объектно-ориентированного проектирования"). Закрытые элементы, такие как объявление типа или тело подпрограммы, включаются в раздел реализации. Однако такой подход приводит к добавочной работе для разработчиков модулей, заставляя их дублировать заголовки объявлений компонентов. При глубоком осмыслении правила Скрытия Информации все это не требуется. Подробнее эта проблема обсуждается в последующих лекциях.
Пакеты: оценка
По сравнению с подпрограммами, механизм пакетов приводит к существенному совершенствованию разбиения системы ПО на абстрактные модули. Собрать нужные компоненты "под одной крышей" крайне полезно как для поставщиков, так и для клиентов:
[x]. Автор модуля-поставщика может хранить в одном месте и совместно компилировать все элементы, относящиеся к некоторому заданному понятию. Это облегчает отладку и изменения. В отличие от этого, при использовании отдельных самостоятельных подпрограмм всегда есть опасность забыть произвести обновление некоторых подпрограмм при изменениях проекта или реализации; например, можно обновить new, put и has, но забыть обновить remove.
[x]. Для авторов модулей-клиентов несомненно легче найти и использовать множество взаимосвязанных компонентов, если все они собраны в одном месте.
Преимущество пакетов по сравнению с подпрограммами особенно очевидно в таких случаях, как рассмотренный здесь пример с таблицей, где в пакете собраны все операции, применимые к конкретной структуре данных.
Однако пакеты все же не обеспечивают полного решения проблем повторного использования. Как уже отмечалось, они отвечают требованию Группирования Подпрограмм, но не удовлетворяют всем остальным требованиям. В частности, они не обеспечивают возможности факторизации общего поведения - "вынесения за скобки" общих компонентов. Заметим, что INTEGER_TABLE_HANDLING в нашем наброске текста пакета основывается на одном частном выборе реализации, - двоичных деревьев поиска. Конечно, благодаря скрытию информации, клиентам незачем интересоваться этим выбором. Но библиотека повторно используемых компонентов должна будет содержать модули для многих различных реализаций. Возникающую при этом ситуацию нетрудно предвидеть: типичная библиотека пакетов будет предлагать массу похожих, но вовсе не идентичных, модулей для заданной прикладной области, например, для работы с таблицами, но без какого-либо учета их общности. Обеспечивая возможность повторного использования для клиентов, такая методика приносит в жертву возможность повторного использования со стороны поставщиков.
Но даже со стороны клиентов ситуация остается не вполне приемлемой. Каждое использование таблицы клиентом требует упомянутого выше объявления вида:
t: INTEGER_TABLE_HANDLING$INTBINTREE
Клиент вынужден выбирать конкретную реализацию. Этим нарушается требование Независимости Представлений: авторы модулей-клиентов должны будут знать больше о реализациях представлений модуля-поставщика, чем это принципиально необходимо.
Перегрузка и универсальность
Два технических приема - перегрузка (overloading) и универсальность (genericity) предлагают свои решения, направленные на достижение большей гибкости описанных выше механизмов. Рассмотрим, что же они могут дать.
Синтаксическая перегрузка
Перегрузка - это связывание с одним именем более одного содержания. Наиболее часто перегружаются имена переменных: почти во всех языках программирования различные по смыслу переменные могут иметь одно и то же имя, если они принадлежат различным модулям (различным блокам - в языке Algol и подобных ему).
Для этого обсуждения более существенной является перегрузка подпрограмм, частным случаем которой является перегрузка операторов, которая позволяет использовать одинаковые имена для нескольких подпрограмм. Такая возможность почти всегда имеет место для арифметических операторов: одна и та же запись, a +b, означает различные виды сложения, в зависимости от типов a и b (целые, вещественные с обычной точностью, вещественные с удвоенной точностью). Начиная с языка Algol 68, в котором допускалась перегрузка основных операторов, некоторые языки программирования распространили возможность перегрузки на операции, определяемые пользователем, и на обычные подпрограммы.
Например, в языке Ada пакет может содержать несколько подпрограмм с одним и тем же именем, но с разной сигнатурой, определяемой здесь числом и типами аргументов. В общем случае сигнатура функций содержит также тип результата, но язык Ada разрешает перегрузку, учитывающую только аргументы. Например, пакет может содержать несколько функций square:4.5)
square (x: INTEGER): INTEGER is do ... end
square (x: REAL): REAL is do ... end
square (x: DOUBLE): DOUBLE is do ... end
square (x: COMPLEX): COMPLEX is do ... end
Тогда при вызове square (y) тип аргумента y определит, какой вариант подпрограммы имелся в виду.
Подобным же образом, пакет может описывать набор функций поиска одинакового вида:
has (t: "SOME_TABLE_TYPE"; x: ELEMENT) is do ... end
Каждая из них задает свою реализацию и отличается фактическим типом, используемым вместо "SOME_TABLE_TYPE". Тип первого фактического аргумента, в любом клиентском вызове has, позволяет определить, какая из подпрограмм имелась в виду.
Из этих соображений следует общая характеризация перегрузки, которая будет полезной, когда несколько позже это свойство будет сопоставляться с универсальностью:
Роль перегрузки
Перегрузка подпрограмм является средством, предназначенным для клиентов. Она позволяет писать один и тот же текст, используя разные реализации некоторого понятия.
Так что же дает перегрузка подпрограмм решению проблемы повторного использования? Не много. Это - синтаксическое средство, освобождающее разработчиков от необходимости придумывать различные имена для разных реализаций некоторой операции и, по существу, перекладывает эту ношу на компьютер. Но это не решает ни одной из ключевых задач повторного использования. В частности, перегрузка не дает ничего для выполнения требования Независимости Представлений. Когда записывается вызов
has (t, x)
то необходимо будет объявить t, а следовательно (даже если скрытие информации освобождает вас от заботы о деталях каждого варианта алгоритма поиска) нужно точно знать, каков вид таблицы t! Единственным достоинством перегрузки является то, что во всех случаях можно пользоваться одним и тем же именем. Без перегрузки в каждой реализации потребуется другое имя, например
has_binary_tree (t, x)
has_hash (t, x)
has_linked (t, x)
Но является ли таки достоинством возможность избежать использования различных имен? Наверное нет. Основным правилом создания ПО, объектно оно или нет, является принцип честности (non-deception): различия в семантике должны отражаться в различиях текстов программ. Это позволяет существенно улучшить понятность ПО и минимизировать опасность возникновения ошибок. Если подпрограммы has являются различными, то использование для них одинакового имени может вводить в заблуждение - при чтении текста программы возникает предположение, что это одинаковые подпрограммы. Лучше предложить клиенту немного более многословный текст (как в случае введенных выше индивидуальных имен) и устранить какую-либо опасность путаницы.
Чем больше анализируешь перегрузку, тем более ограниченной она выглядит.
Критерий, используемый для устранения неоднозначности вызовов - сигнатуры списков аргументов - не обладает никакими конкретными достоинствами. Он работает в приведенных выше примерах, где все различные перегружаемые процедуры square и has имеют разные сигнатуры, но нетрудно представить себе множество случаев, когда у разных вариантов сигнатуры совпадают. Одним из простейших примеров перегрузки, по-видимому, является множество функций системы компьютерной графики, используемых для создания новых точек, например в виде:
p1 := new_point (u, v)
Точку можно задать: декартовыми координатами x и y; или полярными координатами r и q (расстоянием от начала координат и углом, отсчитываемым от горизонтальной оси). Но если перегрузить функцию new_point, то возникнет затруднение, связанное с тем, что оба варианта имеют одинаковую сигнатуру: