Основы объектно-ориентированного программирования - Бертран Мейер
Шрифт:
Интервал:
Закладка:
Горизонтальное и вертикальное обобщение типа
Рис. 10.1. Размерности обобщения
Уже изученные механизмы позволяют написать класс, помещенный в центр рисунка - LIST_OF_BOOKS, экземпляр которого представляет список книг. У класса следующие компоненты: put для вставки элемента, remove для удаления элемента, count для подсчета числа элементов и т.д. Очевидны два пути обобщения понятия LIST_OF_BOOKS.
[x]. Списки являются специальным видом структур, представляющих контейнеры. Из многих других примеров контейнеров отметим деревья, стеки и массивы. В сравнение со списком LIST_OF_BOOKS, более абстрактным вариантом контейнера является класс SET_OF_BOOKS (множество книг). Более специализированным вариантом является класс LINKED_LIST_OF_BOOKS, определяющий связный список книг - специализированный вариант списка. Три класса задают вертикальную размерность на рисунке - размерность наследования.
[x]. Списки книг являются, с другой стороны, специальным случаем списков объектов некоторого вида. Из многих других видов отметим списки журналов, списки людей, списки целых чисел. Это горизонтальная размерность на нашем рисунке - размерность универсализации, тема нашего изучения в последующей части этой лекции. Если задать параметр класса, представляющий произвольный тип, то можно не создавать почти идентичные классы, такие как LIST_OF_BOOKS и LIST_OF_PEOPLE, не жертвуя при этом безопасностью, вносимой статической типизацией.
Отношение между двумя этими механизмами - трудный вопрос для изучающих ОО-концепции. Как рассматривать наследование и параметризацию, как соперников или как соратников, когда целью является построение более гибкого ПО?10.1)
Данная лекция посвящена универсализации, кроме того, мы подробно рассмотрим один из наиболее общих примеров родовых структур: массивы. Заметьте, термины универсальный класс, родовой класс, параметризованный класс являются синонимами. Во всех случаях речь идет о классе с параметрами, задающими некоторый тип (класс).
Необходимость параметризованных классов
Универсализация уже рассматривалась в данной книге, но не применялась для классов. Мы столкнулись с ней при обзоре традиционных подходов к повторному использованию и при изучении математической модели класса - АТД, где была показана необходимость определения параметризированного АТД.
Родовые АТД
Наш работающий пример АТД, 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)
где элемент, полученный из вершины стека, рассматривается как банковский счет, хотя в действительности это круг, что можно понять из первой инструкции. Выполнение программы закончится, при попытке получить пять тысяч долларов от "дырки от бублика".
Статическая типизация защищает от подобных неудач. Совмещение типизации с требованием повторного использования приведет нас к механизму универсализации.
Родовые классы
Согласование статической типизации с требованием повторного использования для классов, описывающих контейнерные структуры, означает, как показано на примере стека, что мы хотим одновременно иметь возможность:
[x]. Объявить тип каждой сущности, появляющейся в классе стека, включая сущности, представляющие элементы стека.
[x]. Написать класс так, чтобы он не содержал никаких намеков на тип элемента стека, и следовательно, мог использоваться для построения стеков с элементами произвольных типов.