Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Как
Здесь я выделил «целевую сигнатуру» данной конкретизации tr1::function. Словами ее можно описать так: «функция, принимающая ссылку на объект типа const GameCharacter и возвращающая int». Объект типа HealthCalcFunc может содержать любую вызываемую сущность, чья сигнатура совместима с заданной. Быть совместимой в данном случае означает, что параметр можно неявно преобразовать в const GameCharacter&, а тип возвращаемого значения неявно конвертируется в int.
Если сравнить с предыдущим вариантом дизайна (где GameCharacter включал в себя указатель на функцию), то вы не обнаружите почти никаких отличий. Единственная разница в том, что GameCharacter теперь содержит объект типа tr1::function – обобщенный указатель на функцию. Это изменение так незначительно, что я назвал бы его несущественным, если бы не то обстоятельство, что теперь пользователь получает ошеломляющую гибкость в спецификации функций, вычисляющих жизненную силу:
Лично я поражаюсь тому, какие удивительные вещи позволяет делать шаблон 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» означает «использовать currentLevel в качестве объекта GameLevel при вызове GameLevel::health для ebg2». Эти детали не столь сложны, к тому же они не имеют прямого отношения к основной идее, которую я хочу продемонстрировать, а именно: используя tr1::function вместо указателя на функцию, мы позволяем пользователям применять любую совместимую вызываемую сущность для вычислении жизненной силы персонажа. Впечатляет, не правда ли?
«Классический» паттерн «Стратегия»
Если вас больше интересуют паттерны проектирования, чем собственно язык C++, то более традиционный подход к реализации паттерна «Стратегия» состоит в том, чтобы сделать функцию вычисления жизненной силы виртуальной функцией-членом в классах, принадлежащих отдельной иерархии. Эта иерархия может выглядеть примерно так:
Если вы не знакомы с нотацией UML, поясню: здесь говорится, что GameCharacter – корень иерархии, в которой EvilBadGay и EyeCandyCharacter являются производными классами; HealthCalcFunc – корень иерархии, в которой производными классами являются SlowHealthLooser и FastHealthLooser; и каждый объект типа GameCharacter содержит указатель на объект из иерархии HealthCalcFunc. А вот как структурируется соответствующий код: