Множественное наследование, применяемое для описания объектов при объектно-ориентированном программировании, описывая в ряде случаев наблюдаемые связи между существующими реалиями, имеет проблемы технической реализации

Как известно, одним из главных преимуществ объектно-ориентированного программирования является наглядное представление свойств объектов окружающего мира и взаимосвязей между ними. О наиболее удачных примерах эффективного применения этого метода можно сказать: «Как видим, так и программируем». Концепции, позволяющие достичь этого, хорошо известны — инкапсуляция, наследование, полиморфизм. Естественным развитием второй концепции является множественное наследование, воплощенное, например, в языке программирования С++, когда объект некоторого класса наследует свойства объектов двух и более классов. Однако множественное наследование, описывая в ряде случаев наблюдаемые связи между существующими реалиями, имеет проблемы технической реализации.

Говоря о проблемах реализации, в первую очередь отмечают совпадение имен переменных и методов у предков класса и неоднозначности пути наследования в случае более чем двухуровневой иерархии. Для того чтобы разрешить эти проблемы, потребовалось ввести дополнительные конструкции, такие как виртуальные базовые классы и полные ссылки на методы. Применение этих конструкций усложняет код программы и снижает его наглядность. У концепции множественного наследования имеются и иные изъяны принципиального характера, не позволяющие имеющимися средствами описать те зависимости, с которыми мы сталкиваемся в реальной жизни. Возьмем в качестве примера шахматную фигуру — ферзя. Фраза «Ферзь ходит как слон и как ладья» полностью описывает его свойства: она абсолютно информативна. С точки зрения логики, это очень четкий, предельно концентрированный пример множественного наследования. Но можно ли «произнести» эту фразу в программе на С++ и ничего больше не добавлять? Иными словами, должен быть работоспособен следующий код, выдержанный в классических традициях объектно-ориентированного анализа (рис. 1) и языка С++. Для краткости опущены проверки принадлежности значений параметров конструктора и методов допустимому диапазону.

Рис. 1. Иерархия классов шахматных фигур
enum coord1 {a ,b ,c, d, e, f, g, h};
enum  color {Черный, Белый};
/* абстрактный класс Фигура — общий
 предок всех остальных */
class Фигура
{coord1 буква;	//1-я координата a..h
int цифра;	//2-я координата 1..8
color цвет;	//цвет фигуры
public:
//конструктор
Фигура(coord1 x, int y, color z)
{буква=x;
цифра=y;
цвет=z;
}
  /*чисто виртуальная функция «ход» —
 реализации в этом классе не имеет*/
  virtual int ход(coord1 новая_буква, 
int новая_цифра)=0;
}
/* Класс Ладья реализует 
 функцию «ход» */
class Ладья: public virtual Фигура {
public:
int ход(coord1 новая_буква, 
int новая_цифра) {
if (((новая_буква==буква)&& 
(новая_цифра!=цифра))||
((новая_буква!=буква) &&
(новая_цифра==цифра))) {
буква=новая_буква; 
цифра=новая_цифра; return 1;
}
  else return 0;
  }
/* Класс Слон реализует 
свою функцию ход */
class Слон: public virtual Фигура {
public:
int ход(coord1 новая_буква, 
int новая_цифра) {
if((abs((новая_буква-буква)==
abs (новая_цифра-цифра)) &&
(новая_буква!=буква)){
буква=новая_буква;
цифра=новая_цифра; return 1;
}
  else return 0;
}
/* Класс Ферзь — сказано лишь,
что он наследник классов
Ладья и Слон */
class Ферзь: public Слон, public Ладья{}
main(){
Ферзь q(e,5,Белый);
q.ход(h, 8);
}

Отметим, что свойство цвет в рассмотренном примере не существенно, и добавлено для общности и завершенности описания шахматной фигуры как объекта, стоящего на шахматной доске, а не лежащего в коробке. Так, для рассмотренных фигур реализация функции ход, конечно же, не зависит от цвета, так как они ходят и вперед, и назад, но, например, функция ход для класса Пешка уже должна будет располагать информацией о цвете для определения допустимости хода. Если же предположить существование на доске и других фигур, то для определения того, может ли одна фигура сбить другую, информация о цвете будет необходима уже для всех видов фигур.

Однако любой программист, имеющий опыт работы с языком С++, сразу же скажет, что такой код работать не будет более того, его не удастся даже откомпилировать из-за неоднозначности наследования функции ход. Одно из решений — функцию ход для класса Ферзь реализовать явно, например:

int Ферзь::ход(coord1 новая_буква, 
int новая_цифра){
    if (Ладья::ход(coord1 новая_буква, 
int новая_цифра)!=0) return 1;
    else if (Слон::ход(coord1 новая_буква, 
int новая_цифра)!=0) return 1;
    else return 0;
}

Однако в этом случае теряется смысл множественного наследования, ибо тогда зачем описывать класс Queen наследником слона и ладьи?

Таким образом, указанное отношение множественного наследования между объектами существующими средствами выразить нельзя. Для того чтобы устранить этот недостаток, можно предложить следующее расширение (или, как принято говорить, patch) для компилятора С++.

Пусть класс B является производным от классов A1, A2, ..., An. Пусть, кроме того, в каждом из родительских классов имеется реализация некоторого метода method, каждая из которых совпадает по количеству и типам входных параметров, а возвращает 0 или 1. Тогда, если в протоколе класса B отсутствует явная реализация метода method, по умолчанию она должна иметь следующий вид:

int B::method(){
if (A1::method()!=0) return 1;
else if (A2::method()!=0) return 1;
…
else if (An::method()!=0) return 1;
else return 0;
}

При данном расширении рассмотренный программный код будет успешно компилироваться и работать.

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

Укажем еще один аспект проблемы. Хотя описанный псевдокод действительно реализует дизъюнкцию, на самом деле он неявно предполагает совсем другую логическую функцию — «исключающее ИЛИ», когда из нескольких условий истинным должно быть не хотя бы одно из них, а ровно одно: только в этом случае выбор метода будет однозначен. Для шахматных фигур это естественно заложено в природе самих объектов, но теоретически возможна ситуация, когда условия применимости могут быть выполнены более чем для одной вариации метода, и тогда потребуется доопределить дополнительную функцию выбора. Это будет происходить в тех случаях, когда диапазоны значений параметров, задающих условия применимости для объектов базовых классов, имеют хотя бы одно непустое попарное пересечение, а параметры в вызове метода для объекта производного класса попадут именно в область этого пересечения. Впрочем, основываясь на практическом опыте, можно утверждать, что ситуация, которую покрывает предложенное правило, является наиболее распространенной.

Итак, концепция множественного наследования таит в себе немало «подводных камней», и ее полноценная реализация далеко не проста. В этой связи весьма показателен тот факт, что создатели языка Java, во многом базирующегося на С++, отказались от множественного наследования, заменив его менее рискованной реализацией абстрактных интерфейсов (implements) и разрешив обычное наследование (extends) только одного класса. Вообще говоря, практический опыт показывает, что задач, которые хорошо «ложатся» на объекты и для которых использование объектно-ориентированного программирования дает ощутимое преимущество, не так уж много. В большинстве приводимых в литературе примерах применение подобной методологии выглядит совершенно искусственным и ни в чем не убеждает.

Илья Труб (trub@surgu.wsnet.ru) — сотрудник Сургутского государственного университета.

Поделитесь материалом с коллегами и друзьями