Можно также представлять частично отложенный класс поведения (или набор таких классов, называемый "библиотекой"), как устройство с несколькими электрическими розетками - отложенными классами - в которые разработчик приложения будет вставлять совместимые с ними устройства. Эту метафору можно продолжить: для устройства важны меры предосторожности - утверждения, выражающие требования к допустимым съемным устройствам, например, спецификация розетки определяет допустимое напряжение, силу тока и другие электрические параметры.
Роль отложенных классов при анализе и глобальном проектировании
Отложенные классы играют также ключевую роль при использовании ОО-метода не только на уровне реализации, но и на самых ранних и верхних уровнях построения системы - анализе и глобальном проектировании. Целью является создание спецификации системы и ее архитектуры, для проекта требуется также абстрактное описание каждого модуля без деталей его реализации.
Обычно даваемая в этом случае рекомендация состоит в использовании отдельных обозначений: некоторого "метода" анализа (за этим термином во многих случаях стоит просто некоторая графическая нотация) и некоторого ЯПП (PDL) (языка проектирования программ, зачастую тоже графического). Но у этого подхода много недостатков:
[x]. Разрыв между последовательными шагами процесса разработки представляет серьезную угрозу для качества ПО. Необходимость трансляции из одного формализма в другой может привести к ошибкам и подвергает опасности целостность системы. ОО-технология, напротив, предлагает перспективу непрерывного процесса разработки ПО.
[x]. Многоярусный подход является особенно губительным для этапов сопровождения и эволюции системы. Крайне сложно гарантировать согласованность проекта и реализации на этих этапах.
[x]. Наконец, большинство существующих подходов к анализу и проектированию не предлагают никакой поддержки формальной спецификации функциональных свойств модулей, не зависящей от их реализации, например в форме утверждений.
Последний комментарий приводит к парадоксу уровней: точная нотация, подобная языку, используемому в этой книге, иногда отклоняется как "низкоуровневая" или "ориентированная на реализацию", поскольку внешне выглядит как язык программирования. На самом же деле, благодаря утверждениям и такому механизму абстракции как отложенные классы, их уровень существенно выше уровня большинства имеющихся подходов к анализу и проектированию. Многим требуется время, чтобы осознать это, поскольку раньше их учили тому, что высокий уровень абстракции означает неопределенность и что абстракция всегда должна быть неточной.
Использование отложенных классов для анализа и проектирования позволяет нам одновременно быть абстрактными и точными, и применять один и тот же язык на протяжении всего процесса разработки. При этом устраняются разрывы в концепциях, переход от описания модуля на высоком уровне к реализациям может происходить плавно внутри одного формализма. Даже нереализованные операции проектируемых модулей, представленные отложенными процедурами, можно достаточно точно охарактеризовать с помощью предусловий, постусловий и инвариантов.
Система обозначений, которая к этому моменту развернута почти до конца, покрывает этапы анализа и проектирования, а также и реализации. Одни и те же понятия и конструкции применяются на всех стадиях, различаются только уровни абстракции и детализации.
Обсуждение
В этой лекции введены основные понятия, связанные с наследованием. Оценим сейчас достоинства некоторых введенных соглашений. Дальнейшие комментарии о механизме наследования (в частности, о множественном наследовании) появятся в следующей лекции.
Явное переопределение
Роль предложения redefine состоит в улучшении читаемости и надежности. Компиляторам, на самом деле, оно не нужно, так как в классе может быть лишь один компонент с данным именем, то объявленный в данном классе компонент, имеющий то же имя, что и компонент некоторого предка, может быть только переопределением этого компонента (или ошибкой).
Не следует пренебрегать возможностью ошибки, так как программист может наследовать некоторый класс, не зная всех компонентов, объявленных в его предках. Для избежания этой опасности требуется явно указать каждое переопределение. В этом и состоит основная роль предложения redefine, которое также полезно при чтении класса.
Доступ к предшественнику процедуры
Напомним правило использования конструкции Precursor (...): она может появляться только в переопределяемой версии процедуры.
Этим обеспечивается цель введения этой конструкции: позволить новому определению использовать первоначальную реализацию. При этом возможность явного указания родителя устраняет всякую неопределенность (в частности, при множественном наследовании). Если бы допускался доступ любой процедуры к любому компоненту предков, то текст класса было бы трудно понять, читателю все время приходилось бы обращаться к текстам многих других классов.
Динамическое связывание и эффективность
Можно подумать, что сила механизма динамического связывания приведет во время выполнения к недопустимым накладным расходам. Такая опасность существует, но аккуратное проектирование языка и хорошие методы его реализации могут ее предотвратить.
Дело в том, что динамическое связывание требует несколько большего объема действий во время выполнения. Сравним вызов обычной процедуры в традиционном языке программирования (Pascal, Ada, C, ...)
1
f (x, a, b, c...)
с ОО-формой
3.
x.f (a, b, c...)
Разница между этими двумя формами уже была разъяснена при введении понятия класса, для идентификации типа модуля. Но сейчас мы понимаем, что это связано не только со стилем, имеется также различие и в семантике. В форме (1), какой именно компонент обозначает имя f известно статически во время компиляции или, в худшем случае, во время компоновки, если для объединения раздельно откомпилированных модулей используется компоновщик. Однако при динамическом связывании такая информация недоступна статически: для f в форме (2) выбор компонента зависит от объекта, к которому присоединен x во время конкретного выполнения. Каким будет этот тип нельзя (в общем случае) определить по тексту программы, это служит источником гибкости этого ранее разрекламированного механизма.
Предположим вначале, что динамическое связывание реализовано наивно. Во время выполнения хранится копия иерархии классов. Каждый объект содержит информацию о своем типе - вершине в этой иерархии. Чтобы интерпретировать во время выполнения x.f, окружение ищет соответствующую вершину и проверяет, содержит ли этот класс компонент f. Если да, то прекрасно, мы нашли то, что требовалось. Если нет, то переходим к вершине-родителю и повторяем всю операцию. Может потребоваться проделать путь до самого верхнего класса (или нескольких таких классов в случае множественного наследования).
В типизированном языке нахождение подходящего компонента гарантировано, но в нетипизированном языке, таком как Smalltalk, поиск может быть неудачным, и придется завершить выполнение диагнозом "сообщение не понято". |
Такая схема все еще применяется с различными оптимизациями во многих реализациях не статически типизированных языков. Она приводит к существенным затратам, снижающим эффективность. Хуже того, эти затраты не прогнозируемы и растут с увеличением глубины структуры наследования, так как алгоритм может постоянно проходить путь до корня иерархии наследования. Это приводит к конфликту между повторным использованием и эффективностью, поскольку упорная работа над повторным использованием м приводит к введению дополнительных уровней наследования. Представьте состояние бедного разработчика, который перед добавлением нового уровня наследования должен оценить, как это ударит по эффективности. Нельзя ставить разработчиков ПО перед таким выбором.