비지터 패턴이란?

 

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://d-yong.tistory.com/56

https://a-researcher.tistory.com/28

https://bloodstrawberry.tistory.com/1401