3. Демонстрация агрегации и наследования Чтобы проиллюстрировать агрегацию и наследование, рассмотрим построение абстрактного типа данных "Множество" (класс CSet) на основе класса "Связный список" (CList). Объекты класса CList содержат списки целочисленных величин. Допустим, что имеется класс СList со следующим интерфейсом: class СList {
public:
СList(); void AddToFront(int);
int FirstElement();
int Length();
bool IsIncludes(int);
int Remove(int);
...
}; Класс "Список" позволяет добавлять новый узел в начало списка, получать значение первого узла, вычислять количество узлов, проверять, содержится ли значение в списке, и удалять узел с заданным значением из списка.
Предположим, что необходимо создать класс "Множество", который позволяет выполнять такие операции, как добавление элемента в множество, определение количества элементов, проверка принадлежности к множеству.
3.1. Использование агрегации Сначала рассмотрим, как построить класс "Множество" с помощью агрегации. Известно, что класс инкапсулирует внутри себя состояние и поведение. Когда для создания нового класса используется агрегация, то существующий класс включается в новый класс в виде переменной-члена. Ниже показано, что внутри класса СSet заведена переменная-член data –объект класса CList. class CSet {
public:
CSet(); void Add(int);
int Size();
bool IsIncludes(int) private:
CList data;
}; Поскольку объект класса CList является переменной-членом класса "Множество", то этот объект надо инициализировать в конструкторе класса CSet. Если у конструктора объекта нет параметров, то он вызывается автоматически (как в данном случае), иначе его приходится вызывать явно.
Функции-члены класса CSet реализованы с использованием функций-членов класса CList. Например, функции-члены IsIncludes и Size для множества просто вызывают соответствующие функции-члены у списка: int CSet::Size()
{
return data.Length();
} int CSet::IsIncludes( int newValue )
{
return data.IsIncludes( newValue );
} Функция-член Add() для добавления нового элемента в множество оказывается более сложной, т.к. сначала нужно убедиться, что в множестве данный элемент отсутствует (в множестве м.б. единственный элемент с заданным значением): void CSet::Add( int newValue )
{
if ( !IsIncludes( newValue ) )
data.AddToFront( newValue );
} Приведенный пример показывает, как агрегация помогает повторному использованию компонент в программах. Большая часть работы, связанной с хранением данных, в классе CSet проделывается существовавшим ранее классом СList. Графическое изображение отношения агрегации показано на рис. 7.1.
Рис. 7.1. Агрегация. Следует понимать, что при создании нового класса с помощью агрегации классы СList и СSet будут абсолютно различны, и ни один из них не является уточнением другого.
3.2. Использование наследования При использовании наследования новый класс может быть объявлен как подкласс существующего класса. В этом случае все области данных и функции, связанные с исходным классом, автоматически переносятся в подкласс. Подкласс может определять дополнительные переменные и функции. Он переопределяет некоторые функции исходного класса, которые были объявлены в нем как виртуальные.
Рис. 7.2. Наследование. Наследование подробно рассматривалось в 5-й лекции. Графическое изображение наследования приведено на рис. 7.2. Ниже показано, как можно применить наследование для создания класса CSet в форме подкласса класса СList. Подкласс является расширением существующего класса СList, поэтому все функции-члены списка оказываются применимы и к множеству. class CSet : public CList {
public:
CSet();
void Add(int);
int Size();
}; Обратите внимание, что в подклассе не определено никаких новых переменных. Вместо этого переменные класса CList будут использоваться для хранения элементов множества.
Аналогично функции-члены родительского класса можно использовать в подклассе, поэтому не надо объявлять функцию-член IsIncludes – в классе CList есть функция-член с таким же именем и подходящим поведением. Функция-член для добавления в множество нового элемента выполняет вызовы двух функций-членов класса CList: void CSet::Add( int newValue )
{
if ( !IsIncludes( newValue ) )
AddToFront( newValue );
} Сравните этот вариант функции Add с вариантом из п. 3.1. Оба вида отношений – агрегация и наследование – являются мощными механизмами для многократного использования кода. В отличие от агрегации, наследование содержит неявное предположение, что подклассы на самом деле являются подтипами. Это значит, что объекты подкласса должны вести себя так же, как и объекты родительского класса.
3.3. Сравнение агрегации и наследования В п. 3.1-3.2 показано, что оба вида отношений между классами – агрегацию и наследование – можно применить для реализации класса "Множество". На рассмотренном примере перечислим некоторые недостатки и преимущества двух подходов.
Агрегация более проста. Ее преимущество заключается в том, что она ясно показывает, какие точно функции-члены будут содержаться в классе. Из описания класса CSet становится очевидно, что для объектов-множеств предусмотрены только операции добавления элемента, проверки на наличие элемента и определение количества элементов в множестве. Это справедливо независимо от того, какие функции-члены определены в классе CList.
При наследовании функции-члены нового класса дополняют и, возможно, переопределяют набор функций-членов родительского класса. Таким образом, чтобы точно знать, какие функции-члены есть у подкласса, программист должен изучить описание родительского класса. Например, из описания класса CSet не видно, что у множеств можно выполнять проверку на наличие элемента в множестве (функция-член IsIncludes). Это можно понять только после изучения описания родительского класса CList. Т.о., у наследования есть неприятная особенность: чтобы полностью понять поведение и свойства подкласса, программист должен изучать описание одного или нескольких родительских классов.
С другой стороны, указанную выше особенность наследования можно считать преимуществом: описание класса получается более кратким, чем в случае агрегации. Применяя наследование, оказывается ненужным писать все функции для доступа к переменным-членам родительского класса. Наследование также часто обеспечивает большую функциональность. Например, применение наследования в нашем случае делает доступным для множеств не только проверку IsIncludes, но и функцию Remove.
Наследование не запрещает пользователям манипулировать новыми классами через вызовы функций-членов родительского класса, даже если эти функции не вполне подходят под идеологию потомка. Например, при использовании наследования для класса CSet пользователи смогут добавлять элементы в множество с помощью унаследованной от класса CList функции AddToFront.
При агрегации тот факт, что класс СList используется для хранения элементов множеств, – просто скрытая деталь реализации. Т.о., можно легко переписать класс CSet более эффективным способом (например, на основе массива) с минимальным воздействием на пользователей класса CSet. Если же пользователи рассчитывают на то, что класс CSet – это более специализированный вариант класса CList, то такие изменения было бы трудно реализовать.
Как в конкретном случае выбрать один из двух механизмов реализации? Надо воспользоваться правилом подстановки "X является экземпляром Y". Корректно ли звучит утверждение "Множество является экземпляром списка"? Т.е. можно ли в программе, где используется класс CList, подставлять вместо него класс CSet? Исходя из смысла понятий "множество" и "список", можно сказать, что нельзя. Поэтому в данном случае агрегация подходит лучше.
|