CHAP8_1 (1018808), страница 3
Текст из файла (страница 3)
Одной из больших проблем плохо организованных иерархий является превышение багажной нормы . Базовые классы должны иметь поля для поддержки возможностей, реализуемых различными обработчиками. Если производный класс не использует такую возможность, тогда его объект распространяет всюду связанные с ним накладные расходы, не давая выгод. Это одна из проблем иерархии в стиле Smalltalk, имеющей одну вершину в виде общего объекта. Все поля, помещенные в него вами (и все ячейки в таблице виртуальных функций), будут переняты каждым объектом в системе, независимо от того, использует объект эти поля, или нет.
Лучшим способом избежать этой проблемы является использование множественного наследования для реализации классов-смешений. Вот как смешение работает. Возвратившись к нашему примеру с employee, вы могли бы реализовать его в виде системы классов следующим образом:
class employee
{
// содержит всю информацию, общую для всех служащих:
// фамилия, адрес и т.д.
};
class manager : public employee
{
// добавляет информацию, специфичную для управляющего, такую, как
// список подчиненных служащих. Управляющий тоже является служащим,
// поэтому применимо наследование
database list_of_managed_emploees;
}
class peon : public employee
{
// добавляет информацию, специфичную для поденщика
manager *this_boss;
}
Все это приемлемо до тех пор, пока не приходит время создавать наш список объектов employee, который поддерживается объектом manager. Во многих реализациях структур данных объект делается сохраняемым путем наследования его класса от класса, который реализует то, что нужно структуре данных для работы по назначению. Вы могли бы сделать это здесь так:
class storable; // сохраняемый
class employee : public storable { /* ... */ };
class manager : public employee { /* ... */ };
class peon : public employee { /* ... */ };
Например, метод add() класса database мог бы получать указатель на объект storable в качестве своего аргумента. Таким способом любой объект storable (или объект, производный от storable) может быть добавлен в database без необходимости модифицировать что-либо в программе, в состав которой входит класс database.
Все кажется правильным до тех пор, пока мы реально не взглянем на то, как используются классы. Давайте скажем, что это средняя фирма, где число управляющих относится к числу поденщиков как 100 к 1. Однако списка управляющих нет, есть лишь список поденщиков. Тем не менее, каждый manager несет на себе излишнюю возможность сохраняемости, хотя она никогда не используется. Решим эту проблему при помощи множественного наследования.
class storable;
class employee { /* ... */ };
class manager : public employee { /* ... */ };
class peon : public employee, public storable { /* ... */ };
Проблема здесь в том, что эта "сохраняемость" является атрибутом объекта. Это не является базовым классом в стандартном смысле типа "круг является фигурой", а скорее - "поденщик является сохраняемым". Здесь важна замена существительного на прилагательное. Базовый класс, который реализует "свойство" типа сохраняемости, называется классом-смешением, потому что вы можете примешивать это свойство к тем классам, которым оно нужно, и только к этим классам. Хороший метод распознавания этих двух употреблений наследования состоит в том, что имя класса-смешения обычно выражено прилагательным (сохраняемый, сортируемый, устойчивый, динамический и т.д.). Именем настоящего базового класса обычно является существительное.
Вследствие природы С++ во всех учебниках рассматривается несколько проблем с множественным наследованием, большинство из которых вызывается ромбовидной иерархией классов:
class parent {}; // родитель
class mother : public parent {}; // мать
class father : public parent {}; // отец
class child : public mother, public father {} // потомок
Здесь имеется две трудности. Если у parent есть метод для укладывания спать с названием go_to_sleep(), то вы получите ошибку, попытавшись послать такое сообщение:
child philip; // Филипп - потомок
philip.go_to_sleep(); // Филипп, иди спать!
Проблема состоит в том, что в объекте child на самом деле два объекта parent. Запомните, что наследование просто добавляет поля (данные-члены) и обработчики сообщений (функции-члены). Объект mother имеет компонент parent: он содержит дополнительно к своим собственным все поля parent.7 То же самое относится и к father. Затем, у child есть mother и father, у каждого из которых есть parent. Проблема с philip.go_to_sleep() состоит в том, что компилятор не знает, какой из объектов parent должен получить это сообщение: тот, который в mother, или тот, который в father.8
Одним из путей решения этой проблемы является введение уточняющей функции, которая направляет сообщение нужному классу (или обоим):
class parent { public: go_to_sleep(); };
class mother : public parent {};
class father : public parent {};
class child : public mother, public father
{
public:
go_to_sleep()
{
mother::go_to_sleep();
father::go_to_sleep();
}
}
Другим решением является виртуальный базовый класс:
class parent {};
class mother : virtual public parent {};
class father : virtual public parent {};
class child : public mother, public father {}
который заставляет компилятор помещать в объект child лишь один объект parent, совместно используемый объектами mother и father. Двусмысленность исчезает, но появляются другие проблемы. Во-первых, нет возможность показать на уровне потомка, хотите вы или нет виртуальный базовый класс. Например, в следующем коде tree_list_node может быть членом как дерева, так и списка одновременно:
class node;
class list_node : public node {};
class tree_node : public node {};
class tree_list_node : public list_node, public tree_node {};
В следующем варианте tree_list_node может быть членом или дерева, или списка, но не обоих одновременно:
class node;
class list_node : virtual public node {};
class tree_node : virtual public node {};
class tree_list_node : public list_node, public tree_node {};
Вам бы хотелось делать этот выбор при создании tree_list_node, но такой возможности нет.
Второй проблемой является инициализация. Конструкторы в list_node и tree_node, вероятно, инициализируют базовый класс node, но разными значениями. Если имеется всего один node, то какой из конструкторов выполнит эту инициализацию? Ответ неприятный. Инициализировать node должен наследуемый последним производный класс (tree_list_node). Хотя это действительно плохая мысль - требовать, чтобы класс знал о чем-либо в иерархии, кроме своих непосредственных родителей - иначе было бы слишком сильное внутреннее связывание.
Обратная сторона той же самой проблемы проявляется, если у вас есть виртуальные функции как в следующем коде:
class persistent
{
public:
virtual flush() = 0;
};
class doc1: virtual public persistent
{
public:
virtual flush() { /* сохранить данные doc1 на диске */ }
};
class doc2: virtual public persistent
{
public:
virtual flush() { /* сохранить данные doc2 на диске */ }
};
class superdoc : public doc1, public doc2 {};
persistent *p = new superdoc();
p->flush(); // ОШИБКА: какая из функций flush() вызвана?
102. Смешения не должны наследоваться от чего попало.
103. Смешения должны быть виртуальными базовыми классами.
104. Инициализируйте виртуальные базовые классы при помощи конструктора, используемого по умолчанию.
Вы можете свести до минимума рассмотренные ранее проблемы, стараясь придерживаться следующих правил (многие смешения не могут соответствовать им всем, но вы делайте все от вас зависящее):
-
Если можно, то смешения не должны наследоваться от чего попало, тем самым полностью устраняя проблему ромбовидной иерархии при множественном наследовании.
-
Для смешения должна обеспечиваться возможность быть виртуальным базовым классом для того, чтобы не возникала проблема неопределенности в случае, если у вас все же получилась ромбовидная структура классов.
-
Если можно, то смешение должно всегда строиться с использованием только конструктора по умолчанию (не имеющего аргументов). Это упрощает оформление смешения в качестве виртуального базового класса, потому что вам не нужно будет заботиться о инициализации большей части наследуемого объекта. В конце концов, по умолчанию всегда используется конструктор по умолчанию.
105. Наследование не подходит, если вы никогда не посылаете сообщения базового класса объекту производного класса.
106. Везде, где можно, предпочитайте включение наследованию.
107. Используйте закрытые базовые классы лишь когда вы должны обеспечить виртуальные замещения.
Главная выгода от наследования состоит в том, что вы можете писать универсальный код, манипулирующий объектами обобщенного базового класса, и тот же самый код может также манипулировать объектами производного класса (или точнее, может манипулировать компонентом базового класса в объекте производного класса). Например, вы можете написать функцию, которая печатает список объектов фигура, но этот список на самом деле содержит объекты, которые унаследованы от фигуры, такие как круг и линия. Тем не менее, функции печати этого знать не нужно. Она вполне довольна, считая их обобщенными фигурами. Это качество является тем, что имеют в виду, когда говорят о повторном использовании кода. Вы повторно используете один и тот же код для разных дел: временами он печатает круг, временами - линию.
Если вы обнаружили у себя объект производного класса, от которого никогда не требуется использовать возможности базового класса, то, вероятно, в проектировании иерархии есть какая-то ошибка, хотя встречаются редкие случаи, когда такое поведение приемлемо; поэтому в языке есть закрытые базовые классы. Но все же включение (назначение объекта полем в классе, а не базовым классом) всегда лучше, чем наследование (при условии, конечно, что у вас есть выбор).
Если объект производного класса никогда не получает сообщения базового класса, то вероятнее всего компонент базового класса в объекте производного класса действительно должен быть полем, и наследование вовсе не должно использоваться. Вместо вот этого:
class derived : public base
{
};
вам почти всегда лучше делать так:
class derived
{
base base_obj;
};
Используйте закрытые базовые классы лишь в случаях, когда вам нужно в производном классе перегружать виртуальные функции базового класса.
Удачный пример подобного неправильного использования наследования есть во многих иерархиях классов для Windows, которые наследуют классы типа "диалоговое окно" от "окна". Однако в реальной программе вы никогда не посылаете относящиеся к окну сообщения (типа "сдвинуться" или "изменить размер") в диалоговое окно. То есть диалоговое окно не является окном, по крайней мере с точки зрения того, как диалоговое окно используется в программе. Скорее диалоговое окно использует окно, чтобы себя показать. Слово "является" подразумевает наследование, а "использует" - включение, которое здесь лучше подходит.
Подобное плохое проектирование, между прочим, обычно имеет причиной отступление от правила определения объектов в первую очередь. То есть концепция "окна" в Microsoft Windows имеет смысл только для подсистемы визуального вывода. Диалоговое окно изображается в виде окна, но это не значит, что это окно, даже если подсистема визуального вывода предпочитает его рассматривать в этом качестве. Плохое проектирование получается, когда исходят из существующей системы визуального вывода и затем помещают вокруг нее оболочку при помощи библиотеки классов, вместо того, чтобы исходить из описания программы, решая затем, как реализовать в программе реальные объекты.
108. Проектируйте структуры данных в последнюю очередь.
Добавление полей данных выполняется в процессе проектирования в последнюю очередь. Другими словами, после того, как вы разработали сообщения, вам нужно понять, как реализовать возможности, запрашиваемые этими сообщениями. Вероятно, это труднейшая часть процесса объектно-ориентированного проектирования для структурного программиста: заставить себя не думать о лежащей в основе структуре данных до тех пор, пока не будет готовы полностью система обмена сообщениями и иерархия классов.
В этот момент процесса проектирования вы также добавляете закрытые (private) "рабочие" (или "вспомогательные") функции, которые помогают обработчикам сообщений справиться со своей работой.















