[Behavioral Patterns] - Visitor

Lee Jeong Min·2022년 1월 23일
1

디자인 패턴

목록 보기
23/23
post-thumbnail

의도

Visitor는 알고리즘을 작동하는 객체와 분리할 수 있는 동작 설계 패턴이다.

문제

팀에서 하나의 거대한 그래프로 구성된 지리 정보를 다루는 앱을 개발한다고 상상해보자. 그래프의 각 노드는 도시와 같은 복잡한 실체를 나타낼 수 있지만 산업, 관광 지역 등과 같은 보다 세분화된 것들을 나타낼 수도 있다.

그래프를 XML로 내보내는 중이다.

어느 시점에서 그래프를 XML 형식으로 내보내는 작업을 수행하였다. 각 노드 클래스에 내보내기 방법을 추가한 다음 재귀를 사용하여 그래프의 각 노드를 지나 내보내기 방법을 실행하려고 계획하였다. 다형성 덕분에 내보내기 메서드를 구체적인 노드 클래스로 호출하는 코드를 결합하지 못하였다.

유감스럽게, 시스템 설계자가 기존 노드 클래스를 변경하는 것을 거부하였다. 그 이유로 코드가 이미 제작 중이고, 변경사항의 잠재적인 버그 때문에 코드를 깨는 위험을 감수하고 싶지 않기 때문이었다.

XML 내보내기 방법은 모든 노드 클래스에 추가해야했기 때문에 변경 시 버그가 발생할 경우 전체 응용 프로그래밍 손상될 위험이 있었다.

게다가 이 시스템 설계자는 노드 클래스 내에 XML 내보내기 코드를 갖는 것이 타당한지에 대해 의문을 제기했다. 이 클래스의 주 업무는 지리 데이터와 연관된 것인데 XML 내보내기 동작이 있는 것은 이상해보였다.

해결책

방문자 패턴은 새 동작을 기존 클래스에 통합하는 대신 방문자라는 별도의 클래스를 만든다. 이를 통해 방문자의 메서드 중 하나에 원래 객체가 인수로 전달되어 객체 내부의 필요한 데이터에 대한 메서드 액세스를 제공한다.

만약 그 행동이 다른 클래스의 객체들에 대해 실행될 수 있다면 어떻게 될까? 예를 들어, XML 내보내기의 경우 실제 구현은 다양한 노드 클래스에 따라 다를 수 있다. 따라서 방문자 클래스는 하나가 아니라 메서드 집합을 정의할 수 있고, 각 메서드는 다음과 같이 서로 다른 유형의 인수를 사용할 수 있다.

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

하지만 전체 그래프를 다룰 때 이러한 메서드를 정확하게 부르려면 클래스를 확인해야한다.

foreach (Node node in graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node)
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node)
    // ...
}

왜 오버로딩을 쓰지 않느냐고 반문할 수 있지만 이 경우 모든 메서드가 서로 다른 매개변수 집합을 지원하더라도 동일한 이름을 지정한다. 그러나 이를 사용한다하더라도 노드 객체의 정확한 클래스를 미리 알 수 없기 때문에 오버로딩 메커니즘은 실행할 올바른 메서드를 결정할 수 없다.

그러나 방문자 패턴은 더블 디스패치라는 기술을 사용하여 적절한 메서드를 실행할 수 있다. 방문자에게 인수로 전달할 객체에 이 선택을 위임하면 객체가 자신의 클래스를 알기 때문에 정확한 메서드를 호출할 수 있다.

// 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)
    // ...

결국 노드 클래스를 변경해야 했지만 변경은 사소하고 코드를 다시 변경하지 않고 동작을 추가할 수 있다.

이제 모든 방문자를 위한 공통 인터페이스를 추출하면 기존의 모든 노드가 앱에 소개하는 모든 방문자와 함께 작동할 수 있다. 노드와 관련된 새로운 동작을 도입하는 상황에선 새로운 방문자 클래스를 구현하기만 하면 된다.

현실 유사성

훌륭한 보험 대리인은 항상 다양한 유형의 조직에 다양한 정책을 제공할 준비가 되어있다.

보험업자가 새로운 고객을 얻기 위해 건물에 따라 다음과 같은 전문적인 보험을 제공할 수 있다.

  • 주거용 건물이면 의료보험을 판매
  • 은행이면 도난 보험을 판매
  • 커피숍이라면 화재와 홍수 보험을 판매

구조

  1. 방문자 인터페이스는 객체 구조의 구체적인 요소를 인수로 받아들일 수 있는 방문 메서드 집합을 선언한다. 프로그램이 오버로딩을 지원하는 언어로 작성된 경우 이 메서드들은 동일한 이름을 가질 수 있지만 매개변수의 유형은 달라야 한다.

  2. 각 구체적인 방문자는 여러 가지 구체적인 요소 클래스에 맞춰 동일한 동작의 여러 버전을 구현한다.

  3. 요소 인터페이스는 방문자를 "접수"하기 위한 메서드를 선언합니다. 이 메서드에는 방문자 인터페이스의 형식으로 선언된 매개 변수가 하나 있어야 한다.

  4. 각 콘크리트 요소는 접수 메서드를 구현해야 합니다. 이 메서드의 목적은 호출을 현재 요소 클래스에 해당하는 적절한 방문자 메서드로 리다이렉션 것이다. 기본 요소 클래스가 이 메서드를 구현하더라도 모든 하위 클래스는 자체 클래스에서 이 메서드를 재정의하고 방문자 개체에서 적절한 메서드를 호출해야 한다.

  5. 클라이언트는 일반적으로 컬렉션이나 다른 복잡한 개체(예: Composite 트리)를 나타낸다. 일반적으로 클라이언트는 추상 인터페이스를 통해 해당 컬렉션의 객체로 작업하기 때문에 모든 구체적인 요소 클래스를 인식하지 못한다.

적용가능성

  • 복잡한 객체 구조의 모든 요소에 대해 작업을 수행해야 하는 경우 방문자를 사용한다.

  • 방문자를 사용하여 보조 행동의 비즈니스 논리를 정리한다.

  • 동작이 클래스 계층의 일부 클래스에서만 적용되고 다른 클래스에서는 적용되지 않는 경우 패턴을 사용하라.

장단점

장점

  • OCP
  • SRP
  • 방문자 객체는 다양한 객체로 작업하는 동안 유용한 정보를 축적할 수 있다. 이 방법은 객체 트리와 같은 복잡한 객체 구조를 이동하고 방문자를 이 구조의 각 객체에 적용하려는 경우에 유용할 수 있다.

단점

  • 요소 계층 구조에 클래스가 추가되거나 요소 계층 구조에서 제거될 때마다 모든 방문자를 업데이트해야 한다.
  • 방문자는 개인 영역과 작업할 요소의 방법에 대한 필수 액세스 권한이 부족할 수 있다.

Visitor in TypeScript

TypeScript의 패턴 사용

복잡도: ★★★

인기: ★☆☆

사용 예: 방문자는 복잡성과 좁은 적용 가능성 때문에 매우 흔한 패턴이 아니다.

index.ts

// 컴포넌트 인터페이스는 기본 방문자 인터페이스를 인수로 받아들여야 하는 'accept' 메서드를 선언한다.
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 패턴은 로직과 구조를 분리하는 패턴으로 알고리즘 로직과 적용할 객체를 분리한다.

→ 더블 디스패치라는 기술을 사용하여 구현된다. (Java에 있는 개념을 말하는 건가..?)

이를 사용하는 패턴의 예시로 프론트엔드에선 바벨이 비지터 패턴을 사용한다고 한다.

참고 사이트

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글