2023年2月28日 星期二

C++:Visitor Pattern 以及 Strategy Pattern 的一體兩面

本文為 C++ Software Design 書的第 16 至 19 節內容。

Visitor Pattern 的目的是在不更改 class 定義的前提下能任意增加與 class 相關的函數。以下以畫圓形與方形為一個例子:

// 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 實作下每個類別都得支援這些操作,會讓整個類的定義變得巨大。