방문자 패턴에서 accept () 메소드의 요점은 무엇입니까?
클래스에서 알고리즘을 분리하는 것에 대한 많은 이야기가 있습니다. 그러나 한 가지는 설명되지 않은 채로 남아 있습니다.
그들은 이렇게 방문자를 사용합니다
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의 패턴 매칭 = 스테로이드 의 방문자 패턴은 accept 메소드없이 방문자 패턴이 얼마나 단순한 지 보여줍니다. Wikipedia는 " accept()
반영이 가능할 때 방법이 불필요 하다는 것을 보여주는 논문을 연결 하여이 기술에 대해 'Walkabout'이라는 용어를 도입합니다."
방문자 패턴의 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
메서드가 있는지 살펴 보겠습니다.MyVisitor
TrainNode
... ". 컴파일러는 컴파일 타임에 호출 될 메소드를 결정합니다. (Java가 실제로 인수의 동적 유형을 검사했다면 성능이 매우 끔찍할 것입니다.)
자바는 메소드가 호출 될 때 객체의 런타임 (즉, 동적) 유형을 고려하는 하나의 도구를 제공합니다 . 가상 메소드 디스패치 . 가상 메서드를 호출 할 때 실제로 호출 은 함수 포인터로 구성된 메모리 의 테이블 로 이동합니다 . 각 유형에는 테이블이 있습니다. 특정 메서드가 클래스에 의해 재정의 된 경우 해당 클래스의 함수 테이블 항목에는 재정의 된 함수의 주소가 포함됩니다. 클래스가 메서드를 재정의하지 않으면 기본 클래스의 구현에 대한 포인터가 포함됩니다. 여전히 성능 오버 헤드가 발생하지만 (각 메서드 호출은 기본적으로 두 개의 포인터를 역 참조합니다. 하나는 유형의 함수 테이블을 가리키고 다른 하나는 함수 자체를 가리키는 것입니다) 매개 변수 유형을 검사하는 것보다 여전히 빠릅니다.
방문자 패턴의 목표는 이중 디스패치 를 수행하는 것입니다. 고려되는 호출 대상 유형 ( MyVisitor
, 가상 메서드를 통해)뿐만 아니라 매개 변수 유형 (어떤 유형을 Node
보고 있는지)도 고려해야합니다 . 방문자 패턴을 사용하면 visit
/ accept
조합으로 이를 수행 할 수 있습니다 .
다음과 같이 라인을 변경합니다.
root.accept(new MyVisitor());
원하는 것을 얻을 수 있습니다. 가상 메서드 디스패치를 통해 하위 클래스에 의해 구현 된 올바른 accept () 호출을 입력합니다.이 예제에서는 의 구현을 TrainElement
입력합니다 .TrainElement
accept()
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
무엇의 범위 내에서,이 시점에서 컴파일러 노하우를 수행 TrainNode
의 accept
? 그것은의 정적 유형이 있음을 알고 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;
}
}
Casting wouldn't get us very far, since we don't know the types of left
or right
in the visit()
methods. Our parser would most likely also just return an object of type Node
which pointed at the root of the hierarchy as well, so we can't cast that safely either. So our simple interpreter can look like:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
The visitor pattern allows us to do something very powerful: given an object hierarchy, it allows us to create modular operations that operate over the hierarchy without needing requiring to put the code in the hierarchy's class itself. The visitor pattern is used widely, for example, in compiler construction. Given the syntax tree of a particular program, many visitors are written that operate on that tree: type checking, optimizations, machine code emission are all usually implemented as different visitors. In the case of the optimization visitor, it can even output a new syntax tree given the input tree.
It has its drawbacks, of course: if we add a new type into the hierarchy, we need to also add a visit()
method for that new type into the IVisitor
interface, and create stub (or full) implementations in all of our visitors. We also need to add the accept()
method too, for the reasons described above. If performance doesn't mean that much to you, there are solutions for writing visitors without needing the accept()
, but they normally involve reflection and thus can incur quite a large overhead.
Of course that would be silly if that was the only way that Accept is implemented.
But it is not.
For example, visitors are really really useful when dealing with hierarchies in which case the implementation of a non-terminal node might be something like this
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;
....
}
You see? What you describe as stupid is the solution for traversing hierarchies.
Here is a much longer and in depth article that made me understand visitor.
Edit: To clarify: The visitor's Visit
method contains logic to be applied to a node. The node's Accept
method contains logic on how to navigate to adjacent nodes. The case where you only double dispatch is a special case where there are simply no adjacent nodes to navigate to.
The purpose of the Visitor pattern is to ensure that objects know when the visitor is finished with them and have departed, so the classes can perform any necessary cleanup afterward. It also allows classes to expose their internals "temporarily" as 'ref' parameters, and know that the internals will no longer be exposed once the visitor is gone. In cases where no cleanup is necessary, the visitor pattern isn't terribly useful. Classes which do neither of these things may not benefit from the visitor pattern, but code which is written to use the visitor pattern will be usable with future classes that may require cleanup after access.
For example, suppose one has a data structure holding many strings that should be updated atomically, but the class holding the data structure doesn't know precisely what types of atomic updates should be performed (e.g. if one thread wants to replace all occurrences of "X", while another thread wants to replace any sequence of digits with a sequence that is numerically one higher, both threads' operations should succeed; if each thread simply read out a string, performed its updates, and wrote it back, the second thread to write back its string would overwrite the first). One way to accomplish this would be to have each thread acquire a lock, perform its operation, and release the lock. Unfortunately, if locks are exposed in that way, the data structure would have no way of preventing someone from acquiring a lock and never releasing it.
The Visitor pattern offers (at least) three approaches to avoid that problem:
- It can lock a record, call the supplied function, and then unlock the record; the record could be locked forever if the supplied function falls into an endless loop, but if the supplied function returns or throws an exception, the record will be unlocked (it may be reasonable to mark the record invalid if the function throws an exception; leaving it locked is probably not a good idea). Note that it's important that if the called function attempts to acquire other locks, deadlock could result.
- On some platforms, it can pass a storage location holding the string as a 'ref' parameter. That function could then copy the string, compute a new string based upon the copied string, attempt to CompareExchange the old string to the new one, and repeat the whole process if the CompareExchange fails.
- It can make a copy of the string, call the supplied function on the string, then use CompareExchange itself to attempt to update the original, and repeat the whole process if the CompareExchange fails.
Without the visitor pattern, performing atomic updates would require exposing locks and risking failure if calling software fails to follow a strict locking/unlocking protocol. With the Visitor pattern, atomic updates can be done relatively safely.
The classes that require modification must all implement the 'accept' method. Clients call this accept method to perform some new action on that family of classes thereby extending their functionality. Clients are able to use this one accept method to perform a wide range of new actions by passing in a different visitor class for each specific action. A visitor class contains multiple overridden visit methods defining how to achieve that same specific action for every class within the family. These visit methods get passed an instance on which to work.
Visitors are useful if you are frequently adding, altering or removing functionality to a stable family of classes because each item of functionality is defined seperately in each visitor class and the classes themselves do not need changing. If the family of classes is not stable then the visitor pattern may be of less use, because many visitors need changing each time a class is added or removed.
A good example is in source code compilation:
interface CompilingVisitor {
build(SourceFile source);
}
Clients can implement a JavaBuilder
, RubyBuilder
, XMLValidator
, etc. and the implementation for collecting and visiting all the source files in a project does not need to change.
This would be a bad pattern if you have separate classes for each source file type:
interface CompilingVisitor {
build(JavaSourceFile source);
build(RubySourceFile source);
build(XMLSourceFile source);
}
It comes down to context and what parts of the system you want to be extensible.
참고URL : https://stackoverflow.com/questions/9132178/what-is-the-point-of-accept-method-in-visitor-pattern
'IT TIP' 카테고리의 다른 글
Python : 목록에서 첫 번째 문자열의 첫 번째 문자를 얻습니까? (0) | 2020.10.18 |
---|---|
EGIT를 사용하여 원격 저장소에서 분기를 삭제하는 방법은 무엇입니까? (0) | 2020.10.18 |
D3.js :“Uncaught SyntaxError : Unexpected token ILLEGAL”? (0) | 2020.10.18 |
객체 배열 선언 (0) | 2020.10.18 |
Elasticsearch로 JSON 파일 가져 오기 / 인덱싱 (0) | 2020.10.18 |