trainings/CreatingReliableSoftwareCpp/Presentation/strategy.md

25 KiB

Strategy

The second most popular design pattern is a strategy. If I need to bet how many of you used this pattern at least once I will say all of you. Strategy is the most common pattern used by the STL library and makes it so flexible. That's why we should use it often (maybe not everywhere) but in most cases, strategy will be an answer.

Why we should use strategy? As David Thomas and Andrew Hunt said in the book: Inheritance is Rarely the Answer.


Example of bad inheritance

Before we talk about strategy patterns, let's look at the bad design of the application. We want to create three types of ammunition: Round, Chain and Grape. Each type of ammunition needs to be displayed as an icon (for instance when we check, the inventory, or buy it from the store) and also needs to be animated when a ship fires this ammunition during combat.

We decided to solve this problem by create inheritance. We inherit from class Ammunition and create three classes of Shots, then depending on which graphic library we used we draw it using metal or OpenGL.

Amunnition UML ___

Next iteration

After we successfully implemented the draw method, we decided to add SDL and WindowsAnimationManager libraries for displaying animation during combat. Because we already use OpenGL and metal libraries, we still need to support them. So we need to create another layer of inheritance.

Amunnition UML

I hope all of you saw that this is not a good example of a design application. Each time we add support for a new library, we need to create another layer of inheritance. How we can make it more flexible? The answer is the Strategy design pattern.

Amunnition UML

Simplify

First of all, we don't want to break 2 of the SOLID letters: Liskov substitution and Single responsibility. That's why we should inherit from class which all methods make sense for us. We create two strategies, one for fire and one for draw. We also want to avoid an oversized hierarchy of class.

Amunnition UML

Implementation

The ammunition class will be a base class with the interface. It also contains info about the amount of ammunition.

class Ammunition {
public:
    Ammunition(size_t amount)
        : _amount(amount) {}
    virtual ~Ammunition() = default;

    virtual void fire() = 0;
    virtual void draw() = 0;
    virtual int getDamage() const = 0;

    size_t amount() const { return _amount; }

private:
    size_t _amount = 0;
};

class FireStrategy {
public:
    virtual void fire(const Ammunition& ammunition) = 0;
};

class SDLFireStrategy : public FireStrategy {
public:
    void fire(const Ammunition& ammunition) override {
        std::cout << "SDL Deal: " << ammunition.getDamage() << " damage!" << '\n';
    }
};

class WAMFireStrategy : public FireStrategy {
public:
    void fire(const Ammunition& ammunition) override {
        std::cout << "WAM Deal: " << ammunition.getDamage() << " damage!" << '\n';
    }
};
class DrawStrategy {
public:
    virtual void draw(const Ammunition& ammunition) = 0;
};

class OpenGLDrawStrategy : public DrawStrategy {
public:
    void draw(const Ammunition& ammunition) override {
        std::cout << "OpenGL Ammunition: " << ammunition.amount() << '\n';
    }
};

class MetalDrawStrategy : public DrawStrategy {
public:
    void draw(const Ammunition& ammunition) override {
        std::cout << "Metal Ammunition: " << ammunition.amount() << '\n';
    }
};

class RoundShot : public Ammunition {
public:
    RoundShot(size_t amount, std::unique_ptr<FireStrategy>&& fireStrategy, std::unique_ptr<DrawStrategy>&& drawStrategy)
        : Ammunition(amount), _fireStrategy(std::move(fireStrategy)), _drawStrategy(std::move(drawStrategy)) {}
    void fire() override { _fireStrategy->fire(*this /* other args if needed*/); }
    void draw() override { _drawStrategy->draw(*this /* other args if needed*/); }
    int getDamage() const override { return 10; }
private:
    std::unique_ptr<FireStrategy> _fireStrategy;
    std::unique_ptr<DrawStrategy> _drawStrategy;
};
class ChainShot : public Ammunition {
public:
    ChainShot(size_t amount, std::unique_ptr<FireStrategy>&& fireStrategy, std::unique_ptr<DrawStrategy>&& drawStrategy)
        : Ammunition(amount), _fireStrategy(std::move(fireStrategy)), _drawStrategy(std::move(drawStrategy)) {}
    void fire() override { _fireStrategy->fire(*this /* other args if needed*/); }
    void draw() override { _drawStrategy->draw(*this /* other args if needed*/); }
    int getDamage() const override { return 15; }
private:
    std::unique_ptr<FireStrategy> _fireStrategy;
    std::unique_ptr<DrawStrategy> _drawStrategy;
};
class GrapeShot : public Ammunition {
public:
    GrapeShot(size_t amount, std::unique_ptr<FireStrategy>&& fireStrategy, std::unique_ptr<DrawStrategy>&& drawStrategy)
        : Ammunition(amount), _fireStrategy(std::move(fireStrategy)), _drawStrategy(std::move(drawStrategy)) {}
    void fire() override { _fireStrategy->fire(*this /* other args if needed*/); }
    void draw() override { _drawStrategy->draw(*this /* other args if needed*/); }
    int getDamage() const override { return 20; }
private:
    std::unique_ptr<FireStrategy> _fireStrategy;
    std::unique_ptr<DrawStrategy> _drawStrategy;
};

Usage

We can easily inject any strategy into an ammunition class. This will help us keep another solid rule Dependency inversion and also dependency injection.

int main() {
    std::unique_ptr<Ammunition> grapeShot = std::make_unique<GrapeShot>(
        100, std::make_unique<SDLFireStrategy>(), std::make_unique<OpenGLDrawStrategy>());

    grapeShot->fire();
    grapeShot->draw();
}
SDL Deal: 20 damage!  
OpenGL Ammunition: 100

Drawback

The main drawback of the strategy pattern is visible when we try to add a new type. In such case, we need to implement a new behavior for each of these classes:

  • DrawOpenGLStrategy
  • DrawMetalStrategy
  • WamFireStrategy
  • SDLFireStrategy

This will be not such big deal, when we have access to all of the source code, but in case, when we can't modify this it will be problematic


Improvement

For now, DrawStrategy and FireStrategy, have one virtual function which probably needs to distinguish which type of ammunition should be drawn or animate. That's why we can create a more flexible interface, which takes a specific type and we don't need to use any casting.

class FireStrategy {
public:
    virtual void fire(const ChainShot& shot) = 0;
    virtual void fire(const RoundShot& shot) = 0;
    virtual void fire(const GrapeShot& shot) = 0;
};

class SDLFireStrategy : public FireStrategy {
public:
    void fire(const ChainShot& shot) override {
        std::cout << "SDL Chain Shot Deal: " << shot.getDamage() << " damage!" << '\n';
    }
    void fire(const RoundShot& shot) override {
        std::cout << "SDL Round Shot Deal: " << shot.getDamage() << " damage!" << '\n';
    }
    void fire(const GrapeShot& shot) override {
        std::cout << "SDL Grape Shot Deal: " << shot.getDamage() << " damage!" << '\n';
    }
};
class WAMFireStrategy : public FireStrategy {
public:
    void fire(const ChainShot& shot) override {
        std::cout << "WAM Chain Shot Deal: " << shot.getDamage() << " damage!" << '\n';
    }
    void fire(const RoundShot& shot) override {
        std::cout << "WAM Round Shot Deal: " << shot.getDamage() << " damage!" << '\n';
    }
    void fire(const GrapeShot& shot) override {
        std::cout << "WAM Grape Shot Deal: " << shot.getDamage() << " damage!" << '\n';
    }
};

class DrawStrategy {
public:
    virtual void draw(const ChainShot& shot) = 0;
    virtual void draw(const RoundShot& shot) = 0;
    virtual void draw(const GrapeShot& shot) = 0;
};
class OpenGLDrawStrategy : public DrawStrategy {
public:
    void draw(const ChainShot& shot) override {
        std::cout << "OpenGL Chain Shot: " << shot.amount() << '\n';
    }
    void draw(const RoundShot& shot) override {
        std::cout << "OpenGL Round Shot: " << shot.amount() << '\n';
    }
    void draw(const GrapeShot& shot) override {
        std::cout << "OpenGL Grape Shot: " << shot.amount() << '\n';
    }
};
class MetalDrawStrategy : public DrawStrategy {
public:
    void draw(const ChainShot& shot) override {
        std::cout << "Metal Chain Shot: " << shot.amount() << '\n';
    }
    void draw(const RoundShot& shot) override {
        std::cout << "Metal Round Shot: " << shot.amount() << '\n';
    }
    void draw(const GrapeShot& shot) override {
        std::cout << "Metal Grape Shot: " << shot.amount() << '\n';
    }
};

Usage

Now the function exactly knows, which type is handling, and can animate it or draw it correctly, without any type checking.

int main() {
    std::unique_ptr<Ammunition> grapeShot = std::make_unique<GrapeShot>(
        100, std::make_unique<SDLFireStrategy>(), std::make_unique<OpenGLDrawStrategy>());

    grapeShot->fire();
    grapeShot->draw();
}
SDL Grape Shot Deal: 20 damage!
OpenGL Grape Shot: 100

Separate implementation

Strategy can distinguish type, but still depends on all types, let's rewrite the code once more and use another letter form SOLID: Interface segregation. Each implementation will be a separate strategy class, so when we add a new type, other strategies will not know about it.

class FireChainShotStrategy {
public:
    virtual void fire(const ChainShot& shot) = 0;
};

class FireRoundShotStrategy {
    virtual void fire(const RoundShot& shot) = 0;
};

class FireGrapeShotStrategy {
    virtual void fire(const GrapeShot& shot) = 0;
};

class SDLFireChainShotStrategy : public FireChainShotStrategy {
public:
    void fire(const ChainShot& shot) override;
};

class SDLFireRoundShotStrategy : public FireRoundShotStrategy {
public:
    void fire(const RoundShot& shot) override;
};

class SDLFireGrapeShotStrategy : public FireGrapeShotStrategy {
public:
    void fire(const GrapeShot& shot) override;
};

class DrawChainShotStrategy {
public:
    virtual void draw(const ChainShot& shot) = 0;
};

class DrawRoundShotStrategy {
    virtual void draw(const RoundShot& shot) = 0;
};

class DrawGrapeShotStrategy {
    virtual void draw(const GrapeShot& shot) = 0;
};
class OpnGLDrawChainShotStrategy : public DrawChainShotStrategy {
public:
    void draw(const ChainShot& shot) override;
};

class OpenGLDrawRoundShotStrategy : public DrawRoundShotStrategy {
public:
    void draw(const RoundShot& shot) override;
};

class OpenGLDrawGrapeShotStrategy : public DrawGrapeShotStrategy {
public:
    void draw(const GrapeShot& shot) override;
};

Single resposibility

In the beginning, we may think that we will need to create too many classes because each Ammunition type needs 4 classes (2 for draw, and 2 for fire), but when we take a look at the first letter of SOLID - Single responsibility, we understand that now we follow this rule. Previously OpenGLDrawStrategy and MEtalDrawStrategy needed to know about 3 different types of ammunition, and we broke all rules:

  • Single resposinility -> each class need to know about all types
  • Open close -> code was not open for extension, when we add a new type, we need to implement behavior in all 4 classes, even if we don't want to, which may cause to break another letter:
  • Liskov substitution -> we may inherit from an interface which is not fully suitable for us, that's why we should:
  • Interface segregation -> separate too big interface into a smaller one.
  • Dependency inversion -> High level class DrawStrategy nad FireStrategy depends on low level classes RoundShot, ChainShot and GrapeShot.

As you can see, the following implementation of strategy, with many small classes is the correct one.


Broken Dependency inversion

Dependency inversion -> High level class DrawStrategy nad FireStrategy depends on low level classes RoundShot, ChainShot and GrapeShot.

Broken architecture ___

Modern approach

Following the rule of SOLID, we almost reached a perfect architecture, but the one thing left. Class DrawChainStrategy, DrawRoundStrategy, and DrawGrapeStrategy is connected with a specific type like: RoundShot or ChainShot. We follow all SOLID rules, but still there is one thing to improve. Do you know how we can improve it?

Good architecture ___

Template strategy

By creating a template for DrawStrategy we remove all dependencies between specific types of ammunition and strategy:

template <typename T>
class DrawStrategy {
    virtual ~DrawStrategy() = default;
    virtual void draw(const T&) const = 0;
};
Perfect architecture ___

Full implementation

template <typename T>
class FireStrategy {
public:
    virtual void fire(const T& shot) = 0;
};

class SDLFireRoundShotStrategy : public FireStrategy<RoundShot> {
public:
    void fire(const RoundShot& shot) override;
};

class SDLFireChainShotStrategy : public FireStrategy<ChainShot> {
public:
    void fire(const ChainShot& shot) override;
};

class SDLFireGrapeShotStrategy : public FireStrategy<GrapeShot> {
public:
    void fire(const GrapeShot& shot) override;
};
class WAMFireRoundShotStrategy : public FireStrategy<RoundShot> {
public:
    void fire(const RoundShot& shot) override;
};

class WAMFirChainShotStrategy : public FireStrategy<ChainShot> {
public:
    void fire(const ChainShot& shot) override;
};

class WAMFireGrapeShotStrategy : public FireStrategy<GrapeShot> {
public:
    void fire(const GrapeShot& shot) override;
};

template <typename T>
class DrawStrategy {
public:
    virtual void draw(const T& shot) = 0;
};

class OpenGLDrawRoundShotStrategy : public DrawStrategy<RoundShot> {
public:
    void draw(const RoundShot& shot) override;
};

class OpenGLDrawChainShotStrategy : public DrawStrategy<ChainShot> {
public:
    void draw(const ChainShot& shot) override;
};

class OpenGLDrawGrapeShotStrategy : public DrawStrategy<GrapeShot> {
public:
    void draw(const GrapeShot& shot) override;
};
class MetalDrawRoundShotStrategy : public DrawStrategy<RoundShot> {
public:
    void draw(const RoundShot& shot) override;
};

class MetalDrawChainShotStrategy : public DrawStrategy<ChainShot> {
public:
    void draw(const ChainShot& shot) override;
};

class MetalDrawGrapeShotStrategy : public DrawStrategy<GrapeShot> {
public:
    void draw(const GrapeShot& shot) override;
};

class RoundShot : public Ammunition {
public:
    RoundShot(size_t amount, std::unique_ptr<FireStrategy<RoundShot>>&& fireStrategy, std::unique_ptr<DrawStrategy<RoundShot>>&& drawStrategy)
        : Ammunition(amount), _fireStrategy(std::move(fireStrategy)), _drawStrategy(std::move(drawStrategy)) {}
    void fire() override {
        _fireStrategy->fire(*this /* other args if needed*/);
    }
    void draw() override {
        _drawStrategy->draw(*this /* other args if needed*/);
    }
    int getDamage() const override { return 10; }

private:
    std::unique_ptr<FireStrategy<RoundShot>> _fireStrategy;
    std::unique_ptr<DrawStrategy<RoundShot>> _drawStrategy;
};
class ChainShot : public Ammunition {
public:
    ChainShot(size_t amount, std::unique_ptr<FireStrategy<ChainShot>>&& fireStrategy, std::unique_ptr<DrawStrategy<ChainShot>>&& drawStrategy)
        : Ammunition(amount), _fireStrategy(std::move(fireStrategy)), _drawStrategy(std::move(drawStrategy)) {}
    void fire() override {
        _fireStrategy->fire(*this /* other args if needed*/);
    }
    void draw() override {
        _drawStrategy->draw(*this /* other args if needed*/);
    }
    int getDamage() const override { return 15; }

private:
    std::unique_ptr<FireStrategy<ChainShot>> _fireStrategy;
    std::unique_ptr<DrawStrategy<ChainShot>> _drawStrategy;
};

class GrapeShot : public Ammunition {
public:
    GrapeShot(size_t amount, std::unique_ptr<FireStrategy<GrapeShot>>&& fireStrategy, std::unique_ptr<DrawStrategy<GrapeShot>>&& drawStrategy)
        : Ammunition(amount), _fireStrategy(std::move(fireStrategy)), _drawStrategy(std::move(drawStrategy)) {}
    void fire() override {
        _fireStrategy->fire(*this /* other args if needed*/);
    }
    void draw() override {
        _drawStrategy->draw(*this /* other args if needed*/);
    }
    int getDamage() const override { return 20; }

private:
    std::unique_ptr<FireStrategy<GrapeShot>> _fireStrategy;
    std::unique_ptr<DrawStrategy<GrapeShot>> _drawStrategy;
};
int main() {
    std::unique_ptr<Ammunition> grapeShot = std::make_unique<GrapeShot>(
        100, std::make_unique<SDLFireGrapeShotStrategy>(), std::make_unique<OpenGLDrawGrapeShotStrategy>());

    grapeShot->fire();
    grapeShot->draw();
}
SDL Grape Shot Deal: 20 damage!
OpenGL Grape Shot: 100

Exercise 1

  • Go into directory Core and implement ShipStrategy as a template class
  • Strategy should have one pure virtual function: Res handle(T&)
  • Implement in directory Ship class ShipEasyDifficultLvlStrategy
    • Each cargo supplies 2 sailors, so if you have 40 crew, you need 20 banas and 20 rum
  • Implement NextDay by using strategy class
  • Run code, and verify result

Exercise 2

  • Implement in directory *Player* class *PalyerEasyDifficultLvlStrategy*

    * Strategy should roll a die (1-20):     * If result is lower than 5 -> 0 DMG     * If result is higher than 19 -> multiple damage by 2     * Damage should be rolled from (25 to 50)     * deal damage to the ship and return how much damage was deal. Print it in console

  • Add all necessary implementation to the *Enemy* file
  • Test it

Exercise 3

  • Implement new classes *EnemyHardDifficultLvlStrategy* and *ShipHardDifficultLvlStrategy*
    • Strategy for Ship, should subtract cargo equal crew size

    * Strategy for Enemy should roll a die (1-20):     * If result is lower than 3 -> 0 DMG * If result is higher than 15 but not equal 20 -> multiple damage by 3     * If result is higher than 19 -> multiple damage by 3     * Damage should be rolled from (30 to 60) * You shouldn't modify any existing file, except the main.cpp, where you should use a new strategy

  • Run code and verify the result

Strategy is everywhere

Before we go on to the next design pattern. I want to show you that Strategy is almost everywhere. Let's closely look at some STL algorithms:

template< class RandomIt >
void sort( RandomIt first, RandomIt last );
template< class RandomIt, class Compare >
void nth_element( RandomIt first, RandomIt nth, RandomIt last, Compare comp );
template< class InputIt1, class InputIt2, class OutputIt, class Compare >
OutputIt set_difference( InputIt1 first1, InputIt1 last1,
                         InputIt2 first2, InputIt2 last2,
                         OutputIt d_first, Compare comp );

What is common for all of these functions?


STL library was designed by using design patterns, which gives this library a lot of flexibility. Users who want to use one of the STL algorithms, need to provide a strategy, mostly it will be a lambda object, which tells the algorithm how to treat a specific object. What's more, it uses modern by-value semantics, because we don't have any polymorphisms and virtual functions. The first version of STL was designed in 1994-1998, so this modern approach has 30 years :) I think we should learn a lot from this design, because not always need to solve everything by creating virtual classes.


Rewrite Difficult Strategy

template <typename DifficultLvlStrategy>
class Ship : public TimeObserver {
public:
    Ship(Time* time, DifficultLvlStrategy strategy, const std::string& name, int capacity, int crew);
    // ohter public methods

private:
    // other members
    DifficultLvlStrategy _strategy;
};
int main() {
    auto strategy = [](Ship& ship[]){
        Ship::StatusCode status;
        ship.unload("Rum", ship.crew() / 2, status);
        Ship::StatusCode status2;
        ship.unload("Banana", ship.crew() / 2, status2);

        return status == Ship::StatusCode::OK && status2 == Ship::StatusCode::OK;
    };

    Ship ship(&time, strategy, "Black Widow", 1000, 40);
}

If we don't like templates

class Ship : public TimeObserver {
public:
    Ship(Time* time, std::function<size_t()> strategy, const std::string& name, int capacity, int crew);
    // ohter public methods

private:
    // other members
    std::function<size_t(Ship&)> _strategy;
};
int main() {
    auto strategy = [](Ship& ship[]){
        Ship::StatusCode status;
        ship.unload("Rum", ship.crew() / 2, status);
        Ship::StatusCode status2;
        ship.unload("Banana", ship.crew() / 2, status2);

        return status == Ship::StatusCode::OK && status2 == Ship::StatusCode::OK;
    };

    Ship ship(&time,strategy, "Black Widow", 1000, 40);
}