24 KiB
Template method
This design pattern similar to strategy was used by you, even if you didn't notice that. This is a very popular approach whenever you have some functionality to be handled, but a base class shouldn't contain such logic. Let's check the following example:
class Player {
public:
virtual ~Player() = default;
void attack() {
std::cout << "Player " << _name << " turn\n";
const auto [damage, attackedPlayer] = doAttack();
std::cout << "Deal: " << damage << " to player: " << attackedPlayer->name() << "\n";
}
protected:
std::pair<int, const Player*> doAttack();
};
class Enemy : public Player {
protected:
std::pair<int, const Player*> doAttack() {
const Player* player = choosePlayer();
const int damage = calculateDamage(player);
return {damage, player};
}
};
UML
Template method is a pattern, that allow you to create a sketch of algorithm, but details will be implemented by derived classes. Each class will decide how to acct in a various scenario, for instance enemy will use AI to choice an opponent to fight, but real user need to take an input from keyboard or mouse.
GTEST
A perfect example of template method is a GTEST. Whenever you inherit from testing::Test class you actually get possibility to overload two virtual functions:
-
void SetUp() -
void TearDown()
A base class has two virtual function that are empty. So if you don't provide any implementation in the derived class, it will do nothing. This class may look like:
class Test {
public:
Test() {
// do stuff related with test
SetUp();
}
~Test() {
// cleanup stuff releated with test
TearDown();
}
protected:
virtual void SetUp() {}
virtual void TearDown() {}
};
Pirates once more
To show you how useful this pattern is, let's implement a new functionality in our project. We need to implement a real battle! Because we played recently a baldurs gate 3 we decided to implement is as a turn based. We create the following rules to battle:
- Each player (enemy or real user) roll a die who will start first
- After a roll we add initiative and based on the final result we decide who will begin
-
Player can do 3 actions. It could choose any of these:
- Sail
- Attack
- Protect
- Boarding
- try to escape
- For instance, player can sail twice and then make boarding or attack 3 times with canons
- After one player finish, next player perform his turn
- Battle ends when all ships from enemy side will be sunk or enemy run away
Battle
We create a simple class for battle. We can have few enemies and only one real player. The battle will ends when user or enemy espacded or one will be defeated.
class Battle {
public:
void battle() {
const std::vector<Player*>& sorted = calculateOrderOfPlayers(_battleField->players());
while (true) {
for (const auto* player : sorted) {
for (size_t i = 0 ; i < 3 ; ++i) {
const auto status = player->makeAction(*_battleField);
if (status == Action::Status::Escaped ||
status == Action::Status::EnemyDefeated ||
status == Action::Status::PlayerDefeated) {
return;
}
}
}
}
}
private:
std::unique_ptr<BattleField> _battleField;
}
Player
A player should have one function makeAction that should return Status. In this method, we will create a draft of algorithm. User need to choose which ship will make an action (Player may have few ships), then choose action and if action is equal board or attack he needs to choose enemy ship.
class Player {
public:
virtual ~Player() = default;
Action::Status makeAction(const BattleField& battleField) {
Ship* ship = chooseShip(battleField);
std::unique_ptr<Action> action = chooseAction(battleField);
if (action->type() == Action::Type::Board || action->type() == Action::Type::Attack) {
Ship* enemyShip = choosEnemyShip(battleField);
return (*action)(ship, enemyShip);
}
return (*action)(ship, nullptr);
}
protected:
virtual Ship* chooseShip(const BattleField& battleField) const = 0;
virtual Ship* chooseEnemyShip(const BattleField& battleField) const = 0;
virtual std::unique_ptr<Action> chooseAction(const BattleField& battleField) const = 0;
};
Enemy
Enemy class will be directed by an AI, so we implement some logic and based on it, it will decide what to do.
class Enemy : public Player {
public:
protected:
Ship* chooseShip(const BattleField& battleField) const override {
// Check the battlefield and choos the best ship
}
Ship* chooseEnemyShip(const BattleField& battleField) const override {
// Check the battlefield and choose the weakened player ship which is closest
}
std::unique_ptr<Action> chooseAction(const BattleField& battleField) const override {
// If don't have chance to whin escape
// If don't have ship in range sail
// If want to bard but it is too far sail
}
};
User
For user class we need to take an input from real person. In the future, we should also add possibility to revert user action and allow choosing once again.
class User : public Player {
public:
protected:
Ship* chooseShip(const BattleField& battleField) const override {
return GetUserInput(battleField, Type::ChoosePlayerShip);
}
Ship* chooseEnemyShip(const BattleField& battleField) const override {
return GetUserInput(battleField, Type::ChooseEnemyShip);
}
std::unique_ptr<Action> chooseAction(const BattleField& battleField) const override {
return GetUserInput(battleField, Type::ChooseAction);
}
};
A Few desing patterns
Can you named all desing patterns I used in this example?
-
The
Playerclass is a Factory method (that was obvious) -
The
BattleFieldclass is a context pattern. We wrap all necessary information about battle here -
The
Actionclass is a command pattern. We wrap the logic in command, and also allow reverting changes.
Enemy AI
We need to develop the Enemy AI. First, we focus on implementation of the algorithm that will choose the enemy ship. In such case we want to measure few parameters and based on the score choose the best (this is something like min-max algorithm).
Ship* chooseEnemyShip(const BattleField& battleField) const override final {
// Sort form highest value to the lowest
std::map<int, Ship*, std::greater<int>> scores;
for (const Ship* ship : battleField->getPlayerShip()) {
int score = 0;
score += calculateDistance(ship);
score += calculateThreat(ship);
score += calculatePossibleDamage(ship);
scores[score] = ship;
}
return scores.begin()->second;
}
We need to implement 3 methods: calculateDistance, calculateThreat and calculatePossibleDamage. We may choose how enemy will measure such things, because we can have a few types of enemy.
Elite and Boss
If we create a new hierarchy of derived class, we need to add implementation on how these classes should measure scores. It may depend on various things, like type of ammunition, protection, resistance, speed etc...
class Enemy : public Player {
protected:
// Don't allow to override this methods
Ship* chooseShip(const BattleField& battleField) const override final;
Ship* chooseEnemyShip(const BattleField& battleField) const override final;
std::unique_ptr<Action> chooseAction(const BattleField& battleField) const override final;
// Define a new template methods
virtual int calculateDistance(Ship* ship) const = 0;
virtual int calculateThreat(Ship* ship) const = 0;
virtual int calculatePossibleDamage(Ship* ship) const = 0;
};
class FlameThrowerShip : public Enemy {
protected:
int calculateDistance(Ship* ship) override;
int calculateThreat(Ship* ship) override;
int calculatePossibleDamage(Ship* ship) override;
};
int FlameThrowerShip::calculateDistance(Ship* ship) override {
// Because flame thrower has short range, we need to be very close to give a high score
const int distance = getDistance(_ship, ship);
if (distance < FLAME_THROWER_RANGE) {
return 10;
// We can reach in one turn
} else if (distance - _ship.speed() < FLAME_THROWER_RANGE) {
return 5;
}
return 1;
}
int FlameThrowerShip::calculateThreat(Ship* ship) override {
// The biggest threat for flameThroweShip is for instance a ship that deal explosive damage
// because may set on fire the oil
if (ship->hasExplosiveAmmo()) {
return 10;
} else if (ship->hasFlameThrower()) {
return 5;
}
return 2;
}
As we can see we can have few layers of template methods, and finally we achieve a final class (we should think about mark it as final also) which has implemented all logic. By using template method pattern we follow SOLID rules, because we have a single responsibility, our code is open to extension and don't demand modification of existing code (of course only if we add new types of enemies not modifying algorithm). And also we have dependency inversion, because low level modules depends on high level module's abstraction (base class defied interface → template methods that should be implemented)
int FlameThrowerShip::calculatePossibleDamage(Ship* ship) override {
// Check the type of shipe. for instance it may has some protection for fire (some black magic)
if (ship->hasProtectionToFire()) {
return 1;
} else if (ship->armor() > 50) {
return 5;
}
return 10;
}
Lets summary everything on UML diagram
Pros and cons
Template method is a powerful design pattern, because we can hide from the end user that we use polymorphism. In case of usage of class Player we have one public method makeAction which is not virtual. The whole magic is hidden in protected methods. What's more template method is very flexible, for class Player we decided to mark class as final because we implement everything. But class enemy which is directed by AI create another set of proceted virtual function and demands form derived class to implement them. We can easily extend this code by adding new types of enemies. Unfortunately, we can't do this with new virtual functions. Whenever we add a new template method we need to implement it in every derived class (or leave it empty as in GTEST, but this is rarely scenario).
To summarize: Use this pattern for adding new implementations rather than new functions.
Exericse 1
-
Go into directory Player and implement a base class
Player, this class should have 3 public virtual members:-
virtual int attack(Ship& playerShip) = 0; -
virtual Ship& getShip() = 0; -
virtual const Ship& getShip() const = 0;
-
-
Add to
Playerclass a non-virtual function:Action::Status makeAction(const BattleField& battleField);. -
Add to
Playerclass 2 virtual protected function (a template method):-
virtual Ship* chooseEnemyShip(const BattleField& battleField) const = 0; -
virtual std::unique_ptr chooseAction(const BattleField& battleField) const = 0;
-
-
Implement some logic in method
makeActionbased on 2 template method:- To simplify, Player can only attack or defense
-
Use class Command from directory
Battle
Exercise 2
-
Implement class
RealUserwhich will inherit from classPlayer. Implement both template methods:- Don't make it too complex, just ask user about action and type of ammunition, then you can randomly choose a damage or if missed.
-
To increase defense, you can use method
setArmorfromShipclass.
-
Rewrite class
Enemyto inherit fromPlayeradd also implementation for both template methods - Uncomment code in main, check if you can perform a real battle!
A little abour command pattern
Similar to Strategy or Template method we also have a Command pattern. This pattern is quite small, so I decided to talk about it in this section. The general idea is to provide functionality that we can do and optionally an option to undo this action.
Battle Actions
A perfect example of command pattern is a battle between two ships. We mentioned earlier that we want to give a few different actions to choose during the combat:
- Sail
- Attack
- Protect
- Boarding
- try to escape
Each player can do three actions per turn. Because a player may decide that he don't like his choose, we may add the possibility to undo an action in a single turn.
Action
Let's create an interface for a Action. We should have two method run for running a command and undo for reverting. We may also have an additional info like Status in return statement. Keep in mind if this status may change it may be not a good idea to have it inside Action class because whenever we will add new field it needs to recompile all classes that include a Action class. A second disadvantage is that sometimes, we may not have access to this interface, so we can't add a new Status. I decided to put Status inside class Action because I know it will not be changed. If Status may change, I will put it in separate file which will be visible for all programmers or move the logic about if player escaped or was defeated outside Action class.
class Action {
public:
// This should not change so we can leave it here
enum class Status {
Escaped,
Defeated,
Nothing,
};
virtual Status run(Player* player, std::unique_ptr<Ship>& enemyShip) = 0;
virtual void undo(Player& player, std::unique_ptr<Ship>& enemyShip) = 0;
};
To implement class Attack we need to store one variable: the amount of damage. This is needed to revert a takeDamage operation. If we also simulate that part of cargo will be destroyed, we also need to keep info how many cargoes we subtract or create a snapshot of cargo and then apply this snapshot during revert. It depends on how big such snapshot will be and how hard it will be to undo the operation of destroying part of cargo.
class Attack : public Action {
public:
Status run(Player* player, std::unique_ptr<Ship>& enemyShip) override {
_damage = calculateDamage(player);
std::cout << "Player Ship: " << player->getShip().name() << " attack: " << enemyShip->name() << '\n';
if (_damage) {
enemyShip->takeDamage(_damage);
std::cout << "Dealed damage: " << _damage << '\n';
return enemyShip->durability() <= 0 ? Status::Defeated : Status::Nothing;
}
std::cout << "Missed\n";
return Status::Nothing;
}
void undo(Player* player, std::unique_ptr<Ship>& enemyShip) override {
enemyShip->repair(_damage);
}
private:
int _damage = 0;
};
How to revert
If we want to allow a user to revert operations, we need to store them. Because we decided to revert action only in a single turn, we need to keep max 3 actions. If the user ends the turn, we will clear it, so it will not be possible to undo action from the previous turn.
while (true) {
for (auto* player : players) {
std::vector<std::unique_ptr<Action>> actions;
while (!endOfTurn()) {
const auto userInput = getuserInput();
if (userInput.revertAction() && !actions.empty()) {
actions.back()->undo();
continue;
} else if (userInput.revertAction() && actions.empty()) {
std::cout << "Can't undo more actions\n";
continue;
}
actions.push_back(player->makeAction(battefield, userInput));
if (actions.back().status() == Action::Status::Escaped ||
actions.back().status() == Action::Status::Defeated) {
std::cout << "The battle ends!\n";
return;
}
}
}
}
As you can see, we can easily implement a battle and give a user possibility to revert actions. This pattern also gives us an easy mechanism to add a new action in the future if we will want to extend a combat possibility.
class Boarding : public Action {
public:
Status run(Player* player, std::unique_ptr<Ship>& enemyShip) override {
// Based on number and type of crew calculate a boarding result
const bool success = doBoarding(player->getShip(), enemyShip);
std::cout << "Player Ship: " << player->getShip().name() << " boarding: " << enemyShip->name() << '\n';
if (success) {
// Hold a pointer to newaly added ship in case of revert
_shipPtr = player->addShip(std::move(enemyShip));
std::cout << "Ship: " << _shipPtr->name() << "conquered\n";
return Status::Defeated;
}
// We lost some crew, but we escaped, so player still not loose
std::cout << "Boarding failed!\n";
return Status::Nothing;
}
void undo(Player* player, std::unique_ptr<Ship>& enemyShip) override {
if (_shipPtr) {
enemyShip = player->moveShip(_shipPtr);
}
}
private:
Ship* _shipPtr{nullptr};
};
Modern approach
We should prefer by-value semantic. For template method pattern we use virtual protected methods, so it will be a tricky to rewrite it by using by-value semantic, but for command pattern this will be easy. We can check the std::transform method, specifically UnaryOperator. This is nothing but command pattern in modern approach.
template<class InputIt, class OutputIt, class UnaryOp>
OutputIt transform(InputIt first1, InputIt last1,
OutputIt d_first, UnaryOp unary_op)
{
while (first1 != last1)
*d_first++ = unary_op(*first1++);
return d_first;
}
Whenever we want to do some action, but we don't need to undo it, we can choose faster and better approach with std::function and in c++23 std::move_only_function. This approach don't require virtual function and creating a base class for command, and we don't need to keep pointer or reference to base class. We will keep command by value, so it will be safer and probably even faster.
Status run(Player* player, std::unique_ptr<Ship>& enemyShip) {
const auto damage = calculateDamage(player);
std::cout << "Player Ship: " << player->getShip().name() << " attack: " << enemyShip->name() << '\n';
if (damage) {
enemyShip->takeDamage(damage);
std::cout << "Dealed damage: " << damage << '\n';
return enemyShip->durability() <= 0 ? Status::Defeated : Status::Nothing;
}
std::cout << "Missed\n";
return Status::Nothing;
}
while (true) {
for (auto* player : players) {
std::vector<std::function<Status(Player* player, std::unique_ptr<Ship>& enemyShip)>> actions;
while (!endOfTurn()) {
const auto userInput = getuserInput();
if (userInput.revertAction() && !actions.empty()) {
actions.back().undo();
continue;
} else if (userInput.revertAction() && actions.empty()) {
continue;
}
actions.push_back(player->makeAction(battefield, userInput));
if (actions.back().status() == Action::Status::Escaped || actions.back().status() == Action::Status::Defeated) {
return;
}
}
}
}
Exericse 3
-
Go into directory
Battleand implement classEscapewhich will inherit from classAction.-
Roll a die (1 to 20), if value is bigger than
12a player successfully escaped.
-
Roll a die (1 to 20), if value is bigger than
- Noticed that we shouldn't modify existing code, but we need to do this in two place, where?
- How would you refactor the code?