#23. [디자인 패턴-행위 패턴] Visitor
Visitor는 개체를 수정하지 않고도 새로운 기능을 추가할 수 있게 합니다.
설명
일반적으로 개체에 기능을 추가하고 싶은 경우 해당 개체에 멤버 함수를 작성하면 됩니다. 하지만, 자꾸 추가하다보면 개체의 덩치가 커지는 문제가 있습니다. 결국 블롭이 될 수도 있죠.
이를 막기 위해 Visitor 패턴을 활용할 수 있습니다.
다음 그림은 Visitor의 일반적인 구조입니다.
Element
의 Accept()
에 Visitor
를 전달하면, 각 자식 개체에서 관련 함수들을 실행해 줍니다. 따라서 추가할 기능을 ConcreteVisitor1
이나 ConcreteVisitor2
에서 구현해 주면 됩니다.
하지만, 아쉽게도 Visitor
와 Element
간에 상호 참조가 됩니다. 그래서 Visitor를 적용하실 때에는 기능 추가와 상호 참조에 따른 의존성 부패 간의 트레이드 오프가 필요하니 잘 고민해서 선택하셔야 합니다.
구체적인 호출 흐름은 다음과 같습니다.
Visitor
는 기능을 추가할Element
에Accept(visitor)
로 방문하고,Element
는Visitor
의 함수중에 자신의 타입과 맞는VisitConcreteElementA(ConcreteElementA)
를 호출합니다.- 이제
Visitor
는ConcreteElementA
를 알 수 있으므로,ConcreteElementA
의 Getter/Setter를 호출하여 추가 기능을 구현합니다.
항목 | 내용 |
---|---|
Visitor |
방문할 개체에서 호출할 함수들을 정의합니다. |
Element |
Visitor 가 방문할 수 있는 Accept() 를 정의합니다. |
ConcreteVisitor1 ,ConcreteVisitor2 |
Visitor 를 구체화한 개체입니다. |
ConcreteElementA ,ConcreteElementB |
Element 를 구체화한 개체입니다. |
특징
개체는 Visitor
가 필요로 하는 충분한 정보를 제공해야 합니다. 이에 따라 멤버 변수의 Getter/Setter가 모두 public
으로 노출되어 캡슐화가 깨지고, 빈혈 모델이 되는 단점이 있습니다.
하지만 단점만 있는 것은 아닙니다. 기능 추가시 개체에 직접 인터페이스를 추가하지 않고 Visitor
만 추가하면 됩니다. 예를들어 Element
의 자식 개체들이 10개가 있는 경우를 살펴봅시다. 새로운 기능을 추가하려고 하면, 보통은 자식 개체 10개 모두에 인터페이스 함수를 구현해야 합니다. 하지만, Visitor
를 이용하면 Visitor
한개만 추가하면 되고, 해당 기능에 대한 처리가 Visitor
에 응집되어 있어 유지보수도 쉬워집니다.
예제
다음은 Rectangle
과 Circle
개체에 방문하는 IVisitor
의 구현 예입니다. XmlWriter
와 JsonWriter
로 방문하여 해당 개체의 내용을 저장합니다.
- #1 :
IVisitor
는 개체를 방문합니다. 방문한 개체가 호출할 수 있도록VisitRectangle()
과VisitCircle()
을 제공합니다. - #2 :
Shape
은IVisitor
가 방문할 수 있도록Accept()
를 제공합니다. - #3 :
Rectangle
과Circle
은IVisitor
가 방문하면, 현 개체에 해당하는 함수를 호출합니다. - #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);
상기 Rectangle
과 Circle
의 크기를 2배로 확대하는 기능을 추가해 봅시다. 보통은 Shape
에 Scale()
인터페이스를 추가하여 구현하지만, 이렇게 되면, 자식 개체 모두에 인터페이스 함수를 구현해야 합니다. 하지만 Visitor
를 사용한다면 다음과 같이 ScaleVisitor
를 만들어 방문해서 처리 할 수 있습니다.
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);
댓글남기기