Объектно-ориентированное проектирование с примерами



         

Наследование - часть 8


В Smalltalk метод вышестоящего класса вызывают с помощью ключевого слова super, при этом вызывающий может указывать на самого себя с помощью ключевого слова self. В C++ метод любого достижимого вышестоящего класса можно вызывать, добавляя имя класса в качестве префикса, формируя квалифицированное имя метода (как TelemetryData::transmit() в нашем примере). Вызывающий объект может ссылаться на себя с помощью предопределенного указателя this.

На практике метод суперкласса вызывается до или после дополнительных действий. Метод подкласса уточняет или дополняет поведение суперкласса [В CLOS эти различные роли метода выражаются явно с помощью дополнительных квалификаторов :before, :after или :around. Метод без дополнительного квалификатора считается первичным и выполняет основную работу, обеспечивающую требуемое поведение. Before-метод вызывается до первичного, after-метод - после первичного, around-метод действует как оболочка вокруг первичного метода, которая вызывается изнутри этого метода функцией call-next-method].

Все подклассы на рис. 3-5 являются также подтипами вышестоящего класса. В частности, ElectricalData является подтипом TelemetryData. Система типов, развивающаяся параллельно наследованию, обычна для объектно-ориентированных языков с сильной типизацией, включая C++. Для Smalltalk, который едва ли вообще можно считать типизированным, типы не имеют значения.
 

Поиск метода

Рассмотрим иерархию (рис. 3-6), в которой имеется базовый класс и три подкласса с именами circle, Triangle и Rectangle. Для класса Rectangle определен в свою очередь подкласс SolidRectangle. Предположим, что в классе DisplayItem определена переменная экземпляра theCenter (задающая координаты центра изображения), а также следующие операции:

    draw - нарисовать изображение;

    move - передвинуть изображение;

    location - вернуть координаты изображения.

    Операция location является общей для всех подклассов и не требует обязательного переопределения. Однако, поскольку только подклассы могут знать, как их изображать и передвигать, операции draw и move должны быть переопределены.
     

    Рис. 3-6. Диаграмма класса DisplayItem.

    Класс Circle имеет переменную theRadius и соответственно операции для установки (set) и чтения значения этой переменной. Для этого класса операция draw формирует изображение окружности заданного радиуса с центром в точке theCenter. В классе Rectangle есть переменные theHeight и theWidth и соответствующие операции установки и чтения их значений. Операция draw в данном случае формирует изображение прямоугольника заданной высоты и ширины с центром в заданной точке theCenter. Подкласс SolidRectangle наследует все особенности класса Rectangle, но операция draw в этом подклассе переопределена. Сначала вызывается draw вышестоящего класса, а затем полученный контур заполняется цветом.

    Теперь рассмотрим следующий фрагмент программы:

    DisplayItem* items[10]; 
    for (unsigned index = 0; index < 10; index++) items[index]->draw();

    Вызов draw требует полиморфного поведения. У нас есть разнородный массив объектов, содержащий указатели на любые разновидности DisplayItem. Пусть некоторый клиент хочет, чтобы они все изобразили себя на экране. Наш подход - перебрать элементы массива и послать каждому указываемому объекту сообщение draw. Компилятор не может определить, какую функцию и откуда надо при этом вызвать, так как невозможно предсказать, на что будут указывать элементы массива во время выполнения программы. Посмотрим, как эта задача будет решаться в разных объектно-ориентированных языках.

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

      получатель ищет селектор сообщения в словаре методов своего класса;

      если метод найден, то запускается его код;

      если нет, поиск производится в словаре методов суперкласса.

      Таким образом, поиск распространяется вверх по иерархии и заканчивается на классе object, который является "предком" всех классов. Если метод не найден и там, посылается сообщение doesNotUnderstand, то есть, генерируется ошибка.

      Главным действующим лицом в этом алгоритме является словарь методов. Он формируется при создании класса и, являясь частью его реализации, скрыт от клиентов. Вызов метода в Smalltalk требует примерно в 1.5 раза больше времени, чем вызов простой подпрограммы. В коммерческих версиях Smalltalk вызов методов ускорен на 20-30% за счет кеширования доступа к словарю [31].

      Операция draw в подклассе solidRectangle представляет собой особый случай. Мы уже отмечали, что вначале вызывается одноименный метод суперкласса Rectangle. В Smalltalk для вызова метода суперкласса используется ключевое слово super. Поиск метода super draw начинается сразу с суперкласса.

      Исследования Дейча дают основание полагать, что полиморфизм в 85% случаев не нужен, так что вызов метода часто можно свести к обычному вызову процедуры [32]. Дафф замечает, что в таких ситуациях программист часто делает неявные предположения, которые бы позволили раннее связывание [33]. К сожалению, в нетипизированных языках у него нет способа сообщить об этом компилятору.

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

      В C++ операции для позднего связывания объявляются виртуальными (virtual), а все остальные обрабатываются компилятором как обычные вызовы подпрограмм. В нашем примере draw - виртуальная функция, a location - обычная. Есть еще одно средство, используя которое можно выиграть в скорости. Невиртуальные методы могут быть объявлены подставляемыми (inline), при этом соответствующая подпрограмма не вызывается, а явно включается в код на манер макроподстановки.

      Для управления виртуальными функциями в C++ используется концепция vtable (виртуальных таблиц), которые формируются для каждого объекта при его создании (то есть когда класс объекта уже известен). Такая таблица содержит список указателей на виртуальные функции. Например, при создании объекта класса Rectangle виртуальная таблица будет содержать запись для виртуальной функции draw, содержащую указатель на ближайшую в иерархии реализацию функции draw. Если в классе DisplayItem есть виртуальная функция rotate, которая в классе Rectangle не переопределена, то соответствующий указатель для rotate останется связан с классом DisplayItem. Во время исполнения программы происходит косвенное обращение через соответствующий указатель и сразу выполняется нужный код без всякого поиска [34].

      Операция draw в классе SolidRectangle представляет собой особый случай и в языке C++. Чтобы вызвать метод draw из суперкласса, применяется специальный префикс, указывающий на место определения функции. Это выглядит следующим образом:

      Rectangle::Draw(); 

      Исследование Страуструпа показало, что вызов виртуальной функции по эффективности мало уступает вызову обычной функции [35]. Для одиночного наследования вызов виртуальной функции требует дополнительно выполнения трех-четырех операций доступа к памяти по отношению к обычному вызову; при множественном наследовании число таких дополнительных операций составляет пять или шесть.

      Существенно сложнее выполняется поиск нужных функций в языке CLOS, здесь используются дополнительные квалификаторы: : before, : after, : around. Наличие множественного полиморфизма еще более усложняет проблему. При вызове метода в языке CLOS, как правило, реализуется следующий алгоритм:

        Определяется тип аргументов. 

        Вычисляется множество допустимых методов. 

        Методы сортируются в направлении от наиболее специализированных к более общим. 

        Выполняются вызовы всех методов с квалификатором : before. 

        Выполняется вызов наиболее специализированного первичного метода. 

        Выполняются вызовы всех методов с квалификаторами : after. 

        Возвращается значение первичного метода [36]. 

        В CLOS есть протокол для метаобъектов, который позволяет переопределять в том числе и этот алгоритм. На практике, однако, мало кто заходит так далеко. Как справедливо отметили Винстон и Хорн: "Алгоритмы, используемые в языке CLOS, сложны, и даже кудесники программирования стараются не вникать в их детали, так же как физики предпочитают иметь дело с механикой Ньютона, а не с квантовой механикой" [37]. 

<


Содержание  Назад  Вперед