4 분 소요

Visitor는 개체를 수정하지 않고도 새로운 기능을 추가할 수 있게 합니다.

설명

일반적으로 개체에 기능을 추가하고 싶은 경우 해당 개체에 멤버 함수를 작성하면 됩니다. 하지만, 자꾸 추가하다보면 개체의 덩치가 커지는 문제가 있습니다. 결국 블롭이 될 수도 있죠.

이를 막기 위해 Visitor 패턴을 활용할 수 있습니다.

다음 그림은 Visitor의 일반적인 구조입니다.

ElementAccept()Visitor를 전달하면, 각 자식 개체에서 관련 함수들을 실행해 줍니다. 따라서 추가할 기능을 ConcreteVisitor1이나 ConcreteVisitor2에서 구현해 주면 됩니다.

하지만, 아쉽게도 VisitorElement간에 상호 참조가 됩니다. 그래서 Visitor를 적용하실 때에는 기능 추가와 상호 참조에 따른 의존성 부패 간의 트레이드 오프가 필요하니 잘 고민해서 선택하셔야 합니다.

Visitor

구체적인 호출 흐름은 다음과 같습니다.

  1. Visitor는 기능을 추가할 ElementAccept(visitor)로 방문하고,
  2. ElementVisitor의 함수중에 자신의 타입과 맞는 VisitConcreteElementA(ConcreteElementA)를 호출합니다.
  3. 이제 VisitorConcreteElementA를 알 수 있으므로, ConcreteElementA의 Getter/Setter를 호출하여 추가 기능을 구현합니다.

Image

항목 내용
Visitor 방문할 개체에서 호출할 함수들을 정의합니다.
Element Visitor가 방문할 수 있는 Accept()를 정의합니다.
ConcreteVisitor1,ConcreteVisitor2 Visitor를 구체화한 개체입니다.
ConcreteElementA,ConcreteElementB Element를 구체화한 개체입니다.

특징

개체는 Visitor가 필요로 하는 충분한 정보를 제공해야 합니다. 이에 따라 멤버 변수의 Getter/Setter가 모두 public으로 노출되어 캡슐화가 깨지고, 빈혈 모델이 되는 단점이 있습니다.

하지만 단점만 있는 것은 아닙니다. 기능 추가시 개체에 직접 인터페이스를 추가하지 않고 Visitor만 추가하면 됩니다. 예를들어 Element의 자식 개체들이 10개가 있는 경우를 살펴봅시다. 새로운 기능을 추가하려고 하면, 보통은 자식 개체 10개 모두에 인터페이스 함수를 구현해야 합니다. 하지만, Visitor를 이용하면 Visitor 한개만 추가하면 되고, 해당 기능에 대한 처리가 Visitor에 응집되어 있어 유지보수도 쉬워집니다.

예제

다음은 RectangleCircle 개체에 방문하는 IVisitor의 구현 예입니다. XmlWriterJsonWriter로 방문하여 해당 개체의 내용을 저장합니다.

Image

  1. #1 : IVisitor는 개체를 방문합니다. 방문한 개체가 호출할 수 있도록 VisitRectangle()VisitCircle()을 제공합니다.
  2. #2 : ShapeIVisitor가 방문할 수 있도록 Accept()를 제공합니다.
  3. #3 : RectangleCircleIVisitor가 방문하면, 현 개체에 해당하는 함수를 호출합니다.
  4. #4 : 각 개체에서 VisitRectangle()이나 VisitCircle()을 호출하면 해당 기능을 실행합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class Shape; // 상호 참조가 되어 전방 선언이 필요합니다.
class Rectangle;
class Circle;

// ----
// #1. 개체를 방문합니다. 방문한 개체가 호출할 수 있도록 VisitRectangle()과 VisitCircle()을 제공합니다.
// ----
class IVisitor {
protected:
    IVisitor() = default; // 인터페이스여서 상속해서만 사용하도록 protected
    ~IVisitor() = default; // 인터페이스여서 다형 소멸을 하지 않으므로 protected non-virtual
private:
    IVisitor(const IVisitor&) = delete;
    IVisitor(IVisitor&&) = delete;
    IVisitor& operator =(const IVisitor&) = delete;
    IVisitor& operator =(IVisitor&&) = delete;
public:
    virtual void VisitRectangle(Rectangle& rectangle) = 0;
    virtual void VisitCircle(Circle& circle) = 0;
};
// ----
// #2. IVisitor가 방문할 수 있도록 Accept()를 제공합니다.
// ----
class Shape {
protected:
    Shape() = default; // 다형 소멸을 제공하는 추상 클래스. 상속해서만 사용하도록 protected
    Shape(const Shape&) = default; 
public:
    virtual ~Shape() = default; // 다형 소멸 하도록 public virtual 
private:
    Shape(Shape&&) = delete; 
    Shape& operator =(const Shape&) = delete; 
    Shape& operator =(Shape&&) = delete;   
public:
    virtual void Accept(IVisitor& visitor) = 0;
};
// ----
// #3. IVisitor가 방문하면, 현 개체에 해당하는 함수를 실행합니다.
// ----
class Rectangle : public Shape {
    int m_Left;
    int m_Top;
    int m_Width;
    int m_Height;
public:
    Rectangle(int l, int t, int w, int h) : m_Left{l}, m_Top{t}, m_Width{w}, m_Height{h} {}
public:
    int GetLeft() const {return m_Left;}
    int GetTop() const {return m_Top;}
    int GetWidth() const {return m_Width;}
    int GetHeight() const {return m_Height;}

    void SetLeft(int val) {m_Left = val;}
    void SetTop(int val) {m_Top = val;}
    void SetWidth(int val) {m_Width = val;}
    void SetHeight(int val) {m_Height = val;}   

    virtual void Accept(IVisitor& visitor) override {
        visitor.VisitRectangle(*this); // #3
    }
};
class Circle : public Shape {
    int m_CenterX;
    int m_CenterY;
    int m_Diameter;
public:
    Circle(int centerX, int centerY, int diameter) : m_CenterX(centerX), m_CenterY(centerY), m_Diameter(diameter) {}
public:
    int GetCenterX() const {return m_CenterX;}
    int GetCenterY() const {return m_CenterY;}
    int GetDiameter() const {return m_Diameter;}

    void SetCenterX(int val) {m_CenterX = val;}
    void SetCenterY(int val) {m_CenterY = val;}
    void SetDiameter(int val) {m_Diameter = val;}

    virtual void Accept(IVisitor& visitor) override {
        visitor.VisitCircle(*this); // #3
    }
};

// ----
// #4. 각 개체에서 호출하면, 해당 기능을 실행합니다.
// ----
class XmlWriter : public IVisitor {
public:
    XmlWriter() = default;

    virtual void VisitRectangle(Rectangle& rectangle) override {
        std::cout << "XmlWriter Rectangle" << std::endl; 
    };
    virtual void VisitCircle(Circle& rectangle) override {
        std::cout << "XmlWriter Circle" << std::endl; 
    }
};

class JsonWriter : public IVisitor {
public:
    JsonWriter() = default;

    virtual void VisitRectangle(Rectangle& rectangle) override {
        std::cout << "JsonWriter Rectangle" << std::endl; 
    };
    virtual void VisitCircle(Circle& rectangle) override {
        std::cout << "JsonWriter Circle" << std::endl; 
    }
};

// ----
// 테스트 코드
// ----
Rectangle rectangle{0, 0, 10, 20};
Circle circle{0, 0, 10};

XmlWriter xmlWriter;
rectangle.Accept(xmlWriter);
circle.Accept(xmlWriter);

JsonWriter jsonWriter;
rectangle.Accept(jsonWriter);
circle.Accept(jsonWriter);  

상기 RectangleCircle의 크기를 2배로 확대하는 기능을 추가해 봅시다. 보통은 ShapeScale() 인터페이스를 추가하여 구현하지만, 이렇게 되면, 자식 개체 모두에 인터페이스 함수를 구현해야 합니다. 하지만 Visitor를 사용한다면 다음과 같이 ScaleVisitor를 만들어 방문해서 처리 할 수 있습니다.

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ScaleVisitor : public IVisitor {
public:
    ScaleVisitor() = default; 
    
    virtual void VisitRectangle(Rectangle& rectangle) override {
        rectangle.SetWidth(rectangle.GetWidth() * 2); // 2배로 확대합니다.
        rectangle.SetHeight(rectangle.GetHeight() * 2);
    };
    virtual void VisitCircle(Circle& rectangle) override {
        rectangle.SetDiameter(rectangle.GetDiameter() * 2); // 2배로 확대합니다.
    }
};

// ----
// 테스트 코드
// ---- 
ScaleVisitor scaleVisitor; 
rectangle.Accept(scaleVisitor);
circle.Accept(scaleVisitor);

EXPECT_TRUE(rectangle.GetWidth() == 10 * 2 && rectangle.GetHeight() == 20 * 2);
EXPECT_TRUE(circle.GetDiameter() == 10 * 2);  

댓글남기기