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

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

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

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 42 43 44 45 46 47 48 49 50 ... 73
Перейти на страницу:

Идиома NVI не требует, чтобы виртуальные функции обязательно были закрытыми. В некоторых иерархиях классов ожидается, что виртуальная функция, переопределенная в производном классе, будем вызывать одноименную функцию из базового класса (как в примере из правила 27). Чтобы такие вызовы были возможны, виртуальная функция должна быть защищенной, а не закрытой. Иногда она даже может быть открытой (как, например, деструкторы в полиморфных базовых классах – см. правило 7), но к этому случаю идиома NVI уже неприменима.

Реализация паттерна «Стратегия» посредством указателей на функции

Идиома NVI – это интересная альтернатива открытым виртуальным функциям, но с точки зрения проектирования она дает не слишком много. В конце концов, мы по-прежнему используем виртуальные функции для вычисления жизненной силы каждого персонажа. С точки зрения проектирования гораздо более сильным было бы утверждение о том, что вычисление жизненной силы персонажа не зависит от типа персонажа, что такие вычисления вообще не являются свойством персонажа как такового. Например, мы можем потребовать, чтобы конструктору каждого персонажа передавался указатель на функцию, которая вызывалась бы для вычисления его жизненной силы:

class GameCharacter; // опережающее объявление

// функция алгоритма по умолчанию для вычисления жизненной силы персонажа

int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter {

public:

typedef int (*HealthCalcFunc)(const GameCharacter&);

explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

: healthFunc(hcf)

{}

int healthValue() const

{ return healthFunc(*this);}

...

private:

HealthCalcFunc healthFunc;

};

Это простой пример применения другого распространенного паттерна проектирования – «Стратегия» (Strategy). По сравнению с подходами, основанными на виртуальных функциях в иерархии GameCharacter, он предоставляет некоторые любопытные возможности, повышающие гибкость:

• Разные экземпляры персонажей одного и того же типа могут иметь разные функции вычисления жизненной силы. Например:

class EvilBadGay: public GameCharacter {

public:

explicit EvilBadGay(HealthCalcFunc hcf = defaultHealthCalc)

: GameCharacter(hcf)

{...}

...

};

int loseHealthQuickly(const GameCharacter&); // функции вычисления

int loseHealthSlowly(const GameCharacter&); // жизненной силы

// с разным поведением

EvilBadGay ebg1(loseHealthQuickly); // однотипные персонажи

EvilBadGay ebg2(loseHealthSlowly); // с разным поведением

// относительно здоровья

• Функция вычисления жизненной силы для одного и того же персонажа может изменяться во время исполнения. Например, класс GameCharacter мог бы предложить функцию-член setHealthCalculator, которая позволяет заменить текущую функцию вычисления жизненной силы.

С другой стороны, тот факт, что функция вычисления жизненной силы больше не является функцией-членом иерархии GameCharacter, означает, что она не имеет специального доступа к внутреннему состоянию объекта, чью жизненную силу вычисляет. Например, defaultHealthCalc не имеет доступа к закрытым частям EvilBadGay. Это не страшно, если жизненная сила персонажа может быть вычислена с помощью его открытого интерфейса, но для максимально точных расчетов может понадобиться доступ к закрытой информации. На самом деле такая проблема может возникать всегда, когда некоторая функциональность выносится из класса наружу (например, из функций-членов в свободные функции, не являющиеся друзьями класса, или в функции-члены другого класса, не дружественного данному). Она будет встречаться в настоящем правиле и далее, потому что все прочие проектные решения, которые нам еще предстоит рассмотреть, тоже включают использование функций, находящихся вне иерархии GameCharacter.

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

Реализация паттерна «Стратегия» посредством класса tr::function

Если вы привыкли к шаблонам и их применению для построения неявных интерфейсов (см. правило 41), то применение указателей на функции покажется вам не слишком гибким решением. Почему вообще для вычисления жизненной силы нужно обязательно использовать функцию, а не что-то ведущее себя как функция (например, функциональный объект)? Если от функции никуда не деться, то почему не сделать ее членом класса? И почему функция должна возвращать int, а не объект, который можно преобразовать в int?

Эти ограничения исчезают, если вместо указателя на функцию (подобную healthFunc) воспользоваться объектом типа tr::function. Как объясняется в правиле 54, такой объект может содержать любую вызываемую сущность (указатель на функцию, функциональный объект либо указатель на функцию-член), чья сигнатура совместима с ожидаемой. Вот пример такого подхода, на этот раз с использованием tr1::function:

class GameCharacter; // как раньше

int defaultHealthCalc(const GameCharacter& gc); // как раньше

class GameCharacter {

public:

// HealthCalcFunction – это любая вызываемая сущность, которой можно

// передать в качестве параметра нечто, совместимое с GameCharacter,

// и которая возвращает нечто, совместимое с int; подробности см. ниже

typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;

explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

: healthFunc(hcf)

{}

int healthValue() const

{ return healthFunc(*this);}

...

private:

HealthCalcFunc healthFunc;

};

Как видите, HealthCalcFunc – это typedef, описывающий конкретизацию шаблона tr1::function. А значит, он работает как обобщенный указатель на функцию. Посмотрим внимательнее, как определен тип HealthCalcFunc:

std::tr1::function<int (const GameCharacter&)>

Здесь я выделил «целевую сигнатуру» данной конкретизации tr1::function. Словами ее можно описать так: «функция, принимающая ссылку на объект типа const GameCharacter и возвращающая int». Объект типа HealthCalcFunc может содержать любую вызываемую сущность, чья сигнатура совместима с заданной. Быть совместимой в данном случае означает, что параметр можно неявно преобразовать в const GameCharacter&, а тип возвращаемого значения неявно конвертируется в int.

Если сравнить с предыдущим вариантом дизайна (где GameCharacter включал в себя указатель на функцию), то вы не обнаружите почти никаких отличий. Единственная разница в том, что GameCharacter теперь содержит объект типа tr1::function – обобщенный указатель на функцию. Это изменение так незначительно, что я назвал бы его несущественным, если бы не то обстоятельство, что теперь пользователь получает ошеломляющую гибкость в спецификации функций, вычисляющих жизненную силу:

short calcHealth(const gameCharacter&); // функция вычисления

// жизненной силы;

// она возвращает не int

stuct HealthCalculator { // класс функциональных

int operator()(const GameCharacter&) const // объектов, вычисляющих

{...} // жизненную силу

};

class GameLevel {

public:

float health(const GameCharacter&) const; // функция-член для

... // вычисления жизненной

}; // силы; возвращает не int

class EvilBadGay: public GameCharacter { // как раньше

...

};

class EyeCandyCharacter: public GameCharacter { // другой тип персонажей;

... // предполагается такой же

}; // конструктор как

// у EvilBadGay

EvilBadGay ebg1(calcHealh); // персонаж использует

// функцию вычисления

// жизненной силы

EyeCandyCharacter ecc1(HealthCalculator()); // персонаж использует

// функциональный объект

// вычисления жизненной

// силы

GameLevel currentLevel;

...

EvilBadGay ebg2( // персонаж использует

std::tr1::bind(&GameLevel::health, // функцию-член для

currentLevel, // вычисления жизненной

_1) // силы; подробности

); // см. ниже

Лично я поражаюсь тому, какие удивительные вещи позволяет делать шаблон tr1::function. Если вы не разделяете моих чувств, то не исключено, что просто не понимаете, для чего используется tr1::bind в определении ebg2. Позвольте мне объяснить.

Мы хотим сказать, что для вычисления жизненной силы персонажа ebg2 следует использовать функцию-член класса GameLevel. Но из объявления GameLevel::health следует, что она должна принимать один параметр (ссылку на GameCharacter), а на самом деле она принимает два, потому что имеется еще неявный параметр типа GameLevel – тот, на который внутри нее указывает this. Все функции вычисления жизненной силы принимают лишь один параметр: ссылку на персонажа GameCharacter, чья жизненная сила вычисляется. Если мы используем функцию GameLevel::health, то должны каким-то образом «адаптировать» ее, чтобы вместо двух параметров (GameCharacter и GameLevel) она принимала только один (GameCharacter). В этом примере мы хотим для вычисления здоровья ebg2 в качестве параметра типа GameLevel всегда использовать объект currentLevel, поэтому «привязываем» его как первый параметр при вызове GameLevel::health. Именно в этом и заключается смысл вызова tr1::bind: указать, что функция вычисления жизненной силы персонажа ebg2 должна в качестве объекта типа GameLevel использовать currentLevel.

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