방문자 패턴(Visitor Pattern)

  • 방문자 패턴은 공통된 객체의 데이터 구조와 처리를 분리하는 패턴입니다.
  • 서로의 메서드가 재귀적으로 호출되는 등 복잡한 호출 관계를 갖고 있어 이해하기 어려운 패턴 중 하나입니다.
  • 이처럼 복잡한 호출 관계를 갖는 이유는 데이터 객체와 작업 객체를 분리하기 때문입니다.

방문자 패턴의 구조

  • 방문자 패턴은 데이터 객체와 작업 객체가 분리되어 있습니다.
  • 따라서 두 가지 분리된 객체가 존재합니다.
    • 원소 객체(Element):
      • 원소 객체는 데이터를 보관하는 구조 클래스입니다.
    • 방문자 객체(Visitor)
      • 원소 객체의 호출 메서드에서 방문자 객체를 전달하고, 전달 받은 방문자 객체의 작업 메서드를 호출합니다.
    • 이처럼 호출 관계가 복잡합니다.

방문자 패턴: 원소(Element) 인터페이스 클래스

  • 원소 인터페이스는 제품의 이름, 가격, 수량을 보관하는 장바구니 객체입니다.
  • 공통된 함수를 인터페이스 함수에서 선언 및 구현합니다.
  • accept() 메서드는 방문자 객체를 매개변수로 하는 호출 메서드 입니다.
class Element
{
public:
    Element(std::string name, int price, int num) :
        m_name(name),
        m_price(price),
        m_num(num)
    {
    }

    // 방문자 객체 호출 메서드
    virtual void accept(Visitor* visitor) = 0;

    // 실제 구현 함수에서 작성해도 된다.
    void setName(std::string name)
    {
        m_name = name;
    }

    std::string getName() const
    {
        return m_name;
    }

    void setPrice(int price)
    {
        m_price = price;
    }

    int getPrice() const
    {
        return m_price;
    }

    void setNum(int num)
    {
        m_num = num;
    }

    int getNum() const
    {
        return m_num;
    }

protected:
    std::string m_name;
    int m_price;
    int m_num;
};

방문자 패턴: 원소(Element) 객체 구현

  • 상품의 공통된 메서드를 인터페이스 클래스에서 강제하였기 때문에 구현 클래스의 코드가 짧은 편입니다.
class Cart :
    public Element
{
public:
    Cart(std::string name, int price, int num = 1) :
        Element(name, price, num)
    {
    }

    void accept(Visitor* visitor)
    {
        visitor->order(this);
    }

    int getTax(int tax = 10)
    {
        return (m_price * m_num) * tax / 100;
    }

    std::string list()
    {
        std::string order = m_name + ", num: " + std::to_string(m_num) + ", price: " + std::to_string(m_price * m_num) + ".\n";
    }
};

방문자 패턴: 방문자(Visitor) 인터페이스 클래스

  • 방문자 객체의 인터페이스 클래스는 원소 객체를 호출하는 메서드가 존재합니다.
  • 원소 객체의 인터페이스 클래스에도 방문자 객체를 호출하는 메서드가 존재합니다.
  • 이처럼 두 인터페이스 클래스는 서로가 서로를 호출하는 메서드를 갖고 있습니다.
// 전방 선언
class Element;

class Visitor
{
public:
    virtual void order(Element* visitable) = 0;
};
  • 원소 객체를 전방 선언으로 구현하여 코드의 꼬임을 방지합니다.
  • 저는 하나의 파일에 전방 선언을 이용하여 인터페이스 클래스를 선언하였습니다.

방문자 패턴: 방문자(Visitor) 객체 구현

  • 방문자 객체의 구현은 원소 객체를 매개변수로 받아 이를 처리하는 order() 메서드를 구현합니다.
  • 물건을 구매하는 장바구니 객체로 설정하였으므로, 각 개별 객체의 상품 정보를 획득하고, 총 금액을 관리합니다.
class ConcreteVisitor :
    public Visitor
{
public:
    ConcreteVisitor()
    {
        m_total = 0;
        m_num = 0;
    }

    void order(Element* visitable)
    {
        std::cout << "Name: " << visitable->getName() << std::endl;
        std::cout << "Num: " << visitable->getNum() << std::endl;
        std::cout << "Price: " << visitable->getPrice() << std::endl;


        int total = visitable->getPrice() * visitable->getNum();
        std::cout << "Total Price: " << total << std::endl;
        m_total += total;

        m_num++;
    }

    int getTotal() const
    {
        return m_total;
    }

private:
    int m_total;
    int m_num;
};

방문자 패턴: 실행 코드

  • 상품 객체를 생성하고, 방문자 객체도 생성합니다.
  • 방문자 객체를 매개변수로 전달하여 이를 처리하는 과정을 확인합니다.
int main(const int argc, const char* argv[])
{
    Cart* noodle = new Cart("Noodle", 900, 2);
    Cart* cola = new Cart("CocaCola", 600, 4);
    ConcreteVisitor* visitor = new ConcreteVisitor();
    noodle->accept(visitor);
    cola->accept(visitor);

    std::cout << "Total: " << std::to_string(visitor->getTotal()) << std::endl;
}
  • 실행 결과
Name: Noodle
Num: 2
Price: 900
Total Price: 1800
Name: CocaCola
Num: 4
Price: 600
Total Price: 2400
Total: 4200
  • 방문자 패턴은 객체의 캡슐화를 위반하는 패턴입니다.
  • 객체의 모든 연산이 공개된 인터페이스로 작성되며 외부에 노출됩니다.
  • 하지만 방문자 패턴은 기존 객체에서 행위 동작을 분리하고 새로운 행위를 추가할 수 있는 유용한 패턴입니다.
  • 또한 기존 객체를 직접 수정하지 않고 행동을 대신 처리할 수 있는 장점도 있습니다.