알고리즘을 해당 알고리즘이 작동하는 객체로부터 분리하는 행동 패턴
거대한 그래프로 구성된 지리적 정보를 나타내는 앱 가정
그래프를 XML 포맷으로 export하는 기능을 구현해야 하는 상황
비지터 패턴 - 새로운 행동을 기존 클래스에 결합하지 않고 비지터라는 별도의 클래스에 위치시킴
만약 행동이 서로 다른 클래스들의 객체들에게 실행되어야 한다면 비지터 클래스는 여러 종류의 인수를 받을 수 있는 메서드들의 집합을 정의
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ...
서로 다른 시그니처를 가진 메서드들 → 다형성 불가
노드 객체의 정확한 클래스를 미리 알 수 없어 메서드 오버로딩 불가
비지터 패턴 → 더블 디스패치로 문제 해결
// Client code
foreach (Node node in graph)
node.accept(exportVisitor)
// City
class City is
method accept(Visitor v) is
v.doForCity(this)
// ...
// Industry
class Industry is
method accept(Visitor v) is
v.doForIndustry(this)
// ...
노드 클래스를 변경하긴 해야 하지만 사소한 변경이고, 추후에는 또 다른 코드 변경 없이 행동 추가 가능
모든 비지터로부터 공통 인터페이스를 추출하면, 기존 노드들은 앱에 새로 도입하는 어떤 비지터와도 협업 가능
객체들은 자신의 클래스를 알기 때문에 비지터를 “수락”하고 어떤 방문 메서드가 실행되어야 하는지 알려줌
e.g. 보험 판매원 → 모든 건물들을 찾아다니며 상황에 맞는 보험 추천
1. 비지터 - 객체 구조의 concrete 요소들을 받는 방문 메서드 집합을 선언하는 인터페이스
- 메서드들의 인자 타입은 모두 달라야 함
2. concrete 비지터 - 같은 행동의 여러 버전들을 서로 다른 concrete 요소 클래스들에 맞게 구현
3. 요소 - 비지터를 “수락”하는 메서드 선언
- 메서드는 비지터 인터페이스 타입으로 선언된 하나의 인자를 가져야 함
4. concrete 요소 - 수락 메서드를 구현해야 함
- 메서드의 목적은 호출을 현재 요소 클래스에 맞는 비지터 메서드로 redirect해주는 것
- 기초 요소 클래스가 해당 메서드를 구현하더라도, 모든 서브클래스들은 메서드를 override해야 함
5. 클라이언트 - 대개 컬렉션 또는 복잡한 객체(e.g. 컴포지트 트리)를 표현함
- 보통 클라이언트는 몇몇 추상 인터페이스를 통해 컬렉션의 객체들과 협업하기 때문에
모든 concrete 요소 클래스들을 다 인지하지는 못함
- 비지터 패턴 - 한 작업을 각 타겟 클래스에 대응되는 여러 변형으로 구현하는 비지터 객체를 통해
서로 다른 클래스들을 가진 객체들의 집합에 작업 실행
- 다른 작업들을 방문자 클래스들의 집합으로 추출해 주 클래스들이 주요 작업에 더 집중할 수 있게 해줌
- 해당 행동을 별도 비지터 클래스로 추출하고,
연관된 클래스들의 객체를 받는 방문 메서드에만 구현하고 나머지는 빈 상태로 두면 됨
1. 프로그램에 존재하는 각 concrete 요소 클래스들마다 방문 메서드들의 집합을 가지는
비지터 인터페이스 선언
2. 요소 인터페이스 선언
- 이미 존재하는 클래스 계층 구조를 다루는 경우,
추상 “수락” 메서드를 계층 구조의 기초 클래스에 추가
- 해당 메서드는 비지터 객체를 인수로 받아야 함
3. 모든 concrete 요소 클래스들 내에 수락 메서드 구현
- 해당 메서드들은 호출을 단순히 현재 요소의 클래스에 맞는 방문 메서드로 redirect하기만 해야 함
4. 요소 클래스들은 비지터 인터페이스를 통해서만 비지터와 협업해야 함
- 그러나 비지터는 방문 메서드의 인자 타입으로 참조되고 있는 모든 concrete 요소 클래스들을
인지하고 있어야 함
5. 요소 계층 구조 내부에서 구현될 수 없는 각각의 행동들에 대해
새 concrete 방문자 클래스를 생성하고 모든 방문 메서드 구현
- 비지터가 요소 클래스의 private 멤버들에 접근해야 하는 경우:
- 필드/메서드들을 public으로 만들기
- 요소의 캡슐화 위반하기
- 방문자 클래스를 요소 클래스 안에 중첩시키기
6. 클라이언트는 방문자 객체들을 생성하고 해당 객체들을 “수락” 메서드를 통해 요소들에 전달해야 함
- OCP - 서로 다른 클래스들의 객체와 작동하는 새로운 행동들을 해당 클래스들의 변경 없이 추가 가능
- SRP - 같은 행동의 여러 버전들을 같은 클래스로 옮길 수 있음
- 비지터 객체들은 다양한 객체들과 작동하며 유용한 정보를 축적할 수 있음
→ 객체 트리와 같은 복잡한 객체 구조를 순회하며 해당 구조의 각 객체들에게 비지터를 적용할 때 편리
- 요소 계층 구조에서 클래스가 추가되거나 제거될 때마다 모든 비지터들을 업데이트해야 함
- 비지터들은 필요한 private 필드나 메서드들에 대한 접근 권한이 없을 수 있음
- 비지터 패턴 - 커맨드 패턴의 강력한 버전,
비지터 패턴 객체들은 작업들을 다양한 클래스들의 다양한 객체들에게 실행할 수 있음
- 비지터 패턴을 사용해 컴포지트 트리 전체에 작업 실행 가능
- 비지터를 이터레이터와 함께 사용해 복잡한 자료 구조를 순회하며
요소들이 모두 다른 클래스를 가지더라도 요소들에게 특정 작업을 실행할 수 있음
/**
* The Component interface declares an `accept` method that should take the base
* visitor interface as an argument.
*/
interface Component {
accept(visitor: Visitor): void;
}
/**
* Each Concrete Component must implement the `accept` method in such a way that
* it calls the visitor's method corresponding to the component's class.
*/
class ConcreteComponentA implements Component {
/**
* Note that we're calling `visitConcreteComponentA`, which matches the
* current class name. This way we let the visitor know the class of the
* component it works with.
*/
public accept(visitor: Visitor): void {
visitor.visitConcreteComponentA(this);
}
/**
* Concrete Components may have special methods that don't exist in their
* base class or interface. The Visitor is still able to use these methods
* since it's aware of the component's concrete class.
*/
public exclusiveMethodOfConcreteComponentA(): string {
return 'A';
}
}
class ConcreteComponentB implements Component {
/**
* Same here: visitConcreteComponentB => ConcreteComponentB
*/
public accept(visitor: Visitor): void {
visitor.visitConcreteComponentB(this);
}
public specialMethodOfConcreteComponentB(): string {
return 'B';
}
}
/**
* The Visitor Interface declares a set of visiting methods that correspond to
* component classes. The signature of a visiting method allows the visitor to
* identify the exact class of the component that it's dealing with.
*/
interface Visitor {
visitConcreteComponentA(element: ConcreteComponentA): void;
visitConcreteComponentB(element: ConcreteComponentB): void;
}
/**
* Concrete Visitors implement several versions of the same algorithm, which can
* work with all concrete component classes.
*
* You can experience the biggest benefit of the Visitor pattern when using it
* with a complex object structure, such as a Composite tree. In this case, it
* might be helpful to store some intermediate state of the algorithm while
* executing visitor's methods over various objects of the structure.
*/
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`);
}
}
/**
* The client code can run visitor operations over any set of elements without
* figuring out their concrete classes. The accept operation directs a call to
* the appropriate operation in the visitor object.
*/
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);
// Output.txt
The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2
참고 자료: Refactoring.guru