(C++ 소프트웨어 디자인 책 정리)
비지터 다지인 패턴은 GoF 서술한 고전적인 디자인 패턴 중 하나다. 타입보다는 연산을 자주 추가할 수 있게 하는데 중점을 둔다.
비지터 패턴의 의도는 객체 구조의 요소에 대해 수행할 연산을 나타낸다. 비지터를 사용하면 연산 대상 요소의 클래스를 변경하지 않고 새 연산을 정의할 수 있다.
아래 shape의 상송 계통을 보자
위 UML 에서 Shape클래스는 특정 도형에 대한 기초 클래스다. 지금 예에서는 Circle, Square, Trapezoid 클래스가 있다.
위 구조를 가지고 유지보수를 하던 중 새로운 연산 'rotate 함수' 를 구현해야 한다고 하자. Shape 클래스에 'rotate'연산을 추가하고 직접 정의한 파생 타입은 약간의 추가 노력으로 훌훌 털어낼 수 있다. 하지만 그 Shape 기초 클래스를 상속해 도형을 생성한 다른 사람들에게는 추가 작업을 유발할 수도 있다.(최악의 경우 다른 동료들에게부터 다음 번 바베큐 파티에서 제외당할 수도 있다.)
새 연산을 정말 자주 추가해야 한다면 연산을 쉽게 확장할 수 있게 디자인해야 한다. 비지터 디자인 패턴이 달성하려는 것이 이것이다.
Shape의 클래스 정의
struct Point
{
double x;
double y;
};
class Shape
{
public:
Shape() = default;
virtual ~Shape() = default;
virtual void draw() const = 0;
virtual Point getCenter() = 0; // 위 uml에는 누락되어 있음
};
여기에 Shape 상속 계통에 더해, ShapeVisitor 상속 계통을 도입한다. ShapeVisitor기초 클래스는 도형 연산의 추상화를 나타낸다.
ShapeVisitor 클래스에는 Shape 상속 계통 내 모든 구체 도형에 대해 순수 가상 visit() 함수가 하나씩 있다.
class Circle;
class Square;
class Trapezoid;
class ShapeVisitor
{
public:
ShapeVisitor() = default;
virtual ~ShapeVisitor() = default;
virtual void visit( Circle & ) = 0;
virtual void visit( Square & ) = 0;
virtual void visit( Trapezoid & ) = 0;
};
ShapeVisitor 기초 클래스가 있으니 이제 새 연산을 쉽게 추가할 수 있다. 새 연산을 추가할려면 새 파생 클래스를 추가하기만 하면 된다.
아까 위에서 언급한 'Rotate' 와 새로운 연산 'Move'를 추가해 보자. 이 연산에 대해 각 클래스를 도입만 하면 된다. 이 연산에 대해 지원하는 라이브러리별로 하나씩 여러 연산 클래스를 도입하는 것에 관해 생각할 수도 있다. 이 역시 쉽게 할 수 있다. 기존 코드를 수정할 필요가 없기 때문이다. 즉 새 코드를 추가해 ShapeVisitor 상속 계통을 계속 확장해 나가면 된다. 따라서 이 디자인은 연산 추가와 관련해 OCP를 충족한다.
새로운 연산 'Rotate' 클래스, 'Move' 클래스
class Rotate : public ShapeVisitor
{
public:
explicit Rotate() {}
void visit( Circle& c) override {
ignore = c;
cout << "Circle is Rotate !! " << endl;
}
void visit( Square& s) override {
ignore = s;
cout << "Square is Rotate !! " << endl;
}
void visit( Trapezoid& t) override {
ignore = t;
cout << "Trapezoid is Rotate !! " << endl;
}
};
class Move : public ShapeVisitor
{
private:
double movingX_;
double movingY_;
public:
explicit Move( double x, double y )
: movingX_(x), movingY_(y) {}
void visit( Circle& c) override {
c.setCenter(movingX_, movingY_);
cout << " Circle move x " << movingX_ << " move y " << movingY_ << endl;
}
void visit( Square& s) override {
s.setCenter(movingX_, movingY_);
cout << " Square move x " << movingX_ << " move y " << movingY_ << endl;
}
void visit( Trapezoid& t) override {
t.setCenter(movingX_, movingY_);
cout << " Trapezoid move x " << movingX_ << " move y " << movingY_ << endl;
}
};
위 설명한 새로운 연산 비지터들을 사용할려면 Shape 상속 계통에 마지막 함수를 하나 추가해야 한다. 바로 accept() 함수다.
accept 함수는 기초 클래스에 순수 가상 함수를 도입하므로 모든 파생 클래스에서 구현해야 한다.
class Circle : public Shape
{
public:
// ...
void accept( ShapeVisitor &&) override { v.visit( *this ); }
// ...
}
class Square : public Shape
{
public:
// ...
void accept( ShapeVisitor &&) override { v.visit( *this ); }
// ...
}
class trapezoid : public Shape
{
public:
// ...
void accept( ShapeVisitor &&) override { v.visit( *this ); }
// ...
}
이제 연산을 수행하는 곳에서 이 accept()함수를 사용할 수 있다.
void visitorAllShape()
{
std::vector<std::unique_ptr<Shape>> Shapes;
Shapes.push_back(std::make_unique<Circle>(5));
Shapes.push_back(std::make_unique<Square>(7));
Shapes.push_back(std::make_unique<Trapezoid>());
for (auto const& shape : Shapes)
{
shape->draw();
// move operation in visitor pattern
shape->accept(std::move(move5m));
// shape->accept( Move{1,5} );
Point getPoint = shape->getCenter();
cout << " x,y is " << getPoint.x << "," << getPoint.y << endl;
// rotate operation in visitor pattern
shape->accept(Rotate{});
}
}