trainings/CreatingReliableSoftwareCpp/Presentation/template_method.md

577 lines
24 KiB
Markdown

# 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:
```C++
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();
};
```
<!-- .element: class="fragment fade-in" -->
```C++
class Enemy : public Player {
protected:
std::pair<int, const Player*> doAttack() {
const Player* player = choosePlayer();
const int damage = calculateDamage(player);
return {damage, player};
}
};
```
<!-- .slide: style="font-size: 0.72em" -->
<!-- .element: class="fragment fade-in" -->
___
## 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.
<img data-src="images/template_method_uml.png" alt="Template method UML">
___
## 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:
* <!-- .element: class="fragment fade-in" --> <code>void SetUp()</code>
* <!-- .element: class="fragment fade-in" --> <code>void TearDown()</code>
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:
<!-- .element: class="fragment fade-in" -->
```C++
class Test {
public:
Test() {
// do stuff related with test
SetUp();
}
~Test() {
// cleanup stuff releated with test
TearDown();
}
protected:
virtual void SetUp() {}
virtual void TearDown() {}
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.78em" -->
___
## 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:
* <!-- .element: class="fragment fade-in" --> Each player (enemy or real user) roll a die who will start first
* <!-- .element: class="fragment fade-in" --> After a roll we add initiative and based on the final result we decide who will begin
* <!-- .element: class="fragment fade-in" --> Player can do 3 actions. It could choose any of these:
* <!-- .element: class="fragment fade-in" --> Sail
* <!-- .element: class="fragment fade-in" --> Attack
* <!-- .element: class="fragment fade-in" --> Protect
* <!-- .element: class="fragment fade-in" --> Boarding
* <!-- .element: class="fragment fade-in" --> try to escape
* <!-- .element: class="fragment fade-in" --> For instance, player can sail twice and then make boarding or attack 3 times with canons
* <!-- .element: class="fragment fade-in" --> After one player finish, next player perform his turn
* <!-- .element: class="fragment fade-in" --> Battle ends when all ships from enemy side will be sunk or enemy run away
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.87em" -->
___
## 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.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.85em" -->
___
## 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.
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.87em" -->
___
## Enemy
Enemy class will be directed by an AI, so we implement some logic and based on it, it will decide what to do.
```C++
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
}
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.87em" -->
___
## 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.
```C++
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);
}
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.87em" -->
___
## A Few desing patterns
Can you named all desing patterns I used in this example?
* <!-- .element: class="fragment fade-in" --> The <code>Player</code> class is a <b>Factory method</b> (that was obvious)
* <!-- .element: class="fragment fade-in" --> The <code>BattleField</code> class is a <b>context pattern</b>. We wrap all necessary information about battle here
* <!-- .element: class="fragment fade-in" --> The <code>Action</code> class is a <b>command pattern</b>. 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).
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
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.
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.79em" -->
___
## 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...
```C++
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;
};
```
```C++
class FlameThrowerShip : public Enemy {
protected:
int calculateDistance(Ship* ship) override;
int calculateThreat(Ship* ship) override;
int calculatePossibleDamage(Ship* ship) override;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.79em" -->
___
```C++
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;
}
```
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.82em" -->
___
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)
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.85em" -->
___
Lets summary everything on UML diagram
<img data-src="images/palyer_uml.png" alt="Player UML">
___
## 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.**
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.97em" -->
___
## Exericse 1
* <!-- .element: class="fragment fade-in" --> Go into directory Player and implement a base class <code>Player</code>, this class should have 3 public virtual members:
* <!-- .element: class="fragment fade-in" --> <code>virtual int attack(Ship& playerShip) = 0;</code>
* <!-- .element: class="fragment fade-in" --> <code>virtual Ship& getShip() = 0;</code>
* <!-- .element: class="fragment fade-in" --> <code>virtual const Ship& getShip() const = 0;</code>
* <!-- .element: class="fragment fade-in" --> Add to <code>Player</code> class a non-virtual function: <code>Action::Status makeAction(const BattleField& battleField);</code>.
* <!-- .element: class="fragment fade-in" --> Add to <code>Player</code> class 2 virtual protected function (a template method):
* <!-- .element: class="fragment fade-in" --> <code>virtual Ship* chooseEnemyShip(const BattleField& battleField) const = 0;</code>
* <!-- .element: class="fragment fade-in" --> <code>virtual std::unique_ptr<Action> chooseAction(const BattleField& battleField) const = 0;</code>
* <!-- .element: class="fragment fade-in" --> Implement some logic in method <code>makeAction</code> based on 2 template method:
* <!-- .element: class="fragment fade-in" --> To simplify, Player can only attack or defense
* <!-- .element: class="fragment fade-in" --> Use class Command from directory <code>Battle</code>
___
## Exercise 2
* <!-- .element: class="fragment fade-in" --> Implement class <code>RealUser</code> which will inherit from class <code>Player</code>. Implement both template methods:
* <!-- .element: class="fragment fade-in" --> 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.
* <!-- .element: class="fragment fade-in" --> To increase defense, you can use method <code>setArmor</code> from <code>Ship</code> class.
* <!-- .element: class="fragment fade-in" --> Rewrite class <code>Enemy</code> to inherit from <code>Player</code> add also implementation for both template methods
* <!-- .element: class="fragment fade-in" --> 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.
<img data-src="images/command_uml.png" alt="Template method UML">
___
## 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:
* <!-- .element: class="fragment fade-in" --> Sail
* <!-- .element: class="fragment fade-in" --> Attack
* <!-- .element: class="fragment fade-in" --> Protect
* <!-- .element: class="fragment fade-in" --> Boarding
* <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
___
## 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.
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.84em" -->
___
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.
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.76em" -->
___
## 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.
```C++
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;
}
}
}
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.76em" -->
___
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.
```C++
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};
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.72em" -->
___
## 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.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.82em" -->
___
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.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
```C++
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;
}
}
}
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.60em" -->
___
## Exericse 3
* <!-- .element: class="fragment fade-in" --> Go into directory <code>Battle</code> and implement class <code>Escape</code> which will inherit from class <code>Action</code>.
* <!-- .element: class="fragment fade-in" --> Roll a die (1 to 20), if value is bigger than <code>12</code> a player successfully escaped.
* <!-- .element: class="fragment fade-in" --> Noticed that we shouldn't modify existing code, but we need to do this in two place, where?
* <!-- .element: class="fragment fade-in" --> How would you refactor the code?