Сайт Андрея Зайчикова
|
|
О проблемах множественного наследованияИлья Труб
Множественное наследование, применяемое для описания объектов при объектно-ориентированном программировании, описывая в ряде случаев наблюдаемые связи между существующими реалиями, имеет проблемы технической реализации
Как известно, одним из главных преимуществ объектно-ориентированного программирования является наглядное представление свойств объектов окружающего мира и взаимосвязей между ними. О наиболее удачных примерах эффективного применения этого метода можно сказать: "Как видим, так и программируем". Концепции, позволяющие достичь этого, хорошо известны — инкапсуляция, наследование, полиморфизм. Естественным развитием второй концепции является множественное наследование, воплощенное, например, в языке программирования С++, когда объект некоторого класса наследует свойства объектов двух и более классов. Однако множественное наследование, описывая в ряде случаев наблюдаемые связи между существующими реалиями, имеет проблемы технической реализации. Говоря о проблемах реализации, в первую очередь отмечают совпадение имен переменных и методов у предков класса и неоднозначности пути наследования в случае более чем двухуровневой иерархии. Для того чтобы разрешить эти проблемы, потребовалось ввести дополнительные конструкции, такие как виртуальные базовые классы и полные ссылки на методы. Применение этих конструкций усложняет код программы и снижает его наглядность. У концепции множественного наследования имеются и иные изъяны принципиального характера, не позволяющие имеющимися средствами описать те зависимости, с которыми мы сталкиваемся в реальной жизни. Возьмем в качестве примера шахматную фигуру — ферзя. Фраза "Ферзь ходит как слон и как ладья" полностью описывает его свойства: она абсолютно информативна. С точки зрения логики, это очень четкий, предельно концентрированный пример множественного наследования. Но можно ли "произнести" эту фразу в программе на С++ и ничего больше не добавлять? Иными словами, должен быть работоспособен следующий код, выдержанный в классических традициях объектно-ориентированного анализа и языка С++. Для краткости опущены проверки принадлежности значений параметров конструктора и методов допустимому диапазону. 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(<list of parameters>){
if (A1::method(<list of \
parameters>)!=0) return 1;
else if (A2::method(<list of
parameters>)!=0) return 1;
-
else if (An::method(<list of \
parameters>)!=0) return 1;
else return 0;
}
При данном расширении рассмотренный программный код будет успешно компилироваться и работать. Предложенное решение нельзя считать универсальным, поскольку оно реализует только одну комбинацию условий — дизъюнкцию. Вполне возможно, что имеются примеры объектных отношений, когда потребуется иная логическая функция. Кроме того, существенным ограничением является условие совпадения числа и типов параметров, а также интерпретация возвращаемого методом значения в виде "истина-ложь". Укажем еще один аспект проблемы. Хотя описанный псевдокод действительно реализует дизъюнкцию, на самом деле он неявно предполагает совсем другую логическую функцию — "исключающее ИЛИ", когда из нескольких условий истинным должно быть не хотя бы одно из них, а ровно одно: только в этом случае выбор метода будет однозначен. Для шахматных фигур это естественно заложено в природе самих объектов, но теоретически возможна ситуация, когда условия применимости могут быть выполнены более чем для одной вариации метода, и тогда потребуется доопределить дополнительную функцию выбора. Это будет происходить в тех случаях, когда диапазоны значений параметров, задающих условия применимости для объектов базовых классов, имеют хотя бы одно непустое попарное пересечение, а параметры в вызове метода для объекта производного класса попадут именно в область этого пересечения. Впрочем, основываясь на практическом опыте, можно утверждать, что ситуация, которую покрывает предложенное правило, является наиболее распространенной. Итак, концепция множественного наследования таит в себе немало "подводных камней", и ее полноценная реализация далеко не проста. В этой связи весьма показателен тот факт, что создатели языка Java, во многом базирующегося на С++, отказались от множественного наследования, заменив его менее рискованной реализацией абстрактных интерфейсов (implements) и разрешив обычное наследование (extends) только одного класса. Вообще говоря, практический опыт показывает, что задач, которые хорошо "ложатся" на объекты и для которых использование объектно-ориентированного программирования дает ощутимое преимущество, не так уж много. В большинстве приводимых в литературе примерах применение подобной методологии выглядит совершенно искусственным и ни в чем не убеждает. Об авторе
Илья Труб - сотрудник Сургутского государственного университета.
|
|