void * _lop, _rop;
opTypes _lop_type, _rop_type;
};
Нам все равно нужен дискриминант, поскольку напрямую использовать объект, адресуемый указателем типа void*, нельзя, равно как невозможно определить тип такого объекта по указателю. (Мы не рекомендуем применять описанное решение в C++, хотя в языке C это весьма распространенный подход.)
Основной недостаток рассмотренных решений состоит в том, что ответственность за определение типа возлагается на программиста. Например, в случае решения, основанного на void*-указателях, операцию eval() для объекта AndQuery можно реализовать так:
void
AndQuery::
eval()
{
// не объектно-ориентированный подход
// ответственность за разрешение типа ложится на программиста
// определить фактический тип левого операнда
switch( _lop_type ) {
case And_query:
AndQuery *paq = static_castAndQuery*(_lop);
paq-eval();
break;
case Or_query:
OrQuery *pqq = static_castOrQuery*(_lop);
poq-eval();
break;
case Not_query:
NotQuery *pnotq = static_castNotQuery*(_lop);
pnotq-eval();
break;
case Name_query:
AndQuery *pnmq = static_castNameQuery*(_lop);
pnmq-eval();
break;
}
// то же для правого операнда
}
В результате явного управления разрешением типов увеличивается размер и сложность кода и добавление нового типа или исключение существующего при сохранении работоспособности программы затрудняется.
Объектно-ориентированное программирование предлагает альтернативное решение, в котором работа по разрешению типов перекладывается с программиста на компилятор. Например, так выглядит код операции eval() для класса AndQuery в случае применения объектно-ориентированного подхода (eval() объявлена виртуальной):
// объектно-ориентированное решение
// ответственность за разрешение типов перекладывается на компилятор
// примечание: теперь _lop и _rop - объекты типа класса
// их определения будут приведены ниже
void
AndQuery::
eval()
{
_lop-eval();
_rop-eval();
}
Если потребуется добавить или исключить какие-либо типы, эту часть программы не придется ни переписывать, ни перекомпилировать.
17.1.1. Объектно-ориентированное проектирование
Из чего складывается объектно-ориентированное проектирование четырех рассмотренных выше видов запросов? Как решаются проблемы их внутреннего представления?
С помощью наследования можно определить взаимосвязи между независимыми классами запросов. Для этого мы вводим в рассмотрение абстрактный класс Query, который будет служить для них базовым (соответственно сами эти классы будут считаться производными). Абстрактный класс можно представить себе как неполный, который становится более или менее завершенным, когда из него порождаются производные классы, - в нашем случае AndQuery, OrQuery, NotQuery и NameQuery.
В нашем абстрактном классе Query определены данные и функции-члены, общие для всех четырех типов запроса. При порождении из Query производного класса, скажем AndQuery, мы выделяем уникальные характеристики каждого вида запроса. К примеру, NameQuery - это специальный вид Query, в котором операндом всегда является строка. Мы будем называть NameQuery производным и говорить, что Query является его базовым классом. (То же самое относится и к классам, представляющим другие типы запросов.) Производный класс наследует данные и функции-члены базового и может обращаться к ним непосредственно, как к собственным членам.
Основное преимущество иерархии наследования в том, что мы программируем открытый интерфейс абстрактного базового класса, а не отдельных производных от него специализированных типов, что позволяет защитить наш код от последующих изменений иерархии. Например, мы определяем eval() как открытую виртуальную функцию абстрактного базового класса Query. Пользовательский код, записанный в виде:
_rop-eval();
экранирован от любых изменений в языке запросов. Это не только позволяет добавлять, модифицировать и удалять типы, не изменяя программы пользователя, но и освобождает автора нового вида запроса от необходимости заново реализовывать поведение или действия, общие для всех типов в иерархии. Такая гибкость достигается за счет двух характеристик механизма наследования: полиморфизма и динамического связывания.
Когда мы говорим о полиморфизме в языке C++, то имеем в виду главным образом способность указателя или ссылки на базовый класс адресовать любой из производных от него. Если определить обычную функцию eval() следующим образом: