의도
- 객체 구조를 이루는 원소에 대해 수행할 연산을 표현함. 연산을 적용할 원소의 클래스를 변경하지 않고도 새로운 연산을 정의할 수 있게 함.
언제 쓰는가 ?
- 다른 인터페이스를 가진 클래스가 객체 구조에 포함되어 있으며, 구체 클래스에 따라 달라진 연산을 이들 클래스의 객체에 대해 수행하고자 할 때
- 각각 특징이 있고, 관련되지 않은 많은 연산이 한 객체 구조에 속해있는 객체들에 대해 수행될 필요가 있으며, 연산으로 클래스들을 더럽히고 싶지 않을 때. Visitor 클래스는 관련된 모든 연산을 하나의 클래스 안에다 정의해 놓음으로써 관련된 연산이 함께 있을 수 있게 해줌.
- 객체 구조를 정의한 클래스는 거의 변하지 않지만, 전체 구조에 걸쳐 새로운 연산을 추가하고 싶을 때. 객체 구조를 변경하려면 모든 방문자에 대한 인터페이스를 재정의해야 하는데, 이 작업에 잠재된 비용이 클 수 있음. 객체 구조가 자주 벼경될 때는 해당 연산을 클래스에 정의하는 편이 더 나음
구조
- Visitor : 객체 구조 내에 있는 각 ConcreteElement 클래스를 위한 Visit 연산을 선언함. 연산의 이름과 인터페이스 형태는 Visit 요청을 방문자에게 보내는 클래스를 식별함. 이로써 방문자는 방문된 원소의 구체 클래스를 결정할 수 있음. 그러고 나서 방문자는 그 원소가 제공하는 인터페이스를 통해 원소에 직접 접근할 수 있음.
- ConcreteVisitor : Visitor 클래스에 선언된 연산을 구현함. 각 연산은 구조 내에 있는 객체의 대응 클래스에 정의된 일부 알고리즘을 구현함. ConcreteVisitor 클래스는 알고리즘이 운영될 수 있는 상황 정보를 제공하며 자체 상태를 저장함. 이 상태는 객체 구조를 순회하는 도중 순회 결과를 누적할 때가 많음.
- Element : 방문자를 인자로 받아들이는 Accept 연산을 정의함.
- ConcreteElement : 인자로 방문 객체를 받아들이는 Accept 연산을 구현함.
- ObjectStructure : 객체 구조 내의 원소들을 나열할 수 있음. 방문자가 이 원소에 접근하게 하는 상위 수준 인터페이스를 제공함. ObjectStructre는 Composite 패턴으로 만든 복합체일 수도 있고, 리스트나 집합 등 컬렉션일 수도 있음.
협력 방법
- 방문자 패턴을 사용하는 사용자는 ConcreteVisitor 클래스의 객체를 생성하고 객체 구조를 따라서 각 원소를 방문하며 순회해야 함
- 구성 원소들을 방문할 때, 구성 원소는 해당 클래스의 Visitor 연산을 호출함. 이 원소들은 자신을 Visitor 연산에 필요한 인자로 제공하여 방문자 자신의 상태에 접근할 수 있도록 함.
결과
- Visitor 클래스는 새로운 연산을 쉽게 추가함
- 방문자를 통해 관련된 연산들을 한 군데로 모으고 관련되지 않은 연산을 떼어낼 수 있음
- 새로운 ConcreteElement 클래스를 추가하기가 어려움
- 새로운 ConcreteElement가 자주 추가되는 상황에서는 유지하기가 상당히 까다로움. 객체 구조를 구성하는 클래스에 대해 적용할 연산을 정의하는 편이 아마 더 쉬울 것.
- 클래스 계층 구조에 걸쳐서 방문함
- Iterator는 Item이라는 특정 클래스로만 순회할 수 있지만 방문자는 특정 타입에 대한 인터페이스만 새로 만든다면 타입에 상관없이 방문할 수 있음
- 상태를 누적할 수 있음
- 객체 구조 내 각 원소들을 방문하면서 상태를 누적할 수 있음. 이 상태는 별도의 다른 인자로서 순회를 담당하는 연산에 전달되든지, 아니면 전역변수로 존재해야 할 것.
- 데이터 은닉을 깰 수 있음
- 방문자 패턴은 ConcreteElement 인터페이스가 방문자에게 필요한 작업을 수행시킬 만큼 충분히 강력하다는 가정을 깔고 있음
- 결국, 이 패턴을 쓰면 원소의 내부 상태에 접근하는 데 필요한 연산들을 모두 공개 인터페이스로 만들 수 밖에 없으며, 이는 캡슐화 전략을 위배하는 것.
구현 / 고려 사항
- 이중 디스패치
- 실질적으로 방문자 패턴은 사용자가 클래스를 변경하지 않고 연산을 클래스에 추가하도록 만드는 패턴(이중 디스패치라는 아주 잘 알려진 기법을 사용함)
- C++는 단일 디스패치이며 요청의 이름과 수신자의 타입이 연산을 결정함.
- "이중 디스패치"란 실행되는 연산이 요청의 종류와 두 수신자의 타입에 따라 달라진다는 뜻. 실제로 실행되는 연산은 Visitor의 타입과 그것이 방문하는 Element의 타입에 따라 달라진다는 점. (키포인트)
- 누가 객체 구조를 순회할 책임을 지는가 ?
- 방문자는 각 객체 구조 요소를 방문해야 함. 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 |