本文為 C++ Software Design 書的第 16 至 19 節內容。
// Interfaces
class ShapeVisitor
{
public:
virtual void visit(Circle const&) const = 0;
virtual void visit(Square const&) const = 0;
};
class Shape
{
public:
virtual void accept(ShapeVisitor const& v) = 0;
};
// Implementations
class Circle : public Shape
{
void accept(ShapeVisitor const& v) override { v.visit(*this); }
};
class Square: public Shape
{
void accept(ShapeVisitor const& v) override { v.visit(*this); }
};
class Draw : public ShapeVisitor
{
public:
void visit(Circle const&) const override;
void visit(Square const&) const override;
}
從以上例子可以看出要加入其他圓形與方形的函數相當容易,只要繼承 ShapeVisitor 即可,因此也可以說 Visitor Pattern 為 open set of operations。
Visitor Pattern 最主要的問題是要加入新的形態(像是三角形)就會變得很麻煩:ShapeVisitor 以及所有繼承它的函數都得更改,因此為 closed set of types。另一個缺點是必須加入侵入式的函數 accept(),並且由於使用了兩個 virtual function(稱為 double dispatch),會影響實際上的效能。但好消息是下面一段我們會介紹使用 std::variant 來改進此缺點。
以下為使用 std::variant 實作 Visitor Pattern 的例子:
class Circle {};
class Square {};
using Shape = std::variant<Circle, Square>
struct Draw
{
void operator()(Circle const& c) const {}
void operator()(Square const& c) const {}
};
void drawShape(Shape const& shape)
{
std::visit(Draw{}, shape);
}
可以看出在使用 std::variant 以後原先提到的問題就解決了,但代價仍然是增加新型態的麻煩,以及當型態們的大小相差太大時會影響整體的效能。
Strategy Pattern 的目的是把相關的演算法封裝起來讓 class 能交互使用。以下為相同的例子:
// Interfaces
template<typename T>
class DrawStrategy
{
public:
virtual void draw(T const&) const = 0;
};
class Shape
{
public:
virtual void draw() const = 0;
};
// Implementations
class Circle : public Shape
{
public:
explicit Circle(double radius, std::unique_ptr<DrawStrategy<Circle>> drawer) {};
void draw() const override { drawer_->draw(*this); }
private:
std::unique_ptr<DrawStrategy<Circle>> drawer_;
};
class Square : public Shape
{
public:
explicit Square(double radius, std::unique_ptr<DrawStrategy<Square>> drawer) {};
void draw() const override { drawer_->draw(*this); }
private:
std::unique_ptr<DrawStrategy<Square>> drawer_;
};
// Algorithms
template<typename T>
class OpenGLCircleStrategy : public DrawStrategy<T>
{
public:
void draw(T const&) const override {};
};
與 Visitor Pattern 比較後可以看出 Visitor Pattern 是把增加演算法或函數當成變異點(也就是繼承 ShapeVisitor 的類別),但是無法輕鬆地加入新的形態。Strategy Pattern 是將演算法的實作細節當成變異點(也就是繼承 DrawStrategy 的類別),雖然可以輕鬆地增加型態(像是三角形),但是要加入新的函數(draw 以外的函數)就很麻煩了。另外當我們需要好幾種操作時,Strategy Pattern 實作下每個類別都得支援這些操作,會讓整個類的定義變得巨大。