[디자인 패턴 15편] 행동 패턴, 방문자 (Visitor)
1. 개념
비지터 패턴은 방문자와 방문 공간을 분리하여,
방문 공간이 방문자를 맞이할 때, 이후에 대한 행동을 방문자에게 위임하는 패턴이다.
간단하게 설명하기 참 어려운 패턴이다.
보통 OOP에서, 객체는 그 객체가 하는 행동을 메쏘드로 가지고 있다.
그리고 행동의 대상이 되는 객체가 있을 경우, 메쏘드의 파라미터로 입력받는다.
그런데, 비지터 패턴은 행동의 대상이 되는 객체가 행동을 일으키는 객체를 입력으로 받는다.
설명이 좀 어려운데, 간단히 예로 설명하면 다음과 같다.
"나는 상점에 방문한다. 나는 ~를 한다."
'나'라는 객체가 '상점'이라는 객체를 입력받은 후, 이 상점에 대해서 뭔가를 한다.
이게 일반적인 OOP 추상화다.
반면 비지터 패턴은 다음과 같다.
"상점에 내가 방문을 했다. 내가 ~를 하게 한다."
'상점' 이라는 객체가 '나'라는 객체를 입력받은 후, '나' 라는 객체의 행동을 호출하는 것이다.
이 때, 상점에 대한 정보를 파라미터로 넘겨준다.
즉, 사용자는 방문자의 입장이 아니라, 방문 공간의 입장에서 먼저 생각해보게 된다.
1.1. 구조
- Visitor
- 방문자 클래스의 인터페이스
visit(Element)
을 공용 인터페이스로 쓴다.Element
는 방문 공간이다.
- Element
- 방문 공간 클래스의 인터페이스
accept(Vistor)
를 공용 인터페이스로 쓴다.Visitor
는 방문자다.- 내부적으로
Vistor.visit(this)
를 호출한다.
- 내부적으로
- ConcreteVisitor
- Visitor 를 구체적으로 구현한 클래스
- ConcreteElement
- Element 를 구체적으로 구현한 클래스
1.2. 장점
- 작업 대상(방문 공간) 과 작업 항목(방문 공간을 가지고 하는 일)을 분리시킨다.
- 작업 대상(방문 공간) 은 단지 데이터를 담고있는 자료구조로 만들고,
- 작업 주체(방문자) 는
visit()
안에 이 작업 대상을 입력받아 작업 항목을 처리하면 된다. - 즉, 데이터와 알고리즘이 분리되어, 데이터의 독립성을 높여준다.
- 작업 대상의 입장에서는
accept()
로 인터페이스를 통일시켜, 사용자에게 동일한 인터페이스를 제공한다.
1.3. 단점
- 새로운 작업 대상(방문 공간)이 추가될 때마다 작업 주체(방문자) 도 이에 대한 로직을 추가해야 한다.
- 두 객체 (방문자와 방문 공간)의 결합도가 높아진다.
- 서로
visit()
과accept()
에 의존하므로.
- 서로
1.4. 활용 상황
- 자료 구조(데이터)와 자료 구조를 처리하는 로직(알고리즘)을 분리해야할 경우
- 데이터 구조보다 알고리즘이 더 자주 바뀌는 경우
- 즉, 방문공간은 어느정도 정해져있고 방문자가 더 자주 바뀌는 경우
2. 구현
클라이언트는 다음과 같이 사용할 수 있다.
concrete_visitor_1 = ConcreteVisitor1()
for concrete_element in [ConcreteElementA(), ConcreteElementB()]:
concrete_element.accept(concrete_visitor_1)
방문자 개념인 Visitor의 추상클래스와 구체적인 클래스는 다음과 같다.
class Visitor(metaclass=abc.ABCMeta):
@abc.abstractmethod
def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
# concrete_element_a 를 방문했을 때, 처리할 로직
# 하위 클래스에서 구현한다.
@abc.abstractmethod
def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
# concrete_element_b 를 방문했을 때, 처리할 로직
# 하위 클래스에서 구현한다.
class ConcreteVisitor1(Visitor):
def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
pass
def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
pass
class ConcreteVisitor2(Visitor):
def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
pass
def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
pass
방문 공간의 개념인 Component의 추상클래스와 구체적인 클래스는 다음과 같다.
class Element(metaclass=abc.ABCMeta):
@abc.abstractmethod
def accept(self, visitor: Visitor):
pass
class ConcreteElementA(Element):
def accept(self, visitor: Visitor):
visitor.visit_concrete_element_a(self)
class ConcreteElementB(Element):
def accept(self, visitor: Visitor):
visitor.visit_concrete_element_b(self)
3. 정리
구현만 보면 별로 어렵지는 않은데, 정확히 어떤 경우에 사용하는지가 좀 의문이긴 하다.
결합도가 높아진다는 측면에서, 그렇게 매우 좋아보이지는 않는데...
찾아보고 찾아봐도, 뭔가 뚜렷하게 사용해야겠다는 느낌이 잘 안온다.
첫 번째 참고링크를 보면, 처음에 다음과 같은 말이 등장한다.
객체에 대한 행위의 내용을 외부 클래스로 빼서 객체의 행위를 위임하기도 한다. 이런 타입의 패턴으로 전략패턴, 커맨드 패턴, 비지터 패턴등이 있다. 셋 모두 객체의 행위를 바깥으로 위임하는 것이지만, 전략패턴이 하나의 객체에 대해 여러 동작을 하게 하거나(1:N), 커맨드 패턴이 하나의 객체에 대하 하나의 동작(+보조동작)에 대한 설계방식(1:1)인 반면에, 방문자 패턴은 여러 객체들에 대해 객체의 동작들을 지정하는 방식(N:N) 이다.
저 3가지를 모두 동시에 봐야 제대로 이해가 될 듯한데... 추후 다시 정리하면서 비교해봐야 겠다.