Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 48 49 50 51 52 53 54 55 56 ... 73
Перейти на страницу:

Оно обходится не бесплатно еще и по другой причине. Правила, определяющие инициализацию виртуальных базовых классов, сложнее и интуитивно не так понятны, как правила для невиртуальных базовых классов. Ответственность за инициализацию виртуального базового класса ложится на самый дальний производный класс в иерархии. Отсюда следует, что: (1) классы, наследующие виртуальному базовому и требующие инициализации, должны знать обо всех своих виртуальных базовых классах, независимо от того, как далеко они от них находятся в иерархии, и (2) когда в иерархию добавляется новый производный класс, он должен принять на себя ответственность за инициализацию виртуальных предков (как прямых, так и непрямых).

Мой совет относительно виртуальных базовых классов (то есть виртуального наследования) прост. Во-первых, не применяйте виртуальных базовых классов до тех пор, пока в этом не возникнет настоятельная потребность. По умолчанию используйте невиртуальное наследование. Во-вторых, если все же избежать виртуальных базовых классов не удается, старайтесь не размещать в них данных. Тогда можно будет забыть о странностях правил инициализации (да, кстати, и присваивания) таких классов. Неспроста интерфейсы Java и. NET, которые во многом подобны виртуальным базовым классам C++, не могут содержать никаких данных.

Теперь рассмотрим следующий интерфейсный класс C++ (см. правило 31) для моделирования физических лиц:

class IPerson {

public:

virtual ~IPerson();

virtual std::string name() const = 0;

virtual std::string birthDate() const = 0;

};

Пользователи IPerson должны программировать в терминах указателей и ссылок на IPerson, поскольку создавать объекты абстрактных классов запрещено. Для создания объектов, которыми можно манипулировать как объектами IPerson, используются функции-фабрики (опять же см. правило 31), которые порождают объекты конкретных классов, производных от IPerson:

// функция-фабрика для создания объекта Person по уникальному

// идентификатору из базы данных; см. в правиле 18,

// почему возвращаемый тип – не обычный указатель

std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

// функция для запроса идентификатора у пользователя

DatabaseID askUserForDtabaseID();

DatabaseID id(askUserForDtabaseID());

std::tr1::shared_ptr<IPerson> pp(makePerson(id)); // создать объект,

// поддерживающий

// интерфейс IPerson

... // манипулировать *pp

// через функции-члены

// IPerson

Но как makePerson создает объекты, на которые возвращает указатель? Ясно, что должен быть какой-то конкретный класс, унаследованный от IPerson, который makePerson может инстанцировать.

Предположим, этот класс называется CPerson. Будучи конкретным классом, CPerson должен предоставлять реализацию чисто виртуальных функций, унаследованных от IPerson. Можно написать его «с нуля», но лучше воспользоваться уже готовыми компонентами, которые делают большую часть работы. Например, предположим, что старый, ориентированный только на базы данных класс Person-Info предоставляет почти все необходимое CPerson:

class PersonInfo {

public:

explicit PersonInfo(DatabaseID pid)

virtual ~PersonInfo();

virtual const char *theName() const;

virtual const char *theBirthDate() const;

...

private:

virtual const char *valeDelimOpen() const; // ñì. íèæå

virtual const char *valeDelimClose() const;

...

};

Понять, что этот класс старый, можно хотя бы потому, что функции-члены возвращают const char* вместо объектов string. Но если ботинки подходят, почему бы не носить их? Имена функций-членов класса наводят на мысль, что результат может оказаться вполне удовлетворительным.

Вскоре вы приходите к выводу, что класс PersonInfo был спроектирован для печати полей базы данных в различных форматах, с выделением начала и конца каждого поля специальными строками-разделителями. По умолчанию открывающим и закрывающим разделителями служат квадратные скобки, поэтому значение поля «Ring-tailed Lemur» будет отформатировано так:

[Ring-tailed Lemur]

Учитывая тот факт, что квадратные скобки не всегда приемлемы для пользователей PersonInfo, в классе предусмотрены виртуальные функции valeDelimOpen и valeDelimClose, позволяющие производным классам задать другие открывающие и закрывающие строки-разделители. Функции-члены PersonInfo вызывают эти виртуальные функции для добавления разделителей к возвращаемым значениям. Так, функция PersonInfo::theName могла бы выглядеть следующим образом:

const char *PersonInfo::valueDelimOpen() const

{

return “[“; // открывающий разделитель по умолчанию

}

const char *PersonInfo::valueDelimClose() const

{

return “]“; // закрывающий разделитель по умолчанию

}

const char * PersonInfo::theName() const

{

// резервирование буфера для возвращаемого значения; поскольку он

// статический, автоматически инициализируется нулями

static char value[Max_Formatted_Field_Value_Length];

// скопировать открывающий разделитель

std::strcpy(value, valueDelimOpen());

добавить к строке value значение из поля name объекта (будьте осторожны –

избегайте переполнения буфера!)

// скопировать закрывающий разделитель

std::strcpy(value, valueDelimClose());

return value;

}

Кто-то может посетовать на устаревший подход к реализации PersonInfo::theName (особенно это касается использования статического буфера фиксированного размера, опасного возможностью переполнения и потенциальными проблемами в многопоточной среде – см. правило 21), но оставим этот вопрос в стороне и сосредоточимся вот на чем: функция theName вызывает valueDelimOpen для получения открывающего разделителя, вставляемого в возвращаемую строку, затем дописывает имя и в конце вызывает valueDelimClose.

Поскольку valueDelimOpen и valueDelimClose – виртуальные функции, возвращаемый результат theName зависит не только от PersonInfo, но и от классов, производных от него.

Для разработчика CPerson это хорошая новость, потому что, внимательно просматривая документацию по функциям печати из класса IPerson, вы обнаруживаете, что функции name и birthDate должны возвращать неформатированные значения, то есть без добавления разделителей. Другими словами, если человека зовут Homer, то вызов функции name должен возвращать «Homer», а не «[Homer]».

Взаимосвязь между CPerson и PersonInfo можно описать так: PersonInfo упрощает реализацию некоторых функций CPerson. И это все! Стало быть, речь идет об отношении «реализован посредством», и, как мы знаем, такое отношение можно представить двумя способами: с помощью композиции (см. правило 38) или закрытого наследования (см. правило 39). В правиле 39 отмечено, что композиция в общем случае более предпочтительна, но если нужно переопределять виртуальные функции, то требуется наследование. В данном случае CPerson должен переопределить valueDelimOpen и valueDelimClose – задача, которая с помощью композиции не решается. Самое очевидное решение – применить закрытое наследование CPerson от PersonInfo, хотя, как объясняется в правиле 39, это потребует несколько больше работы. Можно также при реализации CPerson воспользоваться сочетанием композиции и наследования с целью переопределения виртуальных функций PersonInfo. Но мы остановимся просто на закрытом наследовании.

Однако CPerson также должен реализовать интерфейс IPerson, а для этого требуется открытое наследование. Вот мы и пришли к множественному наследованию: сочетанию открытого наследования интерфейса с закрытым наследованием реализации:

class IPerson { // класс описывает интерфейс,

public: // который должен быть реализован

virtual ~IPerson();

virtual std::string name() const = 0;

virtual std::string birthDate() const = 0;

};

class DatabaseID {...}; // используется далее;

// детали не существенны

class PersonInfo { // в этом классе имеет функции,

public: // помогающие при реализации

explicit PersonInfo(DatabaseID pid) // интерфейса IPerson

virtual ~PersonInfo();

virtual const char *theName() const;

virtual const char *theBirthDate() const;

virtual const char *valeDelimOpen() const;

virtual const char *valeDelimClose() const;

...

};

class CPerson: public IPerson, private PersonInfo { // используется

public: // множественное

explicit CPerson(DatabaseID pid): PersonInfo(pid) {} // наследование

virtual std::string name() const // реализации

{ return PersonInfo::theName();} // функций-членов

// из интерфейса

// IPerson

virtual std::string birthDate() const

{ return PersonInfo::theBirthDate();}

private: // переопределения

const char * valeDelimOpen() const { return “”;} // унаследованных

const char * valeDelimClose() const { return “”;} // виртуальных

}; // функций,

// возвращающих

// строки-разделители

В нотации UML это решение выглядит так:

Рассмотренный пример показывает, что множественное наследование может быть и удобным, и понятным.

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

1 ... 48 49 50 51 52 53 54 55 56 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии