방문자 패턴에서 accept () 메소드의 요점은 무엇입니까?


87

알고리즘을 클래스에서 분리하는 것에 대한 많은 이야기가 있습니다. 그러나 한 가지는 설명되지 않은 채로 남아 있습니다.

그들은 이렇게 방문자를 사용합니다

abstract class Expr {
  public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}

class ExprVisitor extends Visitor{
  public Integer visit(Num num) {
    return num.value;
  }

  public Integer visit(Sum sum) {
    return sum.getLeft().accept(this) + sum.getRight().accept(this);
  }

  public Integer visit(Prod prod) {
    return prod.getLeft().accept(this) * prod.getRight().accept(this);
  }

visit (element)를 직접 호출하는 대신 Visitor는 해당 요소가 방문 메소드를 호출하도록 요청합니다. 이는 방문객에 대한 계급 무 인식의 선언과 모순된다.

PS1 자신의 말로 설명하거나 정확한 설명을 가리 키십시오. 두 가지 응답이 일반적이고 불확실한 것을 언급했기 때문입니다.

PS2 내 추측 : getLeft()기본을 반환하기 때문에 Expression호출 visit(getLeft())하면 결과가 visit(Expression)되지만 getLeft()호출 visit(this)하면 다른 더 적절한 방문 호출이 발생합니다. 따라서 accept()유형 변환 (일명 캐스팅)을 수행합니다.

PS3 Scala의 패턴 매칭 = 스테로이드 의 방문자 패턴은 수락 방법없이 방문자 패턴이 얼마나 단순한 지 보여줍니다. Wikipedia는 " accept()반영이 가능할 때 방법이 불필요 하다는 것을 보여주는 논문을 연결 함으로써이 기술에 대해 'Walkabout'이라는 용어를 도입했습니다."



"방문자가 수락을 호출하면 피 호출자의 유형에 따라 cal이 전달됩니다. 그러면 수신자가 방문자의 유형별 방문 방법을 다시 호출하고이 호출은 방문자의 실제 유형에 따라 전달됩니다." 즉, 나를 혼란스럽게하는 것을 말합니다. 이런 이유로 좀 더 구체적으로 말씀해 주시겠습니까?
Val

답변:


154

방문자 패턴의 visit/ accept구조는 C와 유사한 언어 (C #, Java 등) 의미 체계로 인해 필요한 악입니다. 방문자 패턴의 목표는 코드를 읽을 때 예상하는대로 이중 디스패치를 ​​사용하여 통화를 라우팅하는 것입니다.

일반적으로 방문자 패턴이 사용되는 경우 모든 노드가 기본 Node유형 에서 파생되는 개체 계층 구조가 포함 됩니다 Node. 본능적으로 다음과 같이 작성합니다.

Node root = GetTreeRoot();
new MyVisitor().visit(root);

여기에 문제가 있습니다. MyVisitor클래스가 다음과 같이 정의 된 경우 :

class MyVisitor implements IVisitor {
  void visit(CarNode node);
  void visit(TrainNode node);
  void visit(PlaneNode node);
  void visit(Node node);
}

런타임에 실제 유형에 관계없이 root호출이 overload로 들어가면 visit(Node node). 이것은 type으로 선언 된 모든 변수에 적용됩니다 Node. 왜 이런거야? Java 및 기타 C 계열 언어 는 호출 할 오버로드를 결정할 때 매개 변수 의 정적 유형 또는 변수가 선언 된 유형 만 고려 하기 때문입니다. Java는 런타임시 모든 메서드 호출에 대해 "좋아요, 동적 유형이 root무엇입니까? "라는 추가 단계를 거치지 않습니다 . 유형의 매개 변수를 허용하는 TrainNode메서드가 있는지 살펴 보겠습니다.MyVisitorTrainNode... ". 컴파일러는 컴파일 타임에 호출 될 메서드를 결정합니다. (Java가 실제로 인수의 동적 유형을 검사했다면 성능이 매우 끔찍할 것입니다.)

자바는 메소드가 호출 될 때 객체의 런타임 (즉, 동적) 유형을 고려하는 하나의 도구를 제공합니다 . 가상 메소드 디스패치 . 가상 메서드를 호출 할 때 실제로 호출 은 함수 포인터로 구성된 메모리 의 테이블 로 이동합니다 . 각 유형에는 테이블이 있습니다. 특정 메서드가 클래스에 의해 재정의 된 경우 해당 클래스의 함수 테이블 항목에는 재정의 된 함수의 주소가 포함됩니다. 클래스가 메서드를 재정의하지 않으면 기본 클래스의 구현에 대한 포인터가 포함됩니다. 여전히 성능 오버 헤드가 발생합니다 (각 메서드 호출은 기본적으로 두 개의 포인터를 역 참조합니다 : 하나는 유형의 함수 테이블을 가리키고 다른 하나는 함수 자체를 가리킴). 매개 변수 유형을 검사하는 것보다 여전히 빠릅니다.

방문자 패턴의 목표는 이중 디스패치 를 수행하는 것입니다. 고려되는 호출 대상 유형 ( MyVisitor, 가상 메서드를 통해)뿐만 아니라 매개 변수 유형 (어떤 유형을 Node보고 있는지)도 고려해야합니다 . 방문자 패턴을 사용하면 visit/ accept조합으로 이를 수행 할 수 있습니다 .

다음과 같이 라인을 변경합니다.

root.accept(new MyVisitor());

원하는 것을 얻을 수 있습니다. 가상 메서드 디스패치를 ​​통해 서브 클래스에 의해 구현 된 올바른 accept () 호출을 입력합니다.이 예제에서는 의 구현을 TrainElement입력합니다 .TrainElementaccept()

class TrainNode extends Node implements IVisitable {
  void accept(IVisitor v) {
    v.visit(this);
  }
}

무엇의 범위 내에서,이 시점에서 컴파일러 노하우를 수행 TrainNodeaccept? 그것은의 정적 유형이 있음을 알고 this입니다TrainNode . 거기가 알고있는 모든 약이 컴파일러는 우리 발신자의 범위를 알고 아니었다 정보의 중요한 추가 조각입니다 root그것은을이었다이었다 Node. 이제 컴파일러는 this( root)가 단순한 것이 Node아니라 실제로는 TrainNode. 결과적으로 accept(): 안에있는 한 줄은 v.visit(this)완전히 다른 것을 의미합니다. 의 컴파일러는 이제 과부하를 찾습니다 visit()그이 소요됩니다 TrainNode. 찾을 수없는 경우에는 호출을 컴파일하여Node. 둘 다 존재하지 않으면 컴파일 오류가 발생합니다 (를 사용하는 오버로드가없는 경우 object). 실행 따라서 우리 모두가 함께 구성했던 것을 입력합니다 : MyVisitor의를의 구현 visit(TrainNode e). 캐스트가 필요하지 않았고 가장 중요한 것은 반사가 필요하지 않았다는 것입니다. 따라서이 메커니즘의 오버 헤드는 다소 낮습니다. 포인터 참조로만 구성되고 다른 것은 없습니다.

귀하의 질문이 맞습니다. 캐스트를 사용하여 올바른 행동을 얻을 수 있습니다. 그러나 종종 우리는 Node가 어떤 유형인지조차 모릅니다. 다음 계층의 경우를 살펴보십시오.

abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }

그리고 우리는 소스 파일을 구문 분석하고 위의 사양을 준수하는 객체 계층을 생성하는 간단한 컴파일러를 작성했습니다. 방문자로 구현 된 계층 구조에 대한 인터프리터를 작성하는 경우 :

class Interpreter implements IVisitor<int> {
  int visit(AdditionNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this); 
    return left + right;
  }
  int visit(MultiplicationNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this);
    return left * right;
  }
  int visit(LiteralNode n) {
    return n.value;
  }
}

우리의 유형을 알고하지 않기 때문에, 아주 멀리 우리를 얻을 수 없겠죠 주조 left또는 rightvisit()방법을. 우리의 파서는 Node계층 구조의 루트를 가리키는 유형의 객체도 반환 할 가능성이 높 으므로 안전하게 캐스팅 할 수 없습니다. 따라서 간단한 인터프리터는 다음과 같이 보일 수 있습니다.

Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);

방문자 패턴을 사용하면 매우 강력한 작업을 수행 할 수 있습니다. 객체 계층 구조가 주어지면 계층 구조의 클래스 자체에 코드를 넣을 필요없이 계층 구조에서 작동하는 모듈 식 작업을 만들 수 있습니다. 방문자 패턴은 예를 들어 컴파일러 구성에서 널리 사용됩니다. 특정 프로그램의 구문 트리가 주어지면 해당 트리에서 작동하는 많은 방문자가 작성됩니다. 유형 검사, 최적화, 기계어 코드 방출은 일반적으로 모두 다른 방문자로 구현됩니다. 최적화 방문자의 경우 입력 트리가 주어지면 새로운 구문 트리를 출력 할 수도 있습니다.

물론 단점이 있습니다. 계층 구조에 새 유형을 추가하면 visit()해당 새 유형에 대한 메서드 도 IVisitor인터페이스에 추가하고 모든 방문자에게 스텁 (또는 전체) 구현을 만들어야합니다. accept()위에서 설명한 이유 때문에 메서드도 추가해야합니다 . 성능이 그다지 의미가 없다면를 필요로하지 않고 방문자를 작성하는 솔루션이 accept()있지만 일반적으로 반사가 수반되므로 상당한 오버 헤드가 발생할 수 있습니다.


5
유효한 Java 항목 # 41 에는 다음과 같은 경고가 포함됩니다 . " 캐스트를 추가하여 동일한 매개 변수 세트가 다른 오버로딩에 전달 될 수있는 상황을 피하십시오. " accept()이 경고가 방문자에게 위반 될 때 메소드가 필요합니다.
jaco0646

" 일반적으로 방문자 패턴이 사용되는 경우 모든 노드가 기본 노드 유형에서 파생되는 개체 계층이 포함됩니다. "이것은 C ++에서 절대적으로 필요하지 않습니다. Boost.Variant, Eggs.Variant
Jean-Michaël Celerier

자바에서 우리는 항상 가장 구체적인 유형의 메소드를 호출하기 때문에 자바에서 우리가 정말 받아 들일 방법을 필요로하지 않는다는 것을 나에게 보인다
길 라드 Baruchian

1
와, 이것은 멋진 설명이었습니다. 패턴의 모든 그림자가 컴파일러 제한 때문이라는 것을 깨달음으로써 이제 여러분 덕분에 분명하게 드러납니다.
알폰소 니시카와

@GiladBaruchian, 컴파일러는 컴파일러 가 결정할 수 있는 가장 구체적인 유형 메서드에 대한 호출을 생성합니다 .
mmw dec

15

물론 그것이 Accept가 구현 되는 유일한 방법 이라면 그것은 어리석은 일입니다 .

그러나 그렇지 않습니다.

예를 들어 방문자는 비 터미널 노드의 구현이 다음과 같은 계층 구조를 다룰 때 정말 유용 합니다.

interface IAcceptVisitor<T> {
  void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
  public void Accept(IVisit<T> visitor) {
    visitor.visit(this);
    foreach(var n in this.children)
      n.Accept(visitor);
  }

  private IEnumerable<HierarchyNode> children;
  ....
}

봤어? 당신이 어리 석다고 묘사하는 것은 계층을 순회 하는 해결책입니다.

방문자를 이해하게 만든 훨씬 더 길고 깊이있는 기사가 있습니다.

편집 : 명확히하기 위해 : 방문자의 Visit방법에는 노드에 적용 할 논리가 포함되어 있습니다. 노드의 Accept메서드에는 인접 노드로 이동하는 방법에 대한 논리가 포함되어 있습니다. 당신이 사건 에만 이중 파견 더 간단로 이동하는 인접 노드가있는 특별한 경우이다.


7
귀하의 설명은 방문자의 적절한 visit () 메서드가 아닌 노드의 책임이어야하는 이유를 설명하지 않습니다. 다른 방문자에 대해 동일한 방문 패턴이 필요할 때 주요 아이디어가 계층 구조 순회 코드를 공유한다는 의미입니까? 추천 논문에서 힌트가 보이지 않습니다.
Val

1
수락이 일상적인 순회에 좋다고 말하는 것은 일반 대중에게 합리적이고 가치가 있습니다. 하지만 누군가의 " andymaleh.blogspot.com/2008/04/…을 읽을 때까지 방문자 패턴을 이해할 수 없었습니다."에서 예제를 가져 왔습니다 . 이 예제, Wikipedia 또는 다른 답변 모두 탐색 이점을 언급하지 않습니다. 그럼에도 불구하고 그들은 모두이 어리석은 accept ()를 요구합니다. 그것이 내 질문을하는 이유입니다 : 왜?
Val

1
@Val-무슨 뜻이야? 무엇을 요구 하시는지 잘 모르겠습니다. 다른 기사에 대해 말할 수는 없습니다. 그 사람들은이 일에 대해 다른 견해를 가지고 있기 때문에 우리가 의견이 일치하지 않는 것 같습니다. 일반적으로 계산에서 많은 문제가 네트워크에 매핑 될 수 있으므로 사용은 표면의 그래프와 관련이 없지만 실제로는 매우 유사한 문제입니다.
George Mauer 2012

1
어떤 방법이 유용 할 수 있는지에 대한 예를 제공한다고해서 그 방법이 필수 인 이유에 대한 질문에 답할 수 없습니다. 내비게이션이 항상 필요한 것은 아니기 때문에 accept () 메소드가 방문하기에 항상 좋은 것은 아닙니다. 그러므로 우리는 그것 없이도 목표를 달성 할 수 있어야합니다. 그럼에도 불구하고 필수입니다. 이는 "때로는 유용하다"는 것보다 모든 방문자 패턴에 accept ()를 도입하는 더 강력한 이유가 있음을 의미합니다. 내 질문에서 명확하지 않은 것은 무엇입니까? Wikipedia가 수락을 제거하는 방법을 찾는 이유를 이해하지 않으려면 내 질문을 이해하는 데 관심이 없습니다.
Val

1
@Val 그들이 "방문자 패턴의 본질"에 링크하는 논문은 내가 말한 것처럼 그것의 초록에서 탐색과 작동의 동일한 분리를 지적합니다. 그들은 단순히 GOF 구현 (당신이 묻는 것)에 반사를 사용하여 제거 할 수있는 몇 가지 제한과 성가심이 있다고 말하고 있으므로 Walkabout 패턴을 도입합니다. 이것은 확실히 유용하고 방문자가 할 수있는 것과 동일한 일을 많이 할 수 있지만 상당히 정교한 코드가 많고 (간단하게 읽으면) 유형 안전성의 이점을 잃어 버립니다. 도구 상자를위한 도구이지만 방문자보다 더 무거운 도구입니다
George Mauer 2012

0

방문자 패턴의 목적은 객체가 방문자가 작업을 마치고 떠난시기를 알 수 있도록하여 클래스가 나중에 필요한 정리를 수행 할 수 있도록하는 것입니다. 또한 클래스가 내부를 'ref'매개 변수로 "일시적으로"노출 할 수 있으며 방문자가 사라지면 내부가 더 이상 노출되지 않음을 알 수 있습니다. 정리가 필요하지 않은 경우 방문자 패턴은 그다지 유용하지 않습니다. 이러한 작업을 수행하지 않는 클래스는 방문자 패턴의 이점을 얻지 못할 수 있지만 방문자 패턴을 사용하도록 작성된 코드는 액세스 후 정리가 필요할 수있는 향후 클래스에서 사용할 수 있습니다.

예를 들어 원자 적으로 업데이트해야하는 많은 문자열을 보유한 데이터 구조가 있지만 데이터 구조를 보유한 클래스가 수행해야하는 원자 적 업데이트 유형을 정확히 알지 못한다고 가정합니다 (예 : 한 스레드가 "의 모든 항목을 대체하려는 경우). X ", 다른 스레드는 숫자 시퀀스를 숫자가 한 단계 더 높은 시퀀스로 바꾸려고하지만 두 스레드의 작업이 성공해야합니다. 각 스레드가 단순히 문자열을 읽고 업데이트를 수행 한 다음 다시 쓴 경우 두 번째 스레드 문자열을 다시 쓰려면 첫 번째 문자열을 덮어 씁니다). 이를 수행하는 한 가지 방법은 각 스레드가 잠금을 획득하고 작업을 수행하고 잠금을 해제하도록하는 것입니다. 안타깝게도 자물쇠가 그런 식으로 노출되면

방문자 패턴은 이러한 문제를 피하기 위해 (적어도) 세 가지 접근 방식을 제공합니다.

  1. 레코드를 잠그고 제공된 함수를 호출 한 다음 레코드를 잠금 해제 할 수 있습니다. 제공된 함수가 무한 루프에 빠지면 레코드가 영원히 잠길 수 있지만 제공된 함수가 예외를 반환하거나 throw하면 레코드가 잠금 해제됩니다 (함수가 예외를 throw하면 레코드를 유효하지 않은 것으로 표시하는 것이 합리적 일 수 있음). 잠긴 것은 아마도 좋은 생각이 아닙니다.) 호출 된 함수가 다른 잠금을 얻으려고하면 교착 상태가 발생할 수 있다는 점에 유의하십시오.
  2. 일부 플랫폼에서는 문자열을 'ref'매개 변수로 포함하는 저장 위치를 ​​전달할 수 있습니다. 그런 다음 해당 함수는 문자열을 복사하고, 복사 된 문자열을 기반으로 새 문자열을 계산하고, 이전 문자열을 새 문자열과 비교하고, CompareExchange가 실패하면 전체 프로세스를 반복 할 수 있습니다.
  3. 문자열의 복사본을 만들고 문자열에서 제공된 함수를 호출 한 다음 CompareExchange 자체를 사용하여 원본을 업데이트하고 CompareExchange가 실패하면 전체 프로세스를 반복 할 수 있습니다.

방문자 패턴이 없으면 원자 적 업데이트를 수행하려면 호출 소프트웨어가 엄격한 잠금 / 잠금 해제 프로토콜을 따르지 않으면 잠금을 노출하고 실패 할 위험이 있습니다. 방문자 패턴을 사용하면 원자 적 업데이트를 비교적 안전하게 수행 할 수 있습니다.


2
1. 방문은 방문자가 유용하게 사용할 수 있도록 내부 잠금을 공개적으로 액세스 할 수 있도록해야하는 공개 방문 방법에만 액세스 할 수 있음을 의미합니다. 2 / 전에 본 예 중 어떤 것도 Visitor가 방문 상태를 변경하는 데 사용되어야한다는 것을 의미하지 않습니다. 3. "기존의 VisitorPattern에서는 노드에 들어갈 때만 결정할 수 있습니다. 현재 노드에 들어가기 전에 이전 노드를 떠 났는지 알 수 없습니다." visitEnter 및 visitLeave 대신 방문만으로 잠금을 해제하는 방법은 무엇입니까? 마지막으로 Visitor보다는 accpet ()의 응용 프로그램에 대해 물었습니다.
Val

아마도 내가 패턴에 대한 용어로 속도를내는 것은 아니지만 "방문자 패턴"은 X가 Y를 대리자로 전달하고 Y가 다음과 같이 유효하기 만하면되는 정보를 전달할 수있는 내가 사용한 접근 방식과 유사한 것 같습니다. 델리게이트가 실행되는 동안. 패턴에 다른 이름이있을 수 있습니까?
supercat

2
이것은 특정 문제에 대한 방문자 패턴 의 흥미로운 적용 이지만 패턴 자체를 설명하거나 원래 질문에 답하지는 않습니다. "정리가 필요하지 않은 경우 방문자 패턴은 그다지 유용하지 않습니다." 이 주장은 확실히 거짓이며 일반적인 패턴이 아닌 특정 문제와 만 관련이 있습니다.
Tony O'Hagan

0

수정이 필요한 클래스는 모두 'accept'메소드를 구현해야합니다. 클라이언트는이 accept 메서드를 호출하여 해당 클래스 제품군에 대해 몇 가지 새로운 작업을 수행하여 기능을 확장합니다. 클라이언트는이 하나의 accept 메서드를 사용하여 각 특정 작업에 대해 다른 방문자 클래스를 전달하여 광범위한 새 작업을 수행 할 수 있습니다. 방문자 클래스에는 패밀리 내의 모든 클래스에 대해 동일한 특정 작업을 수행하는 방법을 정의하는 재정의 된 여러 방문 메서드가 포함되어 있습니다. 이러한 방문 방법은 작동 할 인스턴스를 전달받습니다.

방문자는 기능의 각 항목이 각 방문자 클래스에서 별도로 정의되고 클래스 자체를 변경할 필요가 없기 때문에 안정적인 클래스 제품군에 기능을 자주 추가, 변경 또는 제거하는 경우 유용합니다. 클래스 패밀리가 안정적이지 않은 경우 많은 방문자가 클래스를 추가하거나 제거 할 때마다 변경해야하기 때문에 방문자 패턴이 덜 사용됩니다.


-1

좋은의 예는 소스 코드를 컴파일에 있습니다 :

interface CompilingVisitor {
   build(SourceFile source);
}

클라이언트는을 구현할 수 JavaBuilder, RubyBuilder, XMLValidator, 등을 프로젝트의 모든 소스 파일을 수집하고 방문의 구현은 변경이 필요하지 않습니다.

각 소스 파일 유형에 대해 별도의 클래스가있는 경우 이는 잘못된 패턴입니다.

interface CompilingVisitor {
   build(JavaSourceFile source);
   build(RubySourceFile source);
   build(XMLSourceFile source);
}

그것은 맥락과 시스템의 어떤 부분을 확장 할 수 있는지에 달려 있습니다.


아이러니는 VisitorPattern이 우리에게 나쁜 패턴을 사용하도록 제안한다는 것입니다. 방문 할 모든 종류의 노드에 대해 방문 방법을 정의해야한다고 말합니다. 둘째, 당신의 모범이 좋은지 나쁜지 명확하지 않습니까? 내 질문과 어떤 관련이 있습니까?
Val
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.