비지터 패턴이란? |
1) 어려운 설명
알고리즘을 객체 구조에서 분리시키는 디자인 패턴
일반적으로 컴포지트 패턴같은 계층적 구조로 형성된 데이터에서 사용함
2) 쉬운 설명
예를들어 컴파짓 패턴에서의 예시처럼 폴더/파일 클래스가 있을 때 showName()이라는 파일의 이름을 출력해주는 함수가 있다고 하자.
그런데 해당 파일의 이름을 모두 대문자로 출력받아야 할 일이 있다면?
클래스내에 이름을 대문자로 변환하는 함수를 추가할 수 있겠지만
문자를 변환하는 기능은 파일객체의 고유 특징과 관련이 없는 행동이므로 해당 클래스 내에 구현하기에는 좋지 않다.
즉 클래스와 메서드가 서로 크게 관련이 없다.
이런 상황에서 클래스의 구조는 크게 변경하지 않고 메서드를 추가하는 패턴이다.
다시 말하지만 크게 변경하지 않을 뿐 accept함수 구현을 위해 한번의 변경이 필요하다.
이것때문에라도 지금의 이해 상태에서는 대부분의 경우는 그냥 어댑터패턴을 쓰는게 좋지 않을까 싶긴하다.
비지터 패턴의 구조 |
1) Element
- 원본 클래스이며 컴포지트 구조에서 주로 사용하므로 Element가 컴포지트의 상위 클래스인 Component가 된다.
- 또는 컴포지트구조가 아닌 단일구조라면 그냥 상위 추상클래스이다.
2) ConcreteElement(ElementA, ElementB)
- 원본 클래스의 하위 구조인 Leaf와 Composite이 된다.
- 또는 그냥 단일구조 여러가지여도 문제없다.
3) Visitor
- visit()함수를 인터페이스로 작성한 추상클래스
- visit()의 매개변수를 ElementA로 받는지, ElementB로 받는지에따라 다르게 오버로딩한다.
4) ConcreteVisitor(Visitor1)
- 하나의 ConcreteVisitor마다 하나의 함수역할을 한다고 생각하면 된다.
- Visitor1은 showName()을 대문자로 변환해서 출력해주는 함수라면 Visitor2는 소문자로 변환해서 출력하는 함수로 구현하는 등으로 생각할 수 있다.
비지터 패턴 구현 예시 |
변수 a와 b를 갖고 있는 Element가 있다.
Element에 더하기함수와 곱하기 함수를 추가하고자 한다.
#include <iostream>
using namespace std;
/*
Element 인터페이스
*/
class Element
{
public:
virtual ~Element() {};
virtual void accept(Visitor* visitor) = 0;
};
class IntElement : public Element
{
private:
int a, b;
public:
IntElement(int a, int b) : a(a), b(b) {}
~IntElement() { }
// accept함수에 visitor를 매개변수로 받는다.
// visitor에 따라 어떤 연산을 할 지 달라진다.
virtual void accept(Visitor* visitor) override {
visitor->visit(this);
}
int getA() { return a; }
int getB() { return b; }
};
class DoubleElement : public Element
{
private:
double a, b;
public:
DoubleElement(double a, double b) : a(a), b(b) {}
~DoubleElement() { }
// accept함수에 visitor를 매개변수로 받는다.
// visitor에 따라 어떤 연산을 할 지 달라진다.
virtual void accept(Visitor* visitor) override {
visitor->visit(this);
}
double getA() { return a; }
double getB() { return b; }
};
/*
Visitor 인터페이스
*/
class Visitor {
public:
virtual ~Visitor() {};
// 매개변수를 ElementA로 받는지, ElementB로 받는지에 따라 오버로딩
virtual void visit(IntElement* intElement) = 0;
virtual void visit(DoubleElement* doubleElement) = 0;
};
// 더하기용 ConcreteVisitor
class AddVisitor : public Visitor
{
virtual void visit(IntElement* intElement) override {
int sum = intElement->getA() + intElement->getB();
cout << "int Element의 더하기용 Visitor 결과 : " << sum << endl;
}
virtual void visit(DoubleElement* doubleElement) override {
double sum = doubleElement->getA() + doubleElement->getB();
cout << "double Element의 더하기용 Visitor 결과 : " << sum << endl;
}
};
// 곱하기용 ConcreteVisitor
class MulVisitor : public Visitor
{
virtual void visit(IntElement* intElement) override {
int mul = intElement->getA() * intElement->getB();
cout << "int Element의 곱하기용 Visitor 결과 : " << mul << endl;
}
virtual void visit(DoubleElement* doubleElement) override {
double mul = doubleElement->getA() * doubleElement->getB();
cout << "double Element의 곱하기용 Visitor 결과 : " << mul << endl;
}
};
/*
Client 코드
*/
int main(int argc, const char* argv[]) {
IntElement* intElement = new IntElement(1, 2);
DoubleElement* doubleElement = new DoubleElement(3.2, 2.3);
/*
int, double객체에 더하기, 곱하기 연산을 만드는것은 연관성이 떨어지므로
더하기, 곱하기 연산을 수행할 visitor가 방문해서 연산하게 만듬
*/
AddVisitor* addVisitor = new AddVisitor();
MulVisitor* mulVisitor = new MulVisitor();
/*
Element가 accept함수를 호출하되
어떤 Visitor가 방문하냐에 따라 다른 알고리즘이 적용됨
*/
intElement->accept(addVisitor);
doubleElement->accept(addVisitor);
intElement->accept(mulVisitor);
doubleElement->accept(mulVisitor);
return 0;
}
장단점 |
1) 장점
- 관련 없는 메서드를 원본 클래스에 추가할 필요 없으므로 단일책임원칙을 준수할 수 있다.
- 처음에 accept()함수를 추가할때 OCP가 위배되지만 한번 accept()함수를 추가했다면 그 다음부턴 클래스를 변경하지 않아도 새로운 기능을 하는 visitor 클래스만 새로 만들면 되므로 새로운 행동을 OCP를 준수하며 추가할 수 있다.
2) 단점
- Visitor가 Element의 멤버 객체에 접근해야 하므로 접근 권한을 public으로 두거나 getter를 둬야 하므로 접근 권한이 제한되거나 캡슐화에 위배될 수 있다.
- 객체의 동작이 아닌 구조를 변경하는 경우 비지터 패턴도 수정해야 한다.
※ 참고 문헌
https://a-researcher.tistory.com/28
https://bloodstrawberry.tistory.com/1401