[Design Pattern] 비지터 패턴

olwooz·2023년 3월 9일
0

Design Pattern

목록 보기
22/22
알고리즘을 해당 알고리즘이 작동하는 객체로부터 분리하는 행동 패턴

문제

거대한 그래프로 구성된 지리적 정보를 나타내는 앱 가정

  • 각 노드는 도시와 같은 복잡한 개체도, 보다 작은 기업체, 관광지 등도 표현할 수 있음
  • 만약 실제 객체들 사이에 길이 존재한다면 노드 연결
  • 각 노드의 종류는 자체 클래스로 표현되고, 각 노드는 객체임

그래프를 XML 포맷으로 export하는 기능을 구현해야 하는 상황

  • 각 노드 클래스에 export 메서드를 추가해, 그래프의 각 노드를 재귀적으로 순회하며 export 메서드를 실행하려고 했음 → 다형성 덕분에 export 메서드를 호출하는 코드를 노드 concrete 클래스들에 결합하지 않았음
  • 하지만 시스템 설계자가 코드가 이미 배포되어 있어서 새로운 변화로 인해 생길 수 있는 버그 때문에 기존 노드 클래스들을 변경하는 것을 거부한 상황
  • 또한 노드 클래스들의 주요 역할은 지리 데이터를 다루는 것이었기 때문에 XML export 코드가 노드 클래스 내부에 있는 것에 대한 의문 제기
  • 해당 기능이 구현되면 마케팅 부서에서 다른 포맷으로도 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 필드나 메서드들에 대한 접근 권한이 없을 수 있음

다른 패턴과의 관계

- 비지터 패턴 - 커맨드 패턴의 강력한 버전, 
  비지터 패턴 객체들은 작업들을 다양한 클래스들의 다양한 객체들에게 실행할 수 있음

- 비지터 패턴을 사용해 컴포지트 트리 전체에 작업 실행 가능

- 비지터를 이터레이터와 함께 사용해 복잡한 자료 구조를 순회하며 
  요소들이 모두 다른 클래스를 가지더라도 요소들에게 특정 작업을 실행할 수 있음

TypeScript 예제

/**
 * 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

0개의 댓글