의도

  • 객체 구조를 이루는 원소에 대해 수행할 연산을 표현함. 연산을 적용할 원소의 클래스를 변경하지 않고도 새로운 연산을 정의할 수 있게 함.

 

언제 쓰는가 ?

  • 다른 인터페이스를 가진 클래스가 객체 구조에 포함되어 있으며, 구체 클래스에 따라 달라진 연산을 이들 클래스의 객체에 대해 수행하고자 할 때
  • 각각 특징이 있고, 관련되지 않은 많은 연산이 한 객체 구조에 속해있는 객체들에 대해 수행될 필요가 있으며, 연산으로 클래스들을 더럽히고 싶지 않을 때. Visitor 클래스는 관련된 모든 연산을 하나의 클래스 안에다 정의해 놓음으로써 관련된 연산이 함께 있을 수 있게 해줌.
  • 객체 구조를 정의한 클래스는 거의 변하지 않지만, 전체 구조에 걸쳐 새로운 연산을 추가하고 싶을 때. 객체 구조를 변경하려면 모든 방문자에 대한 인터페이스를 재정의해야 하는데, 이 작업에 잠재된 비용이 클 수 있음. 객체 구조가 자주 벼경될 때는 해당 연산을 클래스에 정의하는 편이 더 나음

 

구조

GoF의 디자인 패턴 중 P.429

  • Visitor : 객체 구조 내에 있는 각 ConcreteElement 클래스를 위한 Visit 연산을 선언함. 연산의 이름과 인터페이스 형태는 Visit 요청을 방문자에게 보내는 클래스를 식별함. 이로써 방문자는 방문된 원소의 구체 클래스를 결정할 수 있음. 그러고 나서 방문자는 그 원소가 제공하는 인터페이스를 통해 원소에 직접 접근할 수 있음.
  • ConcreteVisitor : Visitor 클래스에 선언된 연산을 구현함. 각 연산은 구조 내에 있는 객체의 대응 클래스에 정의된 일부 알고리즘을 구현함. ConcreteVisitor 클래스는 알고리즘이 운영될 수 있는 상황 정보를 제공하며 자체 상태를 저장함. 이 상태는 객체 구조를 순회하는 도중 순회 결과를 누적할 때가 많음.
  • Element : 방문자를 인자로 받아들이는 Accept 연산을 정의함.
  • ConcreteElement : 인자로 방문 객체를 받아들이는 Accept 연산을 구현함.
  • ObjectStructure : 객체 구조 내의 원소들을 나열할 수 있음. 방문자가 이 원소에 접근하게 하는 상위 수준 인터페이스를 제공함. ObjectStructre는 Composite 패턴으로 만든 복합체일 수도 있고, 리스트나 집합 등 컬렉션일 수도 있음.

 

협력 방법

  • 방문자 패턴을 사용하는 사용자는 ConcreteVisitor 클래스의 객체를 생성하고 객체 구조를 따라서 각 원소를 방문하며 순회해야 함
  • 구성 원소들을 방문할 때, 구성 원소는 해당 클래스의 Visitor 연산을 호출함. 이 원소들은 자신을 Visitor 연산에 필요한 인자로 제공하여 방문자 자신의 상태에 접근할 수 있도록 함.

 

결과

  1. Visitor 클래스는 새로운 연산을 쉽게 추가함
  2. 방문자를 통해 관련된 연산들을 한 군데로 모으고 관련되지 않은 연산을 떼어낼 수 있음
  3. 새로운 ConcreteElement 클래스를 추가하기가 어려움
    1. 새로운 ConcreteElement가 자주 추가되는 상황에서는 유지하기가 상당히 까다로움. 객체 구조를 구성하는 클래스에 대해 적용할 연산을 정의하는 편이 아마 더 쉬울 것.
  4. 클래스 계층 구조에 걸쳐서 방문함
    1. Iterator는 Item이라는 특정 클래스로만 순회할 수 있지만 방문자는 특정 타입에 대한 인터페이스만 새로 만든다면 타입에 상관없이 방문할 수 있음
  5. 상태를 누적할 수 있음
    1. 객체 구조 내 각 원소들을 방문하면서 상태를 누적할 수 있음. 이 상태는 별도의 다른 인자로서 순회를 담당하는 연산에 전달되든지, 아니면 전역변수로 존재해야 할 것.
  6. 데이터 은닉을 깰 수 있음
    1. 방문자 패턴은 ConcreteElement 인터페이스가 방문자에게 필요한 작업을 수행시킬 만큼 충분히 강력하다는 가정을 깔고 있음
    2. 결국, 이 패턴을 쓰면 원소의 내부 상태에 접근하는 데 필요한 연산들을 모두 공개 인터페이스로 만들 수 밖에 없으며, 이는 캡슐화 전략을 위배하는 것.

 

구현 / 고려 사항

  1. 이중 디스패치
    1. 실질적으로 방문자 패턴은 사용자가 클래스를 변경하지 않고 연산을 클래스에 추가하도록 만드는 패턴(이중 디스패치라는 아주 잘 알려진 기법을 사용함)
    2. C++는 단일 디스패치이며 요청의 이름과 수신자의 타입이 연산을 결정함.
    3. "이중 디스패치"란 실행되는 연산이 요청의 종류와 두 수신자의 타입에 따라 달라진다는 뜻. 실제로 실행되는 연산은 Visitor의 타입과 그것이 방문하는 Element의 타입에 따라 달라진다는 점. (키포인트)
  2. 누가 객체 구조를 순회할 책임을 지는가 ?
    1. 방문자는 각 객체 구조 요소를 방문해야 함. 1) 객체 구조 2) 방문자 3) 반복자 객체 를 통해 수행할 수 있음

 

예제 코드

  • 특정 물건들(Equipment를 공통으로 상속 받은)에 대해 이름과 가격을 부여하고 각 물건에게 어떤 Visitor를 Accept 하는지에 따라 물건들의 이름을 모아둘 수 있거나 물건들의 가격을 누적하는 Visitor를 만들었습니다.
#include "Visitor1.h"
#include <iostream>

int main()
{
	DerivedEquipment1 derived_equipment1("equip1", 10);
	DerivedEquipment2 derived_equipment2("equip2", 20);
	DerivedEquipment3 derived_equipment3("equip3", 30);

	PricingVisitor pricing_visitor;
	NameCollectVisitor namecollect_visitor;

	derived_equipment1.Accept(pricing_visitor);
	derived_equipment2.Accept(pricing_visitor);
	derived_equipment3.Accept(pricing_visitor);

	derived_equipment1.Accept(namecollect_visitor);
	derived_equipment2.Accept(namecollect_visitor);
	derived_equipment3.Accept(namecollect_visitor);

	namecollect_visitor.ShowNames();
	std::cout << "모든 물건의 총합은 " << pricing_visitor.GetTotalPrice() << "입니다. " << std::endl;

	return 0;
}

  • Equipment를 상속받은 DerivedEquipment가 1,2,3이 있습니다. 각각 물건마다 이름과 가격을 생성자로 받습니다.
  • 물건마다 visitor가 방문하여 특정 처리를 할 수 있게 되는데 Priciing Visitor의 경우 물건의 값을 누적할 수 있는 visitor이고 NameCollectVisitor의 경우 방문한 물건의 이름을 담아둘 수 있습니다. 이 때 물건에 방문하기 위해서 Equipment에 Accecpt 메서드를 만들어뒀고 Visitor의 서브클래스라면 모두 인자로 넘길 수 있습니다.
//Visitor1.h

#pragma once
#include <string>
#include <vector>

class EquipmentVisitor;

class Equipment
{
public:
	Equipment(std::string name, size_t price) 
		: name(name), price(price) {}

	std::string GetName() { return name; }
	size_t GetPrice() { return price; }

	virtual void Accept(EquipmentVisitor& visitor) = 0;

private:
	std::string name;
	size_t price;
};

class DerivedEquipment1 : public Equipment
{
public:
	DerivedEquipment1(std::string name, size_t price) 
		: Equipment(name, price) {}

	virtual void Accept(EquipmentVisitor& visitor);
};

class DerivedEquipment2 : public Equipment
{
public:
	DerivedEquipment2(std::string name, size_t price)
		: Equipment(name, price) {}

	virtual void Accept(EquipmentVisitor& visitor);
};

class DerivedEquipment3 : public Equipment
{
public:
	DerivedEquipment3(std::string name, size_t price)
		: Equipment(name, price) {}

	virtual void Accept(EquipmentVisitor& visitor);
};

class EquipmentVisitor
{
public:
	virtual void VisitDerivedEquipment1(DerivedEquipment1* derived_equipment1) {}
	virtual void VisitDerivedEquipment2(DerivedEquipment2* derived_equipment2) {}
	virtual void VisitDerivedEquipment3(DerivedEquipment3* derived_equipment3) {}
};

class NameCollectVisitor : public EquipmentVisitor
{
public:
	void ShowNames();

	virtual void VisitDerivedEquipment1(DerivedEquipment1* derived_equipment1) override;
	virtual void VisitDerivedEquipment2(DerivedEquipment2* derived_equipment2) override;
	virtual void VisitDerivedEquipment3(DerivedEquipment3* derived_equipment3) override;

private:
	std::vector<std::string> names;
};

class PricingVisitor : public EquipmentVisitor
{
public:
	size_t GetTotalPrice() { return total_price; }

	virtual void VisitDerivedEquipment1(DerivedEquipment1* derived_equipment1) override;
	virtual void VisitDerivedEquipment2(DerivedEquipment2* derived_equipment2) override;
	virtual void VisitDerivedEquipment3(DerivedEquipment3* derived_equipment3) override;

private:
	size_t total_price = 0;
};
  • Equipment의 경우 Accept를 오버라이드하여 Visitor에 정의해 둔 인터페이스 중 자신에게 맞는 인터페이스를 호출할 수 있도록 오버라이드 합니다.
  • Visitor의 경우 용도에 맞게 끔 서브클래싱하여 각 서브클래스마다 실제 어떤 처리를 할지 인터페이스를 재정의합니다.
  • NameCollectVisitor의 경우 name들을 vector 컨테이너에 담아둘 것이고 PricingVisitor의 경우 물건의 price들을 PricingVisitor의total_price에 누적할 것입니다.
//Visitor1.cpp

#include "Visitor1.h"
#include <iostream>

void DerivedEquipment1::Accept(EquipmentVisitor& visitor)
{
	visitor.VisitDerivedEquipment1(this);
}

void DerivedEquipment2::Accept(EquipmentVisitor& visitor)
{
	visitor.VisitDerivedEquipment2(this);
}

void DerivedEquipment3::Accept(EquipmentVisitor& visitor)
{
	visitor.VisitDerivedEquipment3(this);
}

void NameCollectVisitor::ShowNames()
{
	for (std::string name : names)
	{
		std::cout << "name : " << name << std::endl;
	}
}

void NameCollectVisitor::VisitDerivedEquipment1(DerivedEquipment1* derived_equipment1)
{
	names.push_back(derived_equipment1->GetName());
}

void NameCollectVisitor::VisitDerivedEquipment2(DerivedEquipment2* derived_equipment2)
{
	names.push_back(derived_equipment2->GetName());
}

void NameCollectVisitor::VisitDerivedEquipment3(DerivedEquipment3* derived_equipment3)
{
	names.push_back(derived_equipment3->GetName());
}

void PricingVisitor::VisitDerivedEquipment1(DerivedEquipment1* derived_equipment1)
{
	total_price += derived_equipment1->GetPrice();
}

void PricingVisitor::VisitDerivedEquipment2(DerivedEquipment2* derived_equipment2)
{
	total_price += derived_equipment2->GetPrice();
}

void PricingVisitor::VisitDerivedEquipment3(DerivedEquipment3* derived_equipment3)
{
	total_price += derived_equipment3->GetPrice();
}
  • 거의 동일한 코드를 반복 작업한 결과입니다. 위 설명대로 Equipment는 Accept를 오버라이드 하는데 이 때 자신에 맞는 Visitor의 인터페이스를 호출하는 것 뿐이고 Visitor는 각 서브클래스마다 실제 처리할 동작을 정의합니다.

'디자인 패턴 > 행위' 카테고리의 다른 글

템플릿 메서드(Template Method)  (2) 2024.01.15
전략(Strategy)  (0) 2024.01.15
상태(State)  (0) 2024.01.12
감시자(Observer)  (0) 2024.01.12
메멘토(Memento)  (1) 2024.01.11

+ Recent posts