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