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

Необходимость параметризованных классов

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

Родовые АТД

Наш работающий пример АТД, STACK, был объявлен с параметром, как STACK [G]. Любое его использование заставляет специфицировать "фактический родовой параметр", представляющий тип хранимого в стеке объекта. Имя G, используемое в спецификации АТД, - формальный родовой параметр класса. Оно указывает, что элементы стека могут иметь любой возможный тип. При таком подходе можно использовать одну спецификацию для всех возможных стеков. Альтернативой, вряд ли приемлемой, было бы введение множества классов: INTEGER_STACK, REAL_STACK и т.д.

Любые АТД, описывающие контейнеры: множества, списки, матрицы, массивы и многие другие, очевидно, также должны быть родовыми.

Это решение применимо к контейнерам классам, также как к контейнерам АТД.

Проблема

Рассмотрим пример стека, но уже не как АТД, а как класс. Мы знаем, как написать класс INTEGER_STACK, задающий стек объектов типа INTEGER. Компоненты будут включать count (число элементов), put (вталкивание элемента), item (элемент в вершине), remove (выталкивание элемента), empty (пустой ли стек?).

Тип INTEGER будет часто использоваться в объявлениях этого класса. Например, это тип аргумента для put и результата для item:

put (element: INTEGER) is
-- Втолкнуть элемент (в вершину стека).
do ... end
item: INTEGER is
-- Элемент в вершине стека
do ... end

Эти появления типа INTEGER следуют из правила явного объявления, используемого при разработке нотации: всякий раз при введении сущности, обозначающей возможные объекты времени выполнения, необходимо явное указание ее типа, такое как element: INTEGER. Здесь это означает, что необходимо указать тип для запроса item, для аргумента element процедуры put и для других сущностей, обозначающих возможные элементы стека.

Как следствие, придется писать различные классы для каждого сорта стека: INTEGER_STACK, REAL_STACK, POINT_STACK, BOOK_STACK... Все эти стековые классы будут одинаковыми за исключением объявления типов item, element и некоторых других сущностей. Основные операции над стеком не зависят от типа элементов стека и реализуются одинаково. Для всех, заинтересованных в повторном использовании, такое дублирование классов представляется мало привлекательным.

Проблема возникает из-за противоречия двух основных требований, предъявляемых к классам и сформулированных в начале этой книги.

[x]. Надежность: сохранение преимуществ безопасности типов с помощью явного объявления типа.

[x]. Повторное использование: возможность написать один программный элемент, покрывающий многие варианты одного понятия.

Роль типизации

Зачем настаивать на явном объявлении типов (первое из двух требований)? Это часть главного вопроса о типизации, которому в этой книге посвящена отдельная лекция (лекция 17). Но уже сейчас можно указать две основные причины, по которым ОО-программа должна быть статически типизирована.

[x]. Читаемость: явное объявление четко сообщает читателю о том, как предполагается использовать каждый элемент. Это важно как для автора, так и для того, кому нужно понять часть программы, чтобы отладить или расширить ее.

[x]. Надежность: благодаря явному объявлению типов компилятор сможет найти ошибочные операции еще на этапе компиляции, не допуская их проявления при выполнении. В фундаментальных операциях ОО-вычислений вызов компонента имеет форму x.f (a,..), где х - некоего типа TX. Причины возникновения ошибок могут быть разными: соответствующий класс TX может не иметь метода f; метод может существовать, но быть скрытым; количество аргументов при вызове может не совпадать с объявленным в описании класса; тип а или другого аргумента может не совпадать с ожидаемым. В языке Smalltalk, в котором отсутствует статическая типизация, любая такая ситуация приведет к краху на этапе выполнения с выдачей, например, сообщения: "Message not understood", в то время как компилятор языка с явной типизацией не пропустит ошибочной конструкции.

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

Без учета требований надежности явное объявление типов было бы не нужно так же как универсализация. Остаток этой лекции обращается к языкам со статической типизацией, т.е. языкам, которые требуют объявления каждой сущности и задают правила, позволяющие компиляторам обнаруживать несоответствие типов до выполнения. В не статически типизированных языках, таких как Smalltalk, универсализация не имеет смысла. Язык упрощается, но не защищает от схем вида:

my_stack.put (my_circle)
my_account := my_stack.item
my_account.withdraw (5000)

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

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

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

Оглавление статьи/книги

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