Жанр: Учеба

Правила программирования на Си и Си плюс плюс.

сторінка №11

ений вывел бы диалоговое окно,
которое бы использовалось пользователем для ввода или изменения
данных.
Главным преимуществом этого подхода является то, что отправитель
сообщения может меньше волноваться о том, как организовано
внутреннее хранение данных. Пока объект может себя печатать,
модифицировать или делать что-нибудь еще - проблемы нет. Вы можете
перевести name на Unicode, не затрагивая отправителя сообщения. Этот
вопрос рассматривается далее во многих правилах этой главы книги.

95. Вам обычно не удастся переделать имеющуюся
структурную программу в объектноориентированную

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

4 Чтобы быть строго корректным, по крайней мере на языке выражений Си++, я
должен называть поле "компонентом данных-членов". Однако довольно неудобно
говорить "компонент данных-членов name", поэтому буду использовать просто
"поле", когда его значение ясно из контекста.


Правила программирования на Си++

140


Возвращаясь вновь к печати, отметим, что соответствующим сообщением
могло бы быть "воспроизвести себя на этом устройстве", а обработчику
сообщения могла быть передана ссылка на обобщенный объект device,
которому нужно переслать данные. Код, который фактически выполняет
воспроизведение, на самом деле находится внутри этого объекта. (В
порядке разъяснения: нет причины, из-за которой нельзя поддерживать
несколько сообщений типа "воспроизведи себя". Например, объект
электронной таблицы мог бы поддерживать сообщения "воспроизвести
себя в виде таблицы", "воспроизвести себя в виде графика" и
"воспроизвести себя в виде круговой диаграммы").
В структурной системе код, который выполняет воспроизведение,
является внешним. Некая функция получает откуда-то объект, после чего
делает различные системные вызовы для вывода его на экран. Если вы
говорите о printf(), то вызовы не очень сложные, но если речь заходит
о Windows или Motif - у вас появляется проблема. Объектноориентированный
проект фактически является вывернутым наизнанку в
сравнении со структурным проектом.
Преимущество объектно-ориентированного подхода - в том, что вы
можете менять операционную среду путем изменения реализации объекта
device
, и при этом остальной код в программе не затрагивается.
Несмотря на это, вызванные изменения столь фундаментальны, что
полный перевод возможен лишь после переработки в программе каждой
функции, которая прямо вызывает функцию операционной системы. Это
нетривиальное мероприятие, которое вероятно потребует отбрасывания
большей части кода в существующем приложении.
Сообщение "модифицировать себя" аналогично: диалоговое окно
модификации в стандартной структурной программе изображается
внешним кодом. В объектно-ориентированном проекте объект сам
взаимодействует с пользователем в ответ на получение сообщения
"модифицировать себя" - наизнанку в сравнении со структурным
подходом. И снова преимущество в том, что изменения полей, которые
должны быть модифицированы, концентрируются в определении класса.
Вам не нужно искать по всей программе код, который использует
объекты класса, каждый раз, когда меняется поле в определении класса.
Мой опыт с гибридными приложениями не очень удачен: кажется, что
в них соединяются все проблемы как структурных, так и объектноориентированных
систем без каких то преимуществ тех и других. Это
реальная опасность для тех, у кого "нет времени, чтобы делать все
правильно" - они могут получить в итоге несопровождаемый гибрид.


Вопросы проект ирования и реализации 141

Ошибочно рассматривать тело существующего кода, вне зависимости
от его размера, в качестве "ценного имущества", в которое вы должны
постоянно инвестировать. Вы не выбросите деньги, потраченные на
написание существующего кода, когда решитесь от него отказаться.
Деньги, потраченные на написание кода, уже, наверное, окупились за счет
продаж, и теперь, чтобы чего-то достичь, вы должны не дорабатывать
существующий код, а писать новый. Начинающая фирма-конкурент,
способная лишь куснуть вас за пятку, разрабатывает свой продукт с
самого начала и получает преимущество за счет использования
современной технологии и идей по методологии проектирования. Между
тем ваш существующий код запирает вас в рамках обветшалого проекта и
устаревшей технологии. Нельзя просто откинуться на спинку кресла и
почить на лаврах - вы должны постоянно переписывать свой продукт
заново, чтобы совершенствовать его сколь-нибудь заметным образом.

Я должен сказать, что многие со мной в этом месте не согласны.
Рецензент одной из моих недавних статей ответил на (возможно, слишком
упрощенное) утверждение, что гибридные приложения "не работают",
заявив: "Я знаю массу торгово-транспортных приложений, которые
написаны именно таким образом, приносят прибыль своим создателям и,
следовательно, работают". С другой стороны, тот факт, что этот рецензент
работает в фирме, владеющей несколькими огромными гибридными
приложениями, очевидно, повлиял на его отзыв. Один из этих гибридов
задерживался с выходом на рынок более чем на год во время, когда
писалась рецензия, и сопровождение было постоянным кошмаром для
большинства остальных, но я думаю, что эти проблемы были
недостаточно важными, чтобы их учитывать, потому что этот конкретный
программист не занимался сопровождением.

96. Объект производного класса является объектом
базового класса
97. Наследование - это процесс добавления полей
данных и методов-членов
В Си++ производный класс может рассматриваться как механизм
добавления полей данных и обработчиков сообщений к существующему
определению класса - к базовому классу. (Вы можете также смотреть на
наследование как на средство изменения поведения объекта базового
класса при получении им конкретного сообщения. Я вернусь к такой
точке зрения при обсуждении виртуальных функций). В таком случае
иерархия классов является просто средством представления полей данных


Правила программирования на Си++

142


и методов, определяемых для конкретного объекта. Объект содержит все
данные и методы, объявленные на его уровне, а также на всех
вышележащих уровнях.
Общая ошибка, совершаемая начинающими программистами на Си++,
состоит в том, что они смотрят на иерархию классов и думают, что
сообщения передаются от объектов производного класса к объектам
базового класса. Помните, что иерархия классов Си++ не существует во
время выполнения. Все, что у вас есть во время выполнения, это
фактические объекты, чьи поля определяются во время компиляции при
помощи иерархии классов.
В этом вопросе путаница создана многими книгами по языку Smalltalk,
описывающими реализацию во время выполнения системы обработки
сообщений так, как если бы сообщения передавались от производного к
базовому классу.5 Это просто неверно (и в случае Smalltalk, и в случае
Си++). Си++ использует наследование. Производный класс - это тот же
базовый класс, но с несколькими добавленными полями и обработчиками
сообщений. Следовательно, когда объект Си++ получает сообщение, он
или обрабатывает его, или нет; он или определяет обработчик, или
получает его в наследство. Если ни то, ни другое не имеет места, то
сообщение просто не может быть обработано. И оно никуда не
передается.
Не путайте отношение наследования с объединением. При
объединении в один класс (контейнер) вложен объект другого класса (в
отличие от наследования от другого класса). Объединение в
действительности лучше, чем наследование, если у вас есть возможность
выбора, потому что отношения сцепления между вложенным объектом и
внешним миром гораздо слабее, чем отношения между базовым классом и
внешним миром.

5 Они не передаются. Даже в Smalltalk есть только один объект, который или
получает сообщение, или нет. Несмотря на это, интерпретаторы Smalltalk склоняются
к реализации обработки сообщений при помощи нескольких таблиц указателей на
функции, по одной на каждый класс. Если интерпретатор не может найти обработчик
сообщения в таблице диспетчеризации производного класса, то он просматривает
таблицу базового класса. Этот механизм не используется в Си++, который является
компилируемым языком и поэтому не использует многоуровневый просмотр таблиц в
время выполнения. Например, даже если бы все функции в базовом классе были
виртуальными, то таблица виртуальных функций производного класса имела бы по
ячейке для каждой виртуальной функции базового класса. Среда времени выполнения
Си++ не просматривает иерархию таблиц, а просто использует таблицу для текущего
объекта. Подробнее об этом позднее.


Вопросы проект ирования и реализации 143

Объединение позволяет методам контейнера действовать подобно
фильтру, через который передаются сообщения, предназначенные для
вложенного объекта. Обработчики сообщений часто будут иметь
одинаковые имена в контейнере и во вложенном объекте. Например:
class string // строка
{
// ...

public:
const string &operator=( const string &r );
};

class numeric_string // строка, содержащая число
{
string str;
// ...
public:
const string &operator=( const string &r );
}

const string &numeric_string::operator=( const string &r )
{
if
( r.all_characters_are_digits() ) // все символы - цифры
str = r;
else
throw invalid_assignment();

return *this;
}

Это на самом деле довольно слабый пример объединения, потому что,
если бы функция operator=() была виртуальной в базовом классе, то
объект numeric_string мог бы наследовать от string и заместить
оператор присваивания для проверки на верное числовое значение. С
другой стороны, если перегруженная операция сложения + в классе
string выполняет конкатенацию, то вам может понадобиться
перегрузить +
в классе numeric_string
для выполнения
арифметического сложения (т.е. преобразовывать строки в числа, которые
складывать и затем присваивать результат строке). Объединение в
последнем случае решило бы немного проблем.
Возвращаясь к наследованию, отметим, что объекты классов,
показанных в таблице 3, вероятно будут размещаться в памяти одинаково.
Каждое из этих определений, как вы заметили, имеет компонент
some_cls
, но доступ к этому компоненту требует совершенно разных
процедур и механизмов. В этой книге я использую выражение "компонент
базового класса" по отношению к той части объекта, которая определена
на уровне базового класса, а не к вложенному объекту. То есть, когда я


Правила программирования на Си++

144


говорю, что объект производного класса имеет "компонент базового
класса", то имею в виду, что некоторые из его полей и обработчиков
сообщений определены на уровне базового класса. При рассмотрении
вложенного объекта я буду называть его "полем" или "вложенным
объектом".

Таблица 3. Два определения класса, одинаково представляемые на
уровне машинного кода

Объединение Наследование
class container
{
some_cls contained;
// ...
};
class base : public some_cls
{
// ...

};

98. Сначала проектируйте объекты
Первым пунктом повестки дня всегда должно быть проектирование
системы обмена сообщениями, обычно посредством диаграмм объектов
типа описанных Бучем. Начиная с иерархии классов, вы проявляете
склонность к избыточному проектированию, реализуя возможности,
которые не нужны. Кроме того, не зная, как нужно связать объекты друг с
другом, обычно трудно сказать заранее, какие возможности потребуются
в каждом классе. Тяжело обобщать, когда у вас нет деталей.


99. Затем проектируйте иерархию снизу вверх
После того, как вы спроектировали систему объектов и сообщений, вы
можете приступать к иерархии. Откиньтесь на спинку кресла и взгляните
на различные объекты, и вы увидите, что многие из них получают
похожие сообщения. Если два сообщения, посылаемые к разным
объектам, похожи, но не одинаковы, то вам может подойти слегка более
общее компромиссное, которое сможет работать и в том, и в другом
месте. Обработчики для всех общих сообщений должны быть
сконцентрированы в единый базовый класс. Например, имея один объект,
получающий сообщения A, B и C, и второй объект, получающий A, B, D и
E, вы должны остановиться на маленькой иерархии классов, в которой
базовый класс реализует обработчики сообщений для A и B, один
производный класс реализует обработчик для C, а второй производный
класс - обработчики для D и E. Вы продолжаете этот процесс
соединения общих элементов в общие базовые классы до тех пор, пока
нечего будет соединять. Теперь у вас есть иерархия базовых классов.
Вы заметите, что, чем более общим является класс, тем выше он


Вопросы проект ирования и реализации 145

расположен в иерархии. Например, класс управляющего manager
вероятно имеет все свойства класса обобщенного служащего employee, а
также несколько дополнительных свойств (таких, как список
подчиненных служащих). Тогда для manager имеет смысл наследовать
от employee, потому что это добавит возможности, отсутствующие в
базовом классе employee.
На этом этапе процесса проектирования вы все еще даже не подумали
о том, что же должно быть внутри объектов. Вы по-прежнему имеете дело
только с системой обмена сообщениями.
Последним шагом на этом этапе проектирования - после того, как вы
сделали эскиз проекта иерархии классов - остается запись определений
классов. Вы добавите открытые (public) функции-члены для каждого
сообщения, получаемого объектом. Эти обработчики сообщений
являются единственными открытыми членами вашего определения
класса. Все остальное должно быть закрытым или защищенным.
Подробнее об этом далее.

99.1. Базовые классы должны иметь более одного
производного объекта
Это просто другая точка зрения на предыдущее правило. Если базовый
класс является способом концентрации сходных свойств в одном месте,
то есть смысл в том, чтобы никогда не иметь всего один производный
класс. Если он у вас один, то возможности этого единственного потомка
должны быть переданы родителю.

100. Возможности, определенные в базовом классе,
должны использоваться всеми производными
классами
101. Си++ - это не Smalltalk: избегайте общего
класса object
Процесс разработки иерархии снизу вверх обычно дает вам лес из
маленьких деревьев, скорее широких, чем высоких. Построение иерархии
снизу вверх поможет вам избежать общей проблемы для иерархий
классов Си++: класса object, от которого наследуется все в системе, как
в Smalltalk. Такой проект хорош для Smalltalk, но, как правило, не
работает в Си++. Какое свойство мог бы реализовывать этот общий
object
? То есть, какое свойство должен иметь каждый объект каждого
класса в вашей программе? Единственное, что приходит на ум, это -


Правила программирования на Си++

146


управление памятью, способность объекта себя создать. Это делается в
Си++ посредством оператора new, который в действительности является
функцией глобального уровня. Фактически вы можете смотреть на
глобальный уровень Си++, как на функциональный эквивалент object в
Smalltalk. Хорошая иерархия классов Си++ представляет собой обычно
коллекцию иерархий меньшего размера. Процитируем такого авторитета,
как самого Бьярна Страуструпа - создателя Си++ - по этому поводу6 :

Смысл здесь заключается в том, что те стили, которые
подходят и хорошо поддерживаются в Smalltalk, не обязательно
подходят для Си++. В частности, рабское следование стилю
Smalltalk в Си++ ведет к неэффективным, уродливым и с трудом
сопровождаемым программам на Си++. Причина в том, что
хороший Си++ требует проекта, который извлекает
преимущества из системы статических типов Си++, а не
борется с ней. Smalltalk поддерживает систему динамических
типов (и только) и эта точка зрения, переведенная на Си++,
ведет к чрезвычайно ненадежному и уродливому приведению
типов.

!Вдобавок, Smalltalk поощряет людей смотреть на
наследование, как на единственный, или, по меньшей мере,
основной метод организации программ, и организовывать классы
в иерархии с единственной вершиной. В Си++ классы являются
типами, и наследование ни в коем случае не является
единственным средством организации программ. В частности,
шаблоны являются основным средством представления
контейнерных классов.

Одной из больших проблем плохо организованных иерархий является
превышение багажной нормы. Базовые классы должны иметь поля для
поддержки возможностей, реализуемых различными обработчиками. Если
производный класс не использует такую возможность, тогда его объект
распространяет всюду связанные с ним накладные расходы, не давая
выгод. Это одна из проблем иерархии в стиле Smalltalk, имеющей одну
вершину в виде общего объекта. Все поля, помещенные в него вами (и все
ячейки в таблице виртуальных функций), будут переняты каждым
объектом в системе, независимо от того, использует объект эти поля, или
нет.
Лучшим способом избежать этой проблемы является использование

6 Эта цитата является отрывком из статьи, размещенной Страуструпом в
телеконференции BIX в декабре 1992 г. Полностью статья опубликована в книге
Мартина Хеллера "Advanced Win32 Programming"(New York: Wiley,1993), pp.72-78.


Вопросы проект ирования и реализации 147

множественного наследования для реализации классов-смешений. Вот как
смешение работает. Возвратившись к нашему примеру с 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 обладает излишней сохраняемостью, которая никогда


Правила программирования на Си++

148


не используется. Решим эту проблему при помощи множественного
наследования.

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: он содержит дополнительно к своим


Вопросы проект ирования и реализации 149

собственным все поля 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.
Двусмысленность исчезает, но появляются другие проблемы. Во-первых,
нет возможности показать на уровне потомка, хотите вы или нет

7 Не путайте этот процесс с объединением. У mother нет поля parent, скорее
та часть mother, которая определена на уровне базового класса, изображается как
"компонент parent".
8 На самом деле правильнее сказать, что во время компиляции компилятор не
знает, от какого из базовых классов parent объект child наследует обработчик
сообщения go_to_sleep(), хотя эта правильность и может сбить с толку. Вы
можете спросить, почему неопределенность имеет значение, ведь эта функция одна и
та же в обоих классах. Компилятор не может создать ветвление времени выполнения,
так как не знает, какое значение присвоить указателю this, когда он вызывает
функцию-член базового класса.


Правила программирования на Си++

150


виртуальный базовый класс. Например, в следующем коде
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
, но

Список сторінок

Поділитися у соцмережах

☏ Замовлення реклами: +3805044-68-872

© Асоціація електронних бібліотек України

☝ Усі матеріали сайту (зокрема статті, зображення, рекламні оголошення тощо) призначені лише для попереднього ознайомлення. Усі права на публікації, представлені на сайті, належать їхнім законним власникам. Просимо Вас не зберігати копії інформації.