디자인 패턴/GoF

[디자인 패턴] 행동 패턴 (10) 방문자 Visitor

로파이 2021. 4. 7. 00:24

방문자 (Visitor) 패턴

 

1. 의도

 

객체 내부 원소에 대해 수행할 연산을 표현하는 방법. 연산을 적용할 원소의 클래스를 변경하지 않고 새로운 연산을 정의한다.

 

2. 활용

 

- 추상 구문트리

 

프로그램 코드를 구성하는 구문의 의미를 분석하는 컴파일러가 있다 하자. 컴파일러는 트리 구조의 코드를 추적하며 각 구문이 의미하는 변수 타입, 최적화, 흐름 분석, 문법 검사 등을 수행할 것이다. 이러한 연산을 각 구문을 구성하는 노드에 정의하면 다음과 같이 클래스 구조가 정의된다.

 

위와 같이 새로운 노드 클래스를 정의할 때마다 그에 맞는 컴파일러 연산을 정의해야한다. 문제는 이러한 연산이 클래스 전반적으로 퍼져있기 때문에 컴파일러 연산에 대한 유지보수와 새로운 기능 정의 등이 어렵다는 점이다.

 

따라서 각 노드에 대한 연산을 추상화 시키고 하나의 객체로 묶는 패턴을 방문자라고한다. 방문자 객체는 각 추상 트리 구문 노드를 방문하여 (Accept) 필요한 연산을 수행하고 빠져나가게 된다.

컴파일러 연산을 묶어서 정의한 방문자 클래스의 예시를 보면 타입 점검을 위한 방문자와 최적화된 코드를 생성하는 방문자 클래스가 있을 수 있다. 각 클래스는 방문할 수 있는 모든 노드 타입에 따른 연산을 정의해야한다.

프로그램에 귀속된 각 구문 노드에서 방문자를 수락(accept)하여 자신에 대한 컴파일러 연산을 수행할 수 있게 한다.

 

3. 참여자

방문자 패턴

Visitor 패턴

  • Visitor(NodeVisitor): 노드를 방문하여 수행할 수 있는 모든 연산을 선언한다. 각 연산을 통해 노드에 대한 참조/포인터를 전달받고 이에 대한 연산을 수행한다.
  • ConcreteVisitor(TypeCheckingVisitor): Visitor 클래스에 선언된 연산을 구현합니다. 
  • Element(Node): 방문자를 인자로 받아들이는 Accept() 연산을 정의한다.
  • ConcreteElement(AssignmentNode, VariableNode): 인자로 방문자 객체를 받아들이는 Accept() 연산을 구현합니다.
  • ObjectStructure(Program): 방문자가 접근할 수 있는 노드에 대한 인터페이스를 제공한다.

4. 협력 방법

  • 방문자 패턴을 사용하는 사용자는 ConcreteVisitor 클래스의 객체를 생성하고 객체 구조를 따라서 각 원소를 방문하며 순회해야한다.
  • 구성 원소들을 방문할때, 구성 원소는 해당 클래스의 Visitor 연산을 호출하게 된다.

5. 결과

 

  1) Visitor 클래스는 새로운 연산을 쉽게 추가한다.

  • 복잡한 객체를 구성하는 요소에 접근하고 처리하는 연산을 노드에 정의하지 않고 Visitor에 추가하도록한다.

  2) 방문자를 통해 관련된 연산을 하나로 묶을 수 있다.

 

  3)  새로운 ConcreteElement 클래스를 추가하기가 어렵다.

  • ConcreteElement는 방문자가 방문하여 자신에 대한 연산을 할 수 있도록 정의해야한다.

  4) 클래스 계층 구조에 따라 방문한다.

 

  5) 상태를 누적할 수 있다.

  • 방문 상태를 기록하여 모든 노드에 방문할 수 있도록한다.

6. 구현

 

1. 각 객체 구조는 자신과 연관된 Visitor 클래스를 가진다. 이 추상 Visitor 클래스는 객체 구조를 정의하는 각각 ConcreteElement의 클래스를 위한 VisitConcreteElement() 연산을 정의한다.

class ElementA;
class ElementB;

class Visitor
{
public:
	virtual void VisitElementA(ElementA*);
	virtual void VisitElementB(ElementB*);
protected:
	Visitor();
};

class Element
{
public:
	virtual ~Element();
	virtual void Accept(Visitor&) = 0;
protected:
	Element();
};

class ElementA : public Element
{
public:
	ElementA();
	virtual void Accept(Visitor& v)
	{
		v.VisitElementA(this);
	}
};

class ElementB : public Element
{
public:
	ElementB();
	virtual void Accept(Visitor& v)
	{
		v.VisitElementB(this);
	}
};

Composite 클래스는 반복자 순회를 이용하여 각 노드를 방문하게 된다.

template<typename Item>
class List;

class CompositeElement : public Element
{
public:
	virtual void Accept(Visitor&);
private:
	List<Element*>* _children;
};

void CompositeElement::Accept(Visitor& v)
{
	ListIterator<Element*> i(_children);
	for (i.First(); !i.IsDone(); i.Next())
	{
		i.CurrentItem()->Accept(v);
	}
	v.VisitCompositeElement(this);
}

 

예제 코드

Equipment(장비) 클래스와 이를 방문하는 다양한 방문자 객체를 정의해본다.

 

Equipment.h

방문자가 이용할 수 있는 인터페이스와 방문자 패턴에서 수락하는 Accept() 연산을 선언한다.

class Watt;
class Currency;
class EquipmentVisitor;
class Equipment
{
public:
	virtual ~Equipment();

	// 방문자가 이용할 수 있는 인터페이스
	const char* Name() { return _name; }
	virtual Watt Power();
	virtual Currency NetPrice();
	virtual Currency DiscountPrice();

	// 방문자가 방문하도록 하는 연산
	virtual void Accept(EquipmentVisitor&);

protected:
	Equipment(const char*);
private:
	const char* _name;
};

 

EquipmentVisitor.h

장비 클래스를 방문하여 수행할 수 있는 모든 연산을 정의한다.

class EquipmentVisitor
{
public:
	virtual ~EquipmentVisitor();

	virtual void VisitFloppyDisk(class FloppyDisk*);
	virtual void VisitCard(class Card*);
	virtual void VisitChassis(class Chassis*);
	virtual void VisitBus(class Bus*);
protected:
	EquipmentVisitor();
};

장비 클래스의 구체 클래스 한 예로 FloppyDisk는 Accept함수에서 다음과 같이 나타난다.

void FloppyDisk::Accept(EquipmentVisitor& visitor)
{
	visitor.VisitFlopyyDisk(this);
}

구체 클래스가 Composite 복합체 라면 다음과 같이 순회 연산을 사용할 수 도 있다.

void Chassis::Accept(EquipmentVisitor visitor)
{
	ListIterator<Equipment*> i(_children);
	for (i.First(); !i.IsDone(); i.Next())
	{
		i.CurrentItem()->Accept(visitor);
	}
	visitor.VisitChassis(this);
}

 

PricingVisitor.h

모든 장비의 가격을 계산하는 방문자

#include "EquipmentVisitor.h"
class PricingVisitor : public EquipmentVisitor
{
public:
	PricingVisitor();

	// 전체 가격을 계산하는 방문자
	class Currency& GetTotalPrice();

	virtual void VisitFloppyDisk(FloppyDisk*);
	virtual void VisitCard(Card*);
	virtual void VisitChassis(Chassis*);
	virtual void VisitBus(Bus*);
private:
	Currency _total;
};

 

InventoryVisitor.h

모든 장비의 재고량을 조사하는 방문자

#include "EquipmentVisitor.h"
class InventoryVisitor :
    public EquipmentVisitor
{
public:
	InventoryVisitor();

	// 전체 재고량을 조사하는 방문자
	class Inventory& GetInventory();

	virtual void VisitFloppyDisk(FloppyDisk*);
	virtual void VisitCard(Card*);
	virtual void VisitChassis(Chassis*);
	virtual void VisitBus(Bus*);
private:
	Inventory _inventory;
};