비지터 패턴은 방문자(visitor)와 방문 공간(알고리즘이 작동하는 객체 또는 자료구조)을 분리하는 패턴이다. 전형적인 비지터 패턴의 모습은 아래 위키 페이지 UML에서 확인할 수 있고, 대부분의 패턴 책에서 소개되는 형태도 크게 다르지 않다.
이 UML 클래스 다이어그램을 그대로 옮기면 아래처럼 되는데,
(public:
을 매번 적기 귀찮아서 class
대신 struct
키워드를 사용함)
struct ElementA;
struct ElementB;
struct Visitor {
virtual void visit(ElementA* element) = 0;
virtual void visit(ElementB* element) = 0;
};
struct Element {
virtual void accept(Visitor* visitor) = 0;
};
struct ElementA : public Element {
void accept(Visitor* visitor) override { visitor->visitElementB(this); }
};
struct ElementB : public Element {
void accept(Visitor* visitor) override { visitor->visitElementA(this); }
};
struct Visitor1 : public Visitor {
void visitElementA(ElementA* element) override { ... }
void visitElementB(ElementB* element) override { ... }
};
요 패턴의 문제점은 Element의 구현을 알아야 Visitor의 인터페이스를 만들 수 있다는 거다. ElementB
, ElementA
를 알아야 Visiter::visitElementA()
, Visitor::visitElementB()
를 만들 수 있다는 의미.
그러다보니 실제로 사용하게 되면 (OCP - Open/Closed Principle - 를 가능하게 해 주는 거라고 위키피디어에 설명되어 있음에도) Visitor2
, Visitor3
형태로만 확장할 수 있을 뿐 ElementC
, ElementD
형태로의 구현객체 확장은 불가능하다.
그래서 난 보통 Visitor
인터페이스를 아래처럼 임의의 Element
파생 클래스만을 대상으로 단순화해서 정의한다.
I
를 사용IElement
는 name()
메쏘드를 갖는 걸로...class IElement;
struct IVisitor {
virtual void visit(IElement* element) = 0;
};
struct IElement {
virtual void accept(IVisitor* visitor) = 0;
virtual const std::string& name() const = 0;
};
Element 들로 nested-list 형태의 tree를 만들 수 있는 ElementList란 놈을 구현하면 대략 아래처럼 적을 수 있겠다.
struct Element : public IElement {
Element(const std::string& name) : name_(name) {}
void accept(IVisitor* visitor) override { visitor->visit(this); }
const std::string& name() const override { return name_; }
private:
std::string name_;
};
struct ElementList : public IElement {
ElementList(const std::string& name) : name_(name) {}
void accept(IVisitor* visitor) override {
visitor->visit(this);
for (auto element : elements_) element->accept(visitor);
}
void add(IElement* element) { elements_.push_back(element); }
const std::string& name() const override { return name_; }
private:
std::string name_;
std::list<IElement*> elements_;
};
PrintingVisitor
란 걸 만들어서 트리를 순회하면서 이름을 출력하는 예제를 만들면 대략 아래와 같다.
struct PrintingVisitor : public IVisitor {
void visit(IElement* element) override {
printf("%s\n", element->name().c_str());
}
};
int main() {
auto root = new ElementList("root");
auto elements1 = new ElementList("element list 1");
auto elements2 = new ElementList("element list 2");
elements1->add(new Element("element 1-1"));
elements1->add(new Element("element 1-2"));
elements2->add(new Element("element 2-1"));
elements2->add(new Element("element 2-2"));
root->add(elements1);
root->add(elements2);
root->add(new Element("element 3"));
PrintingVisitor printer;
root->accept(&printer);
return 0;
}
online example
이 방법은 IVisitor
의 메쏘드를 하나로 제한한 결과 VisitorN
, ElementX
양쪽으로 OCP를 가능하게끔 한다는 장점을 갖는다. 대신 구현 클래스별 분기를 visit()
함수 내부에서 책임져야 한다는 문제가 생기는데, Visitor::visitElementX()
를 호출하는 쪽에서의 고민이 Visitor::vist()
함수 내부로 옮겨진 것에 불과하므로 실전적으로는 큰 문제가 되지 않는다.
앞서 확장한 비지터 패턴은 함수형 버전으로 만들기에 대단히 용이하다. IVisitor
인터페이스를 visitor
람다로 대체한 것에 불과하기 때문이다.
(좀 더 자연스럽다는 생각에 IElement::accept()
메쏘드의 이름을 IElement::visit()
로 변경함)
struct IElement {
virtual const std::string& name() const = 0;
virtual void visit(std::function<void(IElement*)> visitor) = 0;
};
Element
와 ElementList
의 구현도 람다를 사용하는 것으로 대체된다.
struct Element : public IElement {
Element(const std::string& name) : name_(name) {}
void visit(std::function<void(IElement*)> visitor) override { visitor(this); }
const std::string& name() const override { return name_; }
private:
std::string name_;
};
struct ElementList : public IElement {
ElementList(const std::string& name) : name_(name) {}
void visit(std::function<void(IElement*)> visitor) override {
visitor(this);
for (auto element : elements_) element->visit(visitor);
}
void add(IElement* element) { elements_.push_back(element); }
const std::string& name() const override { return name_; }
private:
std::string name_;
std::list<IElement*> elements_;
};
함수형 비지터의 강력한 점은 비지터를 구현하는 시점에 드러난다.
int main() {
auto root = new ElementList("root");
auto elements1 = new ElementList("element list 1");
auto elements2 = new ElementList("element list 2");
elements1->add(new Element("element 1-1"));
elements1->add(new Element("element 1-2"));
elements2->add(new Element("element 2-1"));
elements2->add(new Element("element 2-2"));
root->add(elements1);
root->add(elements2);
root->add(new Element("element 3"));
root->visit([](auto&& element) {
printf("%s\n", element->name().c_str());
});
return 0;
}
차이를 한 눈에 비교할 수 있게끔 아래 클래스형 비지터를 version 2
아래로, 함수형 비지터를 version 3
아래로 옮겨 적었다.
// version 2. extended visitor
struct PrintingVisitor : public IVisitor {
void visit(IElement* element) override {
printf("%s\n", element->name().c_str());
}
};
...
PrintingVisitor printer;
root->accept(&printer);
// version 3. functional visitor
...
root->visit([](auto&& element) {
printf("%s\n", element->name().c_str());
});
online example
샘플 코드가 지나치게 불어나는 것을 피하기 위해 new
한 객체를 delete
하는 걸 생략했는데, shared_ptr
을 도입하는 방식으로 해결해 보겠다.
생성자를 private
으로 감추고 각 객체 별로 Element::create()
형태로 변형된 팩토리 함수를 추가했는데, shared_ptr
이 아닌 객체를 생성할 수 없도록 강제하는 기법이다.
class Element : public IElement, public std::enable_shared_from_this<Element> {
Element(const std::string& name) : name_(name) {}
public:
static auto create(const std::string& name) { return std::shared_ptr<Element>(new Element(name)); }
void visit(std::function<void(std::shared_ptr<IElement>)> visitor) override { visitor(shared_from_this()); }
const std::string& name() const override { return name_; }
private:
std::string name_;
};
class ElementList : public IElement, public std::enable_shared_from_this<ElementList> {
ElementList(const std::string& name) : name_(name) {}
public:
static auto create(const std::string& name) { return std::shared_ptr<ElementList>(new ElementList(name)); }
void visit(std::function<void(std::shared_ptr<IElement>)> visitor) override {
visitor(shared_from_this());
for (auto element : elements_) {
element->visit(visitor);
}
}
void add(std::shared_ptr<IElement> element) { elements_.push_back(element); }
const std::string& name() const override { return name_; }
private:
std::string name_;
std::list<std::shared_ptr<IElement>> elements_;
};
online example