Подходы к повторному использованию
Пакеты
В семидесятые годы двадцатого века, в связи с развитием идей скрытия информации и абстракции данных, возникла необходимость в форме модуля, более совершенном, чем подпрограмма. Появилось несколько языков проектирования и программирования, наиболее известные из них: CLU, Modula-2 и Ada. В них предлагается сходная форма модуля, называемого в языке Ada пакетом, CLU - кластером, Modula - модулем. В нашем обсуждении будет использоваться термин пакет4Этот подход будет рассмотрен подробно в лекции 15 курса "Основы объектно-ориентированного проектирования", с использованием понятия пакета из языка Ada. Напомним, что под "Ada" имеется в виду язык Ada 83. (В версии Ada 95 сохранены пакеты , но с некоторыми дополнениями.)
Пакеты - это единицы программной декомпозиции, обладающие следующими свойствами:
- P1 В соответствии с принципом Лингвистических Модульных Единиц, "пакет" это конструкция языка, так что каждый пакет имеет имя и синтаксически четко определенную область.
- P2 Описание каждого пакета содержит ряд объявлений связанных с ним элементов, таких как подпрограммы и переменные, которые в дальнейшем будут называться компонентами (features) пакета.
- P3 Каждый пакет может точно определять права доступа, ограничивающие использование его компонентов другими пакетами. Другими словами, механизм пакетов поддерживает скрытие информации.
- 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. Одним из ключевых шагов к ОО-программированию явится объединение этих двух понятий. Но не будем опережать события.
with INTEGER_TABLE_HANDLING then ... Здесь has означает INTEGER_TABLE_HANDLING$has, и т.д. ... end
Другим очевидным недостатком пакетов рассмотренного вида является их неспособность удовлетворять требованию Изменчивости Типов: приведенный выше модуль пригоден лишь для таблиц целых чисел. Однако, вскоре мы увидим, как устранить этот недостаток, делая пакеты универсальными (generic).
Механизм пакетов обеспечивает скрытие информации, ограничивая права клиентов на доступ к компонентам. Показанный выше клиент был в состоянии объявить одну из своих собственных переменных, используя тип INTBINTREE, взятый от своего поставщика, и вызывать подпрограммы, описанные этим поставщиком. Но он не имеет доступа ни к внутреннему описанию этого типа (к структуре record, определяющей реализацию таблиц), ни к телу подпрограмм (здесь это операторы do). Кроме того, можно скрыть от клиентов некоторые компоненты пакета (переменные, типы, подпрограммы), делая их используемыми только в тексте пакета.
Языки, поддерживающие работу с пакетами, несколько различаются своими механизмами скрытия информации. Например, в языке Ada, внутренние свойства типа, такого как INTBINTREE, будут доступны клиентам, если не объявить тип как private (закрытый). |
Часто для усиления скрытия информации в языках с инкапсуляцией предлагается объявлять пакет, состоящий из двух частей, интерфейса (interface) и реализации (implementation)(См. "Проектирование по контракту: построение надежного ПО" и лекция 5 курса "Основы объектно-ориентированного проектирования"). Закрытые элементы, такие как объявление типа или тело подпрограммы, включаются в раздел реализации. Однако такой подход приводит к добавочной работе для разработчиков модулей, заставляя их дублировать заголовки объявлений компонентов. При глубоком осмыслении правила Скрытия Информации все это не требуется. Подробнее эта проблема обсуждается в последующих лекциях.
Пакеты: оценка
По сравнению с подпрограммами, механизм пакетов приводит к существенному совершенствованию разбиения системы ПО на абстрактные модули. Собрать нужные компоненты "под одной крышей" крайне полезно как для поставщиков, так и для клиентов:
- Автор модуля-поставщика может хранить в одном месте и совместно компилировать все элементы, относящиеся к некоторому заданному понятию. Это облегчает отладку и изменения. В отличие от этого, при использовании отдельных самостоятельных подпрограмм всегда есть опасность забыть произвести обновление некоторых подпрограмм при изменениях проекта или реализации; например, можно обновить new, put и has, но забыть обновить remove.
- Для авторов модулей-клиентов несомненно легче найти и использовать множество взаимосвязанных компонентов, если все они собраны в одном месте.
Преимущество пакетов по сравнению с подпрограммами особенно очевидно в таких случаях, как рассмотренный здесь пример с таблицей, где в пакете собраны все операции, применимые к конкретной структуре данных.
Однако пакеты все же не обеспечивают полного решения проблем повторного использования. Как уже отмечалось, они отвечают требованию Группирования Подпрограмм, но не удовлетворяют всем остальным требованиям. В частности, они не обеспечивают возможности факторизации общего поведения - "вынесения за скобки" общих компонентов. Заметим, что INTEGER_TABLE_HANDLING в нашем наброске текста пакета основывается на одном частном выборе реализации, - двоичных деревьев поиска. Конечно, благодаря скрытию информации, клиентам незачем интересоваться этим выбором. Но библиотека повторно используемых компонентов должна будет содержать модули для многих различных реализаций. Возникающую при этом ситуацию нетрудно предвидеть: типичная библиотека пакетов будет предлагать массу похожих, но вовсе не идентичных, модулей для заданной прикладной области, например, для работы с таблицами, но без какого-либо учета их общности. Обеспечивая возможность повторного использования для клиентов, такая методика приносит в жертву возможность повторного использования со стороны поставщиков.
Но даже со стороны клиентов ситуация остается не вполне приемлемой. Каждое использование таблицы клиентом требует упомянутого выше объявления вида:
t: INTEGER_TABLE_HANDLING$INTBINTREE
Клиент вынужден выбирать конкретную реализацию. Этим нарушается требование Независимости Представлений: авторы модулей-клиентов должны будут знать больше о реализациях представлений модуля-поставщика, чем это принципиально необходимо.