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

Традиционные модульные структуры

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

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

Подпрограммы

Классический подход к повторному использованию состоит в том, чтобы создавать библиотеки подпрограмм. Здесь термин подпрограмма (routine) означает программный элемент, который может быть вызван другими элементами для выполнения некоторого алгоритма, используя некоторые входные данные, создавая некоторые выходные данные, и, возможно, модифицируя другие данные. Вызывающий элемент передает свои входные данные (а иногда - выходные данные и модифицируемые данные) в виде фактических аргументов (actual arguments) . Подпрограмма может также возвращать выходные данные в виде результата; в этом случае она называется функцией.

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

[x]. R1 Каждая задача допускает простую спецификацию. Точнее, возможно охарактеризовать каждую отдельную задачу небольшим набором входных и выходных параметров.

[x]. R2 Задачи четко отличаются одна от другой, поскольку подход, основанный на подпрограммах, не позволяет воспользоваться возможной сколько-нибудь существенной их общностью - за исключением повторного использования некоторых конструкций.

[x]. R3 Отсутствуют сложные структуры данных, которые пришлось бы распределять между использующими их подпрограммами.

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

[x]. Подпрограмма поиска существует в одном варианте. Но тогда, чтобы охватить все возможные ситуации, ей потребуется длинный список аргументов и она окажется очень сложной.

[x]. Подпрограмм поиска много, каждая из которых относится к конкретному случаю и отличается от других лишь немногими деталями. Нарушается требование Факторизации Общего Поведения; возможные пользователи легко могут заблудиться в неразберихе подпрограмм.

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

Пакеты

В семидесятые годы двадцатого века, в связи с развитием идей скрытия информации и абстракции данных, возникла необходимость в форме модуля, более совершенном, чем подпрограмма. Появилось несколько языков проектирования и программирования, наиболее известные из них: CLU, Modula-2 и Ada. В них предлагается сходная форма модуля, называемого в языке Ada пакетом, CLU - кластером, Modula - модулем. В нашем обсуждении будет использоваться термин пакет.4.4)

Пакеты - это единицы программной декомпозиции, обладающие следующими свойствами:

[x]. P1 В соответствии с принципом Лингвистических Модульных Единиц, "пакет" это конструкция языка, так что каждый пакет имеет имя и синтаксически четко определенную область.

[x]. P2 Описание каждого пакета содержит ряд объявлений связанных с ним элементов, таких как подпрограммы и переменные, которые в дальнейшем будут называться компонентами (features) пакета.

[x]. P3 Каждый пакет может точно определять права доступа, ограничивающие использование его компонентов другими пакетами. Другими словами, механизм пакетов поддерживает скрытие информации.

[x]. P4 В компилируемом языке (таком, который может быть использован для реализации, а не только для спецификации и проектирования) поддерживается независимая компиляция пакетов.

Благодаря свойству P3, пакеты можно рассматривать как абстрактные модули. Их главным вкладом в программирование является свойство P2, удовлетворяющее требованию Группирования Подпрограмм. Пакет может содержать любое количество связанных с ним операций, таких как создание таблицы, включение, поиск и удаление элементов. И нетрудно увидеть, как решение, основанное на использовании пакета, будет работать в рассматриваемом здесь примере табличного поиска. Ниже - в системе обозначений, заимствованной из нотации, используемой в последующих лекциях этого курса для ОО-ПО - приводится набросок пакета INTEGER_TABLE_HANDLING, описывающий частную реализацию таблиц целых чисел, основанную на использовании двоичных деревьев:

package INTEGER_TABLE_HANDLING feature
type INTBINTREE is
record
-- Описание представления двоичного дерева, например:
info: INTEGER
left, right: INTBINTREE
end
new: INTBINTREE is
-- Возвращение нового инициализированного INTBINTREE.
do ... end
has (t: INTBINTREE; x: INTEGER): BOOLEAN is
-- Содержится ли x в t?
do ... Реализация операции поиска ... end
put (t: INTBINTREE; x: INTEGER) is
-- Включить x в t.
do ... end
remove (t: INTBINTREE; x: INTEGER) is
-- Удалить x из t.
do ... end
end -- пакета INTEGER_TABLE_HANDLING

Этот пакет содержит объявление типа (INTBINTREE), и ряда подпрограмм, представляющих операции над объектами этого типа. В данном примере не потребовалось описания переменных пакета (хотя в подпрограммах могут иметься локальные переменные).

Пакеты-клиенты теперь могут работать с таблицами, используя различные методы из INTEGER_TABLE_HANDLING. Введем синтаксическое соглашение, позволяющее клиенту пользоваться методом f из пакета, для чего позаимствуем нотацию из языка CLU: P$f. В нашем примере типичные фрагменты программного текста клиента могут иметь вид:

-- Вспомогательные описания:
x: INTEGER; b: BOOLEAN
-- Описание t типа, определенного в INTEGER_TABLE_HANDLING:
t: INTEGER_TABLE_HANDLING$INTBINTREE
-- Инициализация t новой таблицей, создаваемой функцией new пакета:
t := INTEGER_TABLE_HANDLING$new
-- Включение x в таблицу, используя процедуру put пакета:
INTEGER_TABLE_HANDLING$put (t, x)
-- Присваивание True или False переменной b,
-- для поиска используется функция has пакета:
b := INTEGER_TABLE_HANDLING$has (t, x)

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

Менее важной проблемой является утомительная необходимость неоднократно писать имя пакета (здесь это INTEGER_TABLE_HANDLING). В языках, поддерживающих работу с пакетами, эта проблема решается с помощью различных сокращенных синтаксических конструкций (shortcuts), таких как, например, в языке Ada:

with INTEGER_TABLE_HANDLING then
... Здесь has означает INTEGER_TABLE_HANDLING$has, и т.д. ... end

Другим очевидным недостатком пакетов рассмотренного вида является их неспособность удовлетворять требованию Изменчивости Типов: приведенный выше модуль пригоден лишь для таблиц целых чисел. Однако, вскоре мы увидим, как устранить этот недостаток, делая пакеты универсальными (generic).

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

Языки, поддерживающие работу с пакетами, несколько различаются своими механизмами скрытия информации. Например, в языке Ada, внутренние свойства типа, такого как INTBINTREE, будут доступны клиентам, если не объявить тип как private (закрытый).

Часто для усиления скрытия информации в языках с инкапсуляцией предлагается объявлять пакет, состоящий из двух частей, интерфейса (interface) и реализации (implementation)(См. лекция 11 и лекция 5 курса "Основы объектно-ориентированного проектирования"). Закрытые элементы, такие как объявление типа или тело подпрограммы, включаются в раздел реализации. Однако такой подход приводит к добавочной работе для разработчиков модулей, заставляя их дублировать заголовки объявлений компонентов. При глубоком осмыслении правила Скрытия Информации все это не требуется. Подробнее эта проблема обсуждается в последующих лекциях.

Пакеты: оценка

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

[x]. Автор модуля-поставщика может хранить в одном месте и совместно компилировать все элементы, относящиеся к некоторому заданному понятию. Это облегчает отладку и изменения. В отличие от этого, при использовании отдельных самостоятельных подпрограмм всегда есть опасность забыть произвести обновление некоторых подпрограмм при изменениях проекта или реализации; например, можно обновить new, put и has, но забыть обновить remove.

[x]. Для авторов модулей-клиентов несомненно легче найти и использовать множество взаимосвязанных компонентов, если все они собраны в одном месте.

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

Однако пакеты все же не обеспечивают полного решения проблем повторного использования. Как уже отмечалось, они отвечают требованию Группирования Подпрограмм, но не удовлетворяют всем остальным требованиям. В частности, они не обеспечивают возможности факторизации общего поведения - "вынесения за скобки" общих компонентов. Заметим, что INTEGER_TABLE_HANDLING в нашем наброске текста пакета основывается на одном частном выборе реализации, - двоичных деревьев поиска. Конечно, благодаря скрытию информации, клиентам незачем интересоваться этим выбором. Но библиотека повторно используемых компонентов должна будет содержать модули для многих различных реализаций. Возникающую при этом ситуацию нетрудно предвидеть: типичная библиотека пакетов будет предлагать массу похожих, но вовсе не идентичных, модулей для заданной прикладной области, например, для работы с таблицами, но без какого-либо учета их общности. Обеспечивая возможность повторного использования для клиентов, такая методика приносит в жертву возможность повторного использования со стороны поставщиков.

Но даже со стороны клиентов ситуация остается не вполне приемлемой. Каждое использование таблицы клиентом требует упомянутого выше объявления вида:

t: INTEGER_TABLE_HANDLING$INTBINTREE

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

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


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