trainings/CreatingReliableSoftwareCpp/Presentation/visitor.md

600 lines
No EOL
20 KiB
Markdown

# Visitor
Another popular design pattern is **Visitor**. As previously, I'm certain that You also use it at least once, but maybe you didn't know about it. Let's check the following STL algorithm:
```C++
class VariantVisitor
{
public:
void operator()(int num) const {
std::cout << "int: " << num << "\n";
}
void operator()(std::string str) const {
std::cout << "string: " << str << "\n";
}
};
int main() {
std::variant<int, std::string, double> variant;
variant = "Ala has a cat";
// print: string: Ala has a cat
std::visit(VariantVisitor{}, variant);
}
```
<!-- .slide: style="font-size: 0.84em" -->
<!-- .element: class="fragment fade-in" -->
**Seems familiar? If not, let's check another example**
<!-- .element: class="fragment fade-in" -->
___
```C++
class Shape
{
public:
virtual ~Shape() = default;
virtual void accept( ShapeVisitor const& v ) = 0; 3
};
class Circle : public Shape
{
public:
explicit Circle( double radius ): radius_( radius ) {}
void accept( ShapeVisitor const& v ) override { v.visit( *this ); }
double radius() const { return radius_; }
private:
double radius_;
};
class Square : public Shape
{
public:
explicit Square( double side ): side_( side ) {}
void accept( ShapeVisitor const& v ) override { v.visit( *this ); }
double side() const { return side_; }
private:
double side_;
};
```
```C++
class Draw : public ShapeVisitor
{
public:
void visit( Circle const& c, /*...*/ ) const override;
void visit( Square const& s, /*...*/ ) const override;
};
class Translate : public ShapeVisitor
{
public:
void visit( Circle const& c, /*...*/ ) const override;
void visit( Square const& s, /*...*/ ) const override;
};
```
<!-- .slide: style="font-size: 0.57em" -->
<!-- .element: class="fragment fade-in" -->
___
## Visitor
These two examples were two different implementations of the same design pattern which is `Visitor`. In short, the visitor is used to dispatch functions between a few types.
<img data-src="images/visitor.png" alt="Visitor UML">
<!-- .slide: style="font-size: 0.99em" -->
<!-- .element: class="fragment fade-in" -->
___
## Example
Let's back to the pirates. In the previous example, we implemented the ammunition class and added an implementation of `fire` and `draw` methods, so what next? We need to add also `methods` that will calculate how much damage each ammunition does and what sound should be played.
<img data-src="images/visitor_first_apprach.png" alt="Visitor first apprach">
<!-- .slide: style="font-size: 0.99em" -->
<!-- .element: class="fragment fade-in" -->
___
## Drawback
In the first approach, we can see that we tightly coupled the base class `Visitor` with a base class `Ammunition` and the implementation of `Visitor` with types of `Ammunition`. So we have a lot of dependencies. Unfortunately, the visitor is a nice pattern, but it is good only for extending functions, but it is closed for adding new types because when we add a new type of ammunition we also need to implement every visitor class. There is a solution for this, but it is inefficient, and can't be always used. I will talk about this solution later. For now, it should be enough to know that there is sth like `Acyclic visitor` that can solve this problem, but it is inefficient. In this solution, we have double dispatch and the compiler can't optimize such code, also we have to call 2 or 3 virtual functions which are not as fast as calling normal functions.
___
## Implementation
let's start by defining an interface for both, the `Ammunition` and `Visitor` class.
```C++
class Ammunition {
public:
Ammunition(size_t amount)
: _amount(amount) {}
virtual ~Ammunition() = default;
virtual void accept(const AmmunitionVisitor&) = 0;
size_t amount() const { return _amount; }
private:
size_t _amount = 0;
};
```
```C++
class AmmunitionVisitor {
public:
virtual ~AmmunitionVisitor() = default;
virtual void visit(const RoundShot&) const = 0;
virtual void visit(const ChainShot&) const = 0;
virtual void visit(const GrapeShot&) const = 0;
};
```
<!-- .slide: style="font-size: 0.88em" -->
<!-- .element: class="fragment fade-in" -->
___
Each class that is derived from `Ammunition` needs to create a definition of the `accept` method. The is needed because each class passes a pointer to `*this` which allows us to distinguish which inherited class was provided.
```C++
class RoundShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AmmunitionVisitor& visitor) {
visitor.visit(*this);
}
};
class ChainShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AmmunitionVisitor& visitor) {
visitor.visit(*this);
}
};
class GrapeShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AmmunitionVisitor& visitor) {
visitor.visit(*this);
}
};
```
<!-- .slide: style="font-size: 0.82em" -->
<!-- .element: class="fragment fade-in" -->
___
At the end we provide implementation for each visitor
```C++
class DamageVisitor : public AmmunitionVisitor {
public:
void visit(const RoundShot& shot) const override {
std::cout << "Round shot make 20 damages\n";
}
void visit(const ChainShot& shot) const override {
std::cout << "Chain shot make 30 damages\n";
}
void visit(const GrapeShot& shot) const override {
std::cout << "Grape shot make 40 damages\n";
}
};
class SoundVisitor : public AmmunitionVisitor {
public:
void visit(const RoundShot& shot) const override {
std::cout << "Fiuuuuuuuu Bam!\n";
}
void visit(const ChainShot& shot) const override {
std::cout << "Fiuuuuuuuuuuu Bam bam!\n";
}
void visit(const GrapeShot& shot) const override {
std::cout << "Fiuuuuuuuuuuu Bam bam bam bam bam!\n";
}
};
```
<!-- .slide: style="font-size: 0.77em" -->
<!-- .element: class="fragment fade-in" -->
___
## Usage
```C++
int main() {
std::unique_ptr<Ammunition> grapeShot = std::make_unique<GrapeShot>(100);
SoundVisitor soundVisitor;
DamageVisitor damageVisitor;
grapeShot->accept(soundVisitor);
grapeShot->accept(damageVisitor);
}
```
```bash
Fiuuuuuuuuuuu Bam bam bam bam bam!
Grape shot make 40 damages
```
<!-- .slide: style="font-size: 0.94em" -->
<!-- .element: class="fragment fade-in" -->
___
## Drawbacks
Visitor in this approach has 4 drawbacks:
* <!-- .element: class="fragment fade-in" --> First I mentioned earlier it will be hard to add a new type, we can solve it by using <code>acyclic visitor</code>
* <!-- .element: class="fragment fade-in" --> Visitor is tightly coupled with other classes and not elastic. For instance, <code>DamageVisitor</code> implementations may be similar, but we need to implement a method for every class. We can separate common parts to another function, but still, it will be great if we can have one function for all types if needed. By using visitors, this is not possible
* <!-- .element: class="fragment fade-in" --> If we want to introduce visitors in an already implemented hierarchy of classes we need to add the <code>accept</code> method to every base class, sometimes we don't have access to all files in the repo, or the repository is divided and it is not possible. There is a solution for this, which I will show you later
* <!-- .element: class="fragment fade-in" --> If we add a new class that inherits from an already existing type, and we forget to <code>override</code> method, we will have the same behavior as the class that we inherit, which is a huge bug!
<!-- .slide: style="font-size: 0.94em" -->
___
## Improvements
As I said a few times, the modern approach uses by-value semantics, instead of virtual functions. We can use static or compile-time polymorphism to achieve the same and have even better performance. let's rewrite this example to use `std::variant` which allows us to decouple classes, and make a code more flexible and even faster (I will show you a benchmark later).
```C++
class Ammunition {
public:
Ammunition(size_t amount)
: _amount(amount) {}
virtual ~Ammunition() = default;
size_t amount() const { return _amount; }
private:
size_t _amount = 0;
};
class RoundShot final : public Ammunition {
public:
using Ammunition::Ammunition;
};
class ChainShot final : public Ammunition {
public:
using Ammunition::Ammunition;
};
class GrapeShot final : public Ammunition {
public:
using Ammunition::Ammunition;
};
```
<!-- .slide: style="font-size: 0.67em" -->
<!-- .element: class="fragment fade-in" -->
___
```C++
class DamageVisitor {
public:
void operator()(const RoundShot& shot) const {
std::cout << "Round shot make 20 damages\n";
}
void operator()(const ChainShot& shot) const {
std::cout << "Chain shot make 30 damages\n";
}
void operator()(const GrapeShot& shot) const {
std::cout << "Grape shot make 40 damages\n";
}
};
class SoundVisitor {
public:
void operator()(const RoundShot& shot) const {
std::cout << "Fiuuuuuuuu Bam!\n";
}
void operator()(const ChainShot& shot) const {
std::cout << "Fiuuuuuuuuuuu Bam bam!\n";
}
void operator()(const GrapeShot& shot) const {
std::cout << "Fiuuuuuuuuuuu Bam bam bam bam bam!\n";
}
};
```
<!-- .slide: style="font-size: 0.74em" -->
___
We get rid of a lot of virtual functions and simplify the code.
```C++
int main() {
using Shot = std::variant<RoundShot, ChainShot, GrapeShot>;
Shot shot = GrapeShot(100);
SoundVisitor soundVisitor;
DamageVisitor damageVisitor;
std::visit(damageVisitor, shot);
std::visit(soundVisitor, shot);
}
```
<!-- .element: class="fragment fade-in" -->
```bash
Grape shot make 40 damages
Fiuuuuuuuuuuu Bam bam bam bam bam!
```
<!-- .slide: style="font-size: 0.94em" -->
<!-- .element: class="fragment fade-in" -->
___
## Benchmark
I created a cimple <a href="https://quick-bench.com/q/4_rzq_ZdlvLPybt0YjV7_Oob0d8">bechmark</a> to show you, that visitors based on variant may be faster. It depends on how heavy the is function visit, if we perform a fast operation, we will gain a lot, but if the operation is heavy we will barely see the difference between both solutions.
<img data-src="images/visitor_bench.png" alt="Visitor benchamrk">
<!-- .element: class="fragment fade-in" -->
___
## Simplified class hierarchy
<img data-src="images/visitor_better_design.png" alt="Visitor better design">
___
## Drawback std::variant
We improved our code, but still, one problem exists, we can't easily add new types. The variant also provides another drawback: When types differ in size a lot, we will waste a lot of memory. In such a case we may think about using `std::unique_ptr` but this will force us to use dynamic allocation, so we lost some improvements (not fully use by-value semantic).
<!-- .element: class="fragment fade-in" -->
Last things left: How to eliminate this problem by adding a new type? Let's check the `acyclic variant` class:
<!-- .slide: style="font-size: 0.94em" -->
<!-- .element: class="fragment fade-in" -->
___
## Acyclic visitor
<img data-src="images/visitor_acyclic.png" alt="Visitor acyclic">
___
## Implementation
First, we create an abstract visitor class and three types of visitor for each type. This allows us to cut dependency between each visitor, because each visitor has knowledge about only one ammunition type.
```C++
class AbstractVisitor {
protected:
// Can be only use by derived class
virtual ~AbstractVisitor() = default;
};
class RoundShotVisitor {
public:
virtual void visit(const RoundShot& shot) const = 0;
protected:
virtual ~RoundShotVisitor() = default;
};
class GrapeShotVisitor {
public:
virtual void visit(const GrapeShot& shot) const = 0;
protected:
virtual ~GrapeShotVisitor() = default;
};
class ChainShotVisitor {
public:
virtual void visit(const ChainShot& shot) const = 0;
protected:
virtual ~ChainShotVisitor() = default;
};
```
<!-- .slide: style="font-size: 0.64em" -->
<!-- .element: class="fragment fade-in" -->
___
Next, we create dedicated visitors for `damage` and for `sound`. To hide implementations detail, each class inherit also from `AbstractVisitor`. This allows to use any visitor as pointer to this abstract class.
```C++
class DamageVisitor : public AbstractVisitor,
public RoundShotVisitor,
public GrapeShotVisitor,
public ChainShotVisitor {
public:
void visit(const RoundShot& shot) const override {
std::cout << "Round shot make 20 damages\n";
}
void visit(const ChainShot& shot) const override {
std::cout << "Chain shot make 30 damages\n";
}
void visit(const GrapeShot& shot) const override {
std::cout << "Grape shot make 40 damages\n";
}
};
class SoundVisitor : public AbstractVisitor,
public RoundShotVisitor,
public GrapeShotVisitor,
public ChainShotVisitor {
public:
void visit(const RoundShot& shot) const override {
std::cout << "Fiuuuuuuuu Bam!\n";
}
void visit(const ChainShot& shot) const override {
std::cout << "Fiuuuuuuuuuuu Bam bam!\n";
}
void visit(const GrapeShot& shot) const override {
std::cout << "Fiuuuuuuuuuuu Bam bam bam bam bam!\n";
}
};
```
<!-- .slide: style="font-size: 0.62em" -->
<!-- .element: class="fragment fade-in" -->
___
For now, everything looks ok. But unfortunately, because `AbstracVisitor` has empty interface, we need to cast visitor to a specific type. This `dynamic_cast` cost a lot and make code harder to optimize. That's why this visitor is much slower than previous implementations.
```C++
class RoundShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AbstractVisitor& visitor) {
if (const auto* v = dynamic_cast<const RoundShotVisitor*>(&visitor)) {
v->visit(*this);
}
}
};
class ChainShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AbstractVisitor& visitor) {
if (const auto* v = dynamic_cast<const ChainShotVisitor*>(&visitor)) {
v->visit(*this);
}
}
};
class GrapeShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AbstractVisitor& visitor) {
if (const auto* v = dynamic_cast<const GrapeShotVisitor*>(&visitor)) {
v->visit(*this);
}
}
};
```
<!-- .slide: style="font-size: 0.62em" -->
<!-- .element: class="fragment fade-in" -->
___
## Usage
The usage of this code doesn't change in comparison to the first implementation. But we lost on efficiency.
```C++
int main() {
std::unique_ptr<Ammunition> grapeShot = std::make_unique<GrapeShot>(100);
SoundVisitor soundVisitor;
DamageVisitor damageVisitor;
grapeShot->accept(soundVisitor);
grapeShot->accept(damageVisitor);
}
```
```bash
Fiuuuuuuuuuuu Bam bam bam bam bam!
Grape shot make 40 damages
```
<!-- .slide: style="font-size: 0.92em" -->
<!-- .element: class="fragment fade-in" -->
___
## Lost efficiency
<img data-src="images/acyclic_visitor_bench.png" alt="Acyclic visitor benchmark">
___
## Improvement
When visitor operations are quick, better to avoid acyclic visitor to not make the code slower. But when the operations are heavy, the difference will be slightly visible, and we will decouple visitor and ammunition classes. There is also one improvement, that make a code easier to extend. Instead of writing every time a visitor for a type, we can use template.
```C++
template <typename T>
class Visitor {
public:
virtual void visit(const T& shot) const = 0;
protected:
virtual ~Visitor() = default;
};
class DamageVisitor : public AbstractVisitor,
public Visitor<RoundShot>,
public Visitor<ChainShot>,
public Visitor<GrapeShot> {
public:
void visit(const RoundShot& shot) const override {
std::cout << "Round shot make 20 damages\n";
}
void visit(const ChainShot& shot) const override {
std::cout << "Chain shot make 30 damages\n";
}
void visit(const GrapeShot& shot) const override {
std::cout << "Grape shot make 40 damages\n";
}
};
```
<!-- .slide: style="font-size: 0.72em" -->
<!-- .element: class="fragment fade-in" -->
___
```C++
class RoundShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AbstractVisitor& visitor) {
if (const auto* v = dynamic_cast<const Visitor<RoundShot>*>(&visitor)) {
v->visit(*this);
}
}
};
class ChainShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AbstractVisitor& visitor) {
if (const auto* v = dynamic_cast<const Visitor<ChainShot>*>(&visitor)) {
v->visit(*this);
}
}
};
class GrapeShot : public Ammunition {
public:
using Ammunition::Ammunition;
void accept(const AbstractVisitor& visitor) {
if (const auto* v = dynamic_cast<const Visitor<GrapeShot>*>(&visitor)) {
v->visit(*this);
}
}
};
```
<!-- .slide: style="font-size: 0.62em" -->
___
## UML
<img data-src="images/acyclic_visitor_uml.png" alt="Acyclic visitor UML">
___
## Exericse 1
* <!-- .element: class="fragment fade-in" --> Go into directory <code>Core</code> and implement a base class <code>PrintVisitor</code> which should implement <code>visit</code> method for two objects: <code>Ship</code> and <code>Store</code>
* <!-- .element: class="fragment fade-in" --> Create <code>PrettyPrintVisitor</code> which will print cargo from <code>Ship</code> and <code>Sotore</code> (just move the implementation from <code>printCargo</code>).
* <!-- .element: class="fragment fade-in" --> Add the rest of necessary implementation for <code>Ship</code> and <code>Store</code>. Visitor should be taken as <code>std::unique_ptr</code>
* <!-- .element: class="fragment fade-in" --> Verify in the <code>main.cpp</code> if cargo print in the same way as previously.
___
## Exercise 2
* <!-- .element: class="fragment fade-in" --> Create another visitor: <code>BasicPrintVisitor</code> which should print the cargo but without special frames, just raw info.
* <!-- .element: class="fragment fade-in" --> For <code>Ship</code> print Name and Amount
* <!-- .element: class="fragment fade-in" --> For <code>Store</code> print Name Amount and Price
* <!-- .element: class="fragment fade-in" --> You shouldn't modify anything in your code, just change the visitor in <code>main.cpp</code> and you should get a new output
___
## Exercise 3
* <!-- .element: class="fragment fade-in" --> Create CargoDamageVisitor. You don't need to create a base class, because we know that we will not extend this code,
* <!-- .element: class="fragment fade-in" --> You should create three <code>operator()</code> one for each cargo type: Alcohol, Fruit, Item,
* <!-- .element: class="fragment fade-in" --> For fruit, you should draw a number between <b>0</b> and <b>5</b> and subtract <b>such an amount of cargo</b>
* <!-- .element: class="fragment fade-in" --> For alcohol, you should draw a number between <b>0</b> and <b>10</b> and subtract <b>half</b> an amount of cargo
* <!-- .element: class="fragment fade-in" --> For item, you should draw a number between <b>0</b> and <b>20</b> and subtract <b>one quarter</b> of cargo
* <!-- .element: class="fragment fade-in" --> Visitor should be taken as a template argument by Player class
* <!-- .element: class="fragment fade-in" --> Apply visitor whenever you successfully deal damage to ship
* <!-- .element: class="fragment fade-in" --> Check in the main class if ship lost cargo after get damaged.
___
## Summary
As you can see visitor is a very useful pattern to add additional functionality to your class, in such was you don't need to modify already existing code, instead you can inherit from visitor and create totally new functionality. You can also use `std::variant` if you want to avoid modify original class (by adding accept method) or when you can't modify class (for instance it is in separate repo). Version, with variant, is also more flexible and faster than traditional implementation. If you really want to break all dependencies between visitor class and subject, you can use acyclic visitor, but this solution is less efficient, so you need to be sure you can accept this in your code.