의도
- 비지터 => 알고리즘을 작동하는 객체로부터 분리할 수 있도록 하는 행동 디자인 패턴
문제
- 거대한 그래프로 구성된 지도를 사용한 앱을 개발하고 있다.
- 그래프의 각 노드는 도시와 같은 복잡한 객체를 나타낼 수 있고, 관광지와 같은 세부적인 항목 역시도 포함할 수 있다. 도로가 있으면 노드들은 서로 연결되며, 각 노드 유형은 자체적인 클래스를 가지고 각각의 노드는 객체이다.
- 어느날 그래프를 XML 형식으로 내보내고자 한다. 노드 클래스에 내보내기 메서드를 추가해 재귀를 사용해 각 그래프의 노드에 작업을 진행해주면 간단하게 해결할 수 있다.
- 그런데 제품의 발매일이 얼마 남지 않았고, 시스템의 노드 클래스를 건드리는 것은 치명적인 버그를 불러일으킬 수 있기 때문에 쉽게 작업을 진행할수가 없는 상황이다.
해결책
- 새로운 행동을 기존 클래스에 통합하는 대신 visitor라는 별도에 클래스에 배치
- 행동을 수행하는 객체는 visitor 메서드 중 하나에 인수로 전달되 원래 객체 내의 데이터에 접근할 수 있게 됨
- 객체가 여러개가 되어 메서드가 복잡해지는 경우에는 둘 중 하나의 방법을 사용
- 메서드 내부에 조건문을 사용해 객체를 검사 (메서드 오버로딩은 비지터가 정확한 클래스를 사전에 알 수 없기 때문에 불가능), 이 경우 코드가 복잡해질 수 있음
- 객체에 메서드를 하나 추가해 어떤 비지터 클래스를 사용할 것인지 결정하게 함, 이 경우 결국 코드를 수정해야 함
구조
interface Component {
accept(visitor: Visitor): void;
}
class ConcreteComponentA implements Component {
public accept(visitor: Visitor): void {
visitor.visitConcreteComponentA(this);
}
public exclusiveMethodOfConcreteComponentA(): string {
return 'A';
}
}
class ConcreteComponentB implements Component {
public accept(visitor: Visitor): void {
visitor.visitConcreteComponentB(this);
}
public specialMethodOfConcreteComponentB(): string {
return 'B';
}
}
interface Visitor {
visitConcreteComponentA(element: ConcreteComponentA): void;
visitConcreteComponentB(element: ConcreteComponentB): void;
}
class ConcreteVisitor1 implements Visitor {
public visitConcreteComponentA(element: ConcreteComponentA): void {
console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor1`);
}
public visitConcreteComponentB(element: ConcreteComponentB): void {
console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor1`);
}
}
class ConcreteVisitor2 implements Visitor {
public visitConcreteComponentA(element: ConcreteComponentA): void {
console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor2`);
}
public visitConcreteComponentB(element: ConcreteComponentB): void {
console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor2`);
}
}
function clientCode(components: Component[], visitor: Visitor) {
for (const component of components) {
component.accept(visitor);
}
}
const components = [
new ConcreteComponentA(),
new ConcreteComponentB(),
];
console.log('The client code works with all visitors via the base Visitor interface:');
const visitor1 = new ConcreteVisitor1();
clientCode(components, visitor1);
console.log('');
console.log('It allows the same client code to work with different types of visitors:');
const visitor2 = new ConcreteVisitor2();
clientCode(components, visitor2);
적용
- 복잡한 객체 구조의 모든 요소에 대해 작업을 수행해야 할 때 사용
비지터 패턴을 사용해 모든 대상 클래스에 같은 작업을 여러번 반복할 수 있도록 해줌
- 보조 행동들의 비지니스 로직을 정리하고자 할 때 사용
주 클래스에서 주된 작업을 제외한 모든 행동들을 비지터 클래스의 집합으로 추출하게 됨
- 특정 행동이 클래스 계층구조의 일부 클래스에게만 의미가 있을 때 사용
별도의 비지터 클래스로 행동을 추출하고 관련 클래스의 객체들을 수락하는 비지터 메서드들만 구현을 하면 됨
구현 방법
- 프로그램에 존재하는 각 구상 요소 클래스별로 하나씩 visitor 메서드를 만들고, 메서드들의 집합을 인터페이스로 선언
- 엘리먼트 인터페이스를 선언, 이 때 기존의 element class 계층구조와 적업하면 추상 클래스 레벨에 accept 메서드를 추가
- 모든 구상 엘리먼트 클래스에서 accept 메서드를 구현하고, 비지트 메서드에 대한 호출을 input으로 들어오는 비지터 객체에 리다이렉트를 시킴, 이 때 엘리먼트 클래스는 비지터와 비지터 인터페이스를 통해서만 작동하게 되며 비지터 클래스는 비지터 메서드에 참조된 모든 엘리먼트 클래스를 알고 있어야 함
- 각 객체에서 구현할수 없는 행동들은 새로운 구상 비지터 클래스를 만들고 모든 비지터 메서드를 구현함
- 클라이언트에서 비지터 객체들을 만들고 accept 메서드를 통해 비지터를 전달하게 됨
장단점
- 다른 클래스를 변경하지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 행동을 도입할 수 있기 때문에 개방, 폐쇄 원칙 준수
- 같은 행동의 여러 버전을 같은 클래스로 이동해 단일 책임 원칙 준수
- 비지터 객체는 다양한 객체들과 작업하면서 여러 정보를 축적할 수 있음
- 클래스가 계층구조에 추가되거나 제거될 때 모든 비지터를 업데이트 해야 함
- 비지터들은 함께 작업하는 엘리먼트들에 대한 접근권한이 부족할 수 있음