Yan Hong 박사의 저서 "JAVA and Patterns"는 방문자 패턴에 대한 설명으로 시작됩니다.
방문자 패턴은 객체의 행동 패턴입니다. 방문자 패턴의 목적은 특정 데이터 구조 요소에 적용되는 일부 작업을 캡슐화하는 것입니다. 이러한 작업을 수정해야 하는 경우 이 작업을 허용하는 데이터 구조는 변경되지 않고 그대로 유지될 수 있습니다.
파견의 개념
변수가 선언될 때의 유형을 변수의 정적 유형(Static Type)이라고 하며, 어떤 사람들은 정적 유형을 겉보기 유형(Apparent Type)이라고 부르며, 변수가 참조하는 객체의 실제 유형도 호출합니다. 변수의 실제 유형(실제 유형). 예를 들어:
다음과 같이 코드 코드를 복사합니다.
목록 목록 = null;
목록 = 새로운 ArrayList();
변수 목록이 선언되고 해당 정적 유형(명백한 유형이라고도 함)은 List이고 실제 유형은 ArrayList입니다.
Dispatch는 객체의 종류에 따라 선택되는 방식으로 Static Dispatch와 Dynamic Dispatch의 두 가지 유형으로 나뉜다.
정적 디스패치는 컴파일 타임에 발생하고 정적 유형 정보를 기반으로 디스패치가 발생합니다. 정적 디스패치는 우리에게 낯선 것이 아닙니다. 메소드 오버로딩은 정적 디스패치입니다.
동적 디스패치는 런타임 중에 발생하며 동적 디스패치는 메서드를 동적으로 대체합니다.
정적 파견
Java는 메서드 오버로드를 통해 정적 디스패치를 지원합니다. 묵자가 말을 탄 이야기를 예로 들면, 묵자는 백마를 탈 수도 있고 흑마를 탈 수도 있습니다. 묵자와 백마, 흑마와 말의 클래스 다이어그램은 다음과 같습니다.
이 시스템에서 Mozi는 Mozi 클래스로 표현됩니다. 코드는 다음과 같습니다.
공개 클래스 Mozi {
공공 무효 탑승(말 h){
System.out.println("승마");
}
공공 무효 탑승(WhiteHorse wh){
System.out.println("백마를 타고");
}
퍼블릭 보이드 라이드(BlackHorse bh){
System.out.println("다크호스를 타세요");
}
공개 정적 무효 메인(String[] args) {
말 wh = new WhiteHorse();
말 bh = new BlackHorse();
Mozi mozi = new Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
분명히 Mozi 클래스의 ride() 메소드는 세 가지 메소드로부터 오버로드됩니다. 이 세 가지 메소드는 각각 Horse, WhiteHorse, BlackHorse 및 기타 유형의 매개변수를 허용합니다.
그러면 프로그램이 실행될 때 어떤 결과가 인쇄됩니까? 결과적으로 프로그램은 "horseback"이라는 동일한 두 줄을 인쇄합니다. 즉, 묵자는 자신이 타고 있는 것이 모두 말뿐임을 발견했습니다.
왜? ride() 메서드에 대한 두 호출은 서로 다른 매개 변수, 즉 wh와 bh를 전달합니다. 실제 유형은 다르지만 정적 유형은 모두 동일하며 말 유형입니다.
오버로드된 메서드의 디스패치는 정적 유형을 기반으로 하며 이 디스패치 프로세스는 컴파일 타임에 완료됩니다.
동적 파견
Java는 메서드 재정의를 통해 동적 디스패치를 지원합니다. 말이 풀을 먹는 이야기를 예로 들면, 코드는 다음과 같습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 말 {
공공 무효 먹기(){
System.out.println("풀을 먹는 말");
}
}
다음과 같이 코드 코드를 복사합니다.
공개 클래스 BlackHorse는 Horse {를 확장합니다.
@보수
공공 무효 먹다() {
System.out.println("풀을 먹고 있는 검은 말");
}
}
다음과 같이 코드 코드를 복사합니다.
공개 클래스 클라이언트 {
공개 정적 무효 메인(String[] args) {
말 h = new BlackHorse();
열();
}
}
변수 h의 정적 유형은 Horse이고 실제 유형은 BlackHorse입니다. 위 마지막 줄의 eat() 메서드가 BlackHorse 클래스의 eat() 메서드를 호출하면 위에 인쇄되는 내용은 "Black Horse Eating Grass"입니다. 반대로 위의 eat() 메서드가 eat( ) Horse 클래스의 메서드를 사용하면 "말이 풀을 먹는다"는 내용이 인쇄됩니다.
따라서 문제의 핵심은 Java 컴파일러가 컴파일 중에 어떤 코드가 실행될지 항상 알 수 없다는 것입니다. 왜냐하면 컴파일러는 객체의 정적 유형만 알고 객체의 실제 유형과 메서드를 모르기 때문입니다. 호출은 정적 유형이 아닌 객체의 실제 유형을 기반으로 합니다. 이런 식으로 위 마지막 줄의 eat() 메소드는 BlackHorse 클래스의 eat() 메소드를 호출하여 "검은 말이 풀을 먹고 있다"를 출력합니다.
파견 유형
메소드가 속한 객체를 메소드의 수신자라고 하며 메소드의 매개변수를 총칭하여 메소드의 볼륨이라고 합니다. 예를 들어, 아래 예제에서 Test 클래스의 복사 코드는 다음과 같습니다.
공개 클래스 테스트 {
공공 무효 인쇄(문자열 str){
System.out.println(str);
}
}
위 클래스에서 print() 메소드는 Test 객체에 속하므로 해당 수신자도 Test 객체입니다. print() 메소드에는 str이라는 매개변수가 있으며 해당 유형은 String입니다.
객체지향 언어는 몇 종류의 수량 파견을 기준으로 삼느냐에 따라 단일 파견 언어(Uni-Dispatch)와 다중 파견 언어(Multi-Dispatch)로 나눌 수 있습니다. 단일 디스패치 언어는 하나의 인스턴스 유형을 기준으로 방법을 선택하는 반면, 다중 디스패치 언어는 두 개 이상의 인스턴스 유형을 기준으로 방법을 선택합니다.
C++와 Java는 모두 단일 디스패치 언어이며 다중 디스패치 언어의 예로는 CLOS와 Cecil이 있습니다. 이러한 구별에 따르면, 이 언어의 동적 디스패치는 메서드 수신자의 유형만 고려하기 때문에 Java는 동적 단일 디스패치 언어이고, 이 언어는 오버로드된 메서드를 디스패치하기 때문에 정적 다중 디스패치 언어입니다. 메소드 수신자의 유형과 모든 메소드 매개변수의 유형이 고려됩니다.
동적 단일 디스패치를 지원하는 언어에는 요청이 호출할 작업을 결정하는 두 가지 조건이 있습니다. 하나는 요청의 이름이고 다른 하나는 수신자의 실제 유형입니다. 단일 디스패치는 일반적으로 메서드의 수신자인 하나의 인스턴스만 고려할 수 있도록 메서드 선택 프로세스를 제한합니다. Java 언어에서는 알 수 없는 유형의 객체에 대해 작업이 수행되면 객체의 실제 유형 테스트가 한 번만 발생합니다. 이것이 동적 단일 디스패치의 특징입니다.
이중 파견
메소드는 두 변수의 유형에 따라 다른 코드를 실행하기로 결정합니다. 이것이 "이중 디스패치"입니다. Java 언어는 동적 다중 디스패치를 지원하지 않습니다. 이는 Java가 동적 이중 디스패치를 지원하지 않음을 의미합니다. 그러나 디자인 패턴을 사용하면 동적 이중 디스패치를 Java 언어로 구현할 수도 있습니다.
Java에서는 두 개의 메서드 호출을 통해 두 개의 디스패치를 달성할 수 있습니다. 클래스 다이어그램은 다음과 같습니다.
그림에는 두 개의 물체가 있는데, 왼쪽에 있는 것이 서쪽이고 오른쪽에 있는 것이 동쪽입니다. 이제 West 객체는 먼저 East 객체의 goEast() 메서드를 호출하여 자신을 전달합니다. East 객체가 호출되면 전달된 매개변수를 기반으로 호출자가 누구인지 즉시 알 수 있으므로 "호출자" 객체의 goWest() 메서드가 차례로 호출됩니다. 두 번의 호출을 통해 프로그램 제어가 두 개체에 차례로 넘겨집니다. 시퀀스 다이어그램은 다음과 같습니다.
이러한 방식으로 두 가지 메서드 호출이 있습니다. 프로그램 제어는 먼저 West 개체에서 East 개체로 전달된 다음 다시 West 개체로 전달됩니다.
하지만 단순히 공을 돌려준다고 해서 이중 분배 문제가 해결되는 것은 아닙니다. 핵심은 이 두 호출과 Java 언어의 동적 단일 디스패치 기능을 사용하여 이 전달 프로세스 중에 두 개의 단일 디스패치를 트리거하는 방법입니다.
Java 언어의 동적 단일 디스패치는 하위 클래스가 상위 클래스의 메서드를 재정의할 때 발생합니다. 즉, West와 East는 모두 아래와 같이 고유한 유형 계층 구조에 배치되어야 합니다.
소스 코드
West 클래스 복사 코드는 다음과 같습니다.
공개 추상 클래스 West {
공개 추상 무효 goWest1(SubEast1 east);
공개 추상 무효 goWest2(SubEast2 east);
}
SubWest1 클래스 복사 코드 코드는 다음과 같습니다.
공개 클래스 SubWest1은 West를 확장합니다.
@보수
공공 무효 goWest1(SubEast1 east) {
System.out.println("SubWest1 + " + east.myName1());
}
@보수
공공 무효 goWest2(SubEast2 east) {
System.out.println("SubWest1 + " + east.myName2());
}
}
서브웨스트 클래스 2
다음과 같이 코드 코드를 복사합니다.
공개 클래스 SubWest2는 West를 확장합니다.
@보수
공공 무효 goWest1(SubEast1 east) {
System.out.println("SubWest2 + " + east.myName1());
}
@보수
공공 무효 goWest2(SubEast2 east) {
System.out.println("SubWest2 + " + east.myName2());
}
}
East 클래스 복사 코드는 다음과 같습니다.
공개 추상 클래스 East {
공개 추상 무효 goEast(West west);
}
SubEast1 클래스 복사 코드 코드는 다음과 같습니다.
공개 클래스 SubEast1은 East를 확장합니다.
@보수
공공 무효 goEast(서쪽 서쪽) {
west.goWest1(this);
}
공개 문자열 myName1(){
"SubEast1"을 반환합니다.
}
}
SubEast2 클래스 복사 코드 코드는 다음과 같습니다.
공개 클래스 SubEast2는 East를 확장합니다.
@보수
공공 무효 goEast(서쪽 서쪽) {
west.goWest2(this);
}
공개 문자열 myName2(){
"SubEast2"를 반환합니다.
}
}
클라이언트 클래스 복사 코드는 다음과 같습니다.
공개 클래스 클라이언트 {
공개 정적 무효 메인(String[] args) {
//조합 1
동쪽 동쪽 = new SubEast1();
서쪽 서쪽 = new SubWest1();
east.goEast(west);
//조합 2
east = new SubEast1();
서쪽 = 새로운 SubWest2();
east.goEast(west);
}
}
실행 결과는 다음과 같습니다. 코드는 다음과 같습니다.
서브웨스트1 + 서브이스트1
서브웨스트2 + 서브이스트1
시스템이 실행 중일 때 SubWest1 및 SubEast1 객체가 먼저 생성된 후 클라이언트는 SubEast1의 goEast() 메서드를 호출하고 SubWest1 객체를 전달합니다. SubEast1 객체는 해당 슈퍼클래스 East의 goEast() 메서드를 재정의하므로 이때 동적 단일 디스패치가 발생합니다. SubEast1 객체가 호출을 받으면 매개변수에서 SubWest1 객체를 가져오므로 즉시 이 객체의 goWest1() 메서드를 호출하고 자신을 전달합니다. SubEast1 개체에는 호출할 개체를 선택할 수 있는 권한이 있으므로 이때 또 다른 동적 메서드 디스패치가 수행됩니다.
이때 SubWest1 개체는 SubEast1 개체를 획득했습니다. 이 객체의 myName1() 메서드를 호출하면 자신의 이름과 SubEast 객체의 이름을 출력할 수 있습니다. 시퀀스 다이어그램은 다음과 같습니다.
이 두 이름 중 하나는 동부 계층 구조에서, 다른 하나는 서부 계층 구조에서 나오므로 이들의 조합은 동적으로 결정됩니다. 이것이 동적 이중 디스패치의 구현 메커니즘입니다.
방문자 패턴의 구조
방문자 패턴은 상대적으로 결정되지 않은 데이터 구조를 가진 시스템에 적합합니다. 이는 데이터 구조와 구조에 작용하는 작업 간의 결합을 분리하여 작업 집합이 상대적으로 자유롭게 발전할 수 있도록 합니다. 방문자 패턴의 단순화된 다이어그램은 다음과 같습니다.
데이터 구조의 각 노드는 방문자의 호출을 수락할 수 있습니다. 이 노드는 노드 개체를 방문자 개체에 전달하고 방문자 개체는 차례로 노드 개체의 작업을 수행합니다. 이 프로세스를 "이중 디스패치"라고 합니다. 노드는 방문자를 호출하여 자신을 전달하고 방문자는 이 노드에 대해 알고리즘을 실행합니다. 방문자 패턴의 도식적 클래스 다이어그램은 다음과 같습니다.
방문자 모드와 관련된 역할은 다음과 같습니다.
● 추상 방문자(방문자) 역할 : 모든 특정 방문자 역할이 구현해야 하는 인터페이스를 형성하기 위해 하나 이상의 메소드 작업을 선언합니다.
● 구상 방문자(ConcreteVisitor) 역할 : 추상 방문자가 선언한 인터페이스, 즉 추상 방문자가 선언한 각 접근 동작을 구현한다.
● 추상 노드(Node) 역할 : 수락 작업을 선언하고 방문자 객체를 매개변수로 받아들인다.
● ConcreteNode 역할 : 추상 노드가 지정한 수용 작업을 구현합니다.
● 구조 개체(ObjectStructure) 역할 : 다음과 같은 책임이 있으며, 필요한 경우 구조의 모든 요소를 탐색할 수 있고, 필요한 경우 방문자 개체가 각 요소에 액세스할 수 있도록 상위 수준 인터페이스를 제공하거나 복합 개체로 설계할 수 있습니다. List 또는 Set과 같은 컬렉션입니다.
소스 코드
보시다시피 추상 방문자 역할은 각 특정 노드에 대한 액세스 작업을 준비합니다. 두 개의 노드가 있으므로 두 개의 해당 액세스 작업이 있습니다.
다음과 같이 코드 코드를 복사합니다.
공개 인터페이스 방문자 {
/**
* NodeA의 접근 동작에 해당
*/
공개 무효 방문(NodeA 노드);
/**
* NodeB의 접속 동작에 해당
*/
공개 무효 방문(NodeB 노드);
}
특정 VisitorA 클래스 복사 코드는 다음과 같습니다.
공개 클래스 VisitorA는 방문자 {를 구현합니다.
/**
* NodeA의 접근 동작에 해당
*/
@보수
공개 무효 방문(NodeA 노드) {
System.out.println(node.OperationA());
}
/**
* NodeB의 접속 동작에 해당
*/
@보수
공개 무효 방문(NodeB 노드) {
System.out.println(node.OperationB());
}
}
특정 방문자 VisitorB 클래스 복사 코드는 다음과 같습니다.
공개 클래스 VisitorB는 방문자 {를 구현합니다.
/**
* NodeA의 접근 동작에 해당
*/
@보수
공개 무효 방문(NodeA 노드) {
System.out.println(node.OperationA());
}
/**
* NodeB의 접속 동작에 해당
*/
@보수
공개 무효 방문(NodeB 노드) {
System.out.println(node.OperationB());
}
}
추상 노드 클래스 복사 코드 코드는 다음과 같습니다.
공개 추상 클래스 노드 {
/**
* 작업 수락
*/
공개 추상 무효 수락(방문자 방문자);
}
특정 노드 클래스 NodeA
다음과 같이 코드 코드를 복사합니다.
공개 클래스 NodeA는 노드를 확장합니다{
/**
* 작업 수락
*/
@보수
공개 무효 수락(방문객 방문자) {
방문자.방문(this);
}
/**
*NodeA 관련 방법
*/
공개 문자열 연산A(){
"NodeA"를 반환합니다.
}
}
특정 노드 클래스 NodeB
다음과 같이 코드 코드를 복사합니다.
공개 클래스 NodeB는 Node{를 확장합니다.
/**
*수락 방법
*/
@보수
공개 무효 수락(방문객 방문자) {
방문자.방문(this);
}
/**
*NodeB별 방법
*/
공개 문자열 연산B(){
"NodeB"를 반환합니다.
}
}
구조적 개체 역할 클래스 이 구조적 개체 역할은 컬렉션을 보유하고 컬렉션에 대한 관리 작업으로 외부 세계에 add() 메서드를 제공합니다. 이 메소드를 호출하면 새 노드를 동적으로 추가할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 ObjectStructure {
private List<Node> 노드 = new ArrayList<Node>();
/**
* 메소드 연산 실행
*/
공공 무효 조치(방문자 방문객){
for(노드 노드 : 노드)
{
node.accept(방문자);
}
}
/**
* 새로운 요소 추가
*/
public void add(노드 노드){
노드.추가(노드);
}
}
클라이언트 클래스 복사 코드는 다음과 같습니다.
공개 클래스 클라이언트 {
공개 정적 무효 메인(String[] args) {
//구조체 객체 생성
ObjectStructure os = 새로운 ObjectStructure();
//구조체에 노드 추가
os.add(새 NodeA());
//구조체에 노드 추가
os.add(새 NodeB());
//방문자 생성
방문자 방문자 = new VisitorA();
os.action(방문자);
}
}
이 도식적 구현에는 여러 분기 노드가 있는 복잡한 개체 트리 구조가 나타나지 않지만 실제 시스템에서는 방문자 패턴이 일반적으로 복잡한 개체 트리 구조를 처리하는 데 사용되며 방문자 패턴은 여러 계층에 걸쳐 있는 트리 구조 문제를 처리하는 데 사용됩니다. . 이곳은 방문자 패턴이 매우 강력한 곳입니다.
준비 과정 순서도
먼저 이 예시 클라이언트는 구조 개체를 생성한 다음 새 NodeA 개체와 새 NodeB 개체를 전달합니다.
둘째, 클라이언트는 VisitorA 개체를 생성하고 이 개체를 구조 개체에 전달합니다.
그런 다음 클라이언트는 구조 객체 집합 관리 방법을 호출하여 NodeA 및 NodeB 노드를 구조 객체에 추가합니다.
마지막으로 클라이언트는 구조 개체의 작업 메서드 action()을 호출하여 액세스 프로세스를 시작합니다.
액세스 프로세스 순서도
구조 개체는 저장한 컬렉션의 모든 노드(이 시스템에서는 노드 A 및 NodeB)를 통과합니다. 먼저 NodeA에 액세스합니다. 이 액세스는 다음 작업으로 구성됩니다.
(1) NodeA 객체의 accept() 메서드가 호출되고 VisitorA 객체 자체가 전달됩니다.
(2) NodeA 개체는 차례로 VisitorA 개체의 액세스 메서드를 호출하고 NodeA 개체 자체를 전달합니다.
(3) VisitorA 객체는 NodeA 객체의 고유한 메소드인 OperationA()를 호출합니다.
이로써 이중 디스패치 과정이 완료됩니다. 그러면 NodeB에 접속하게 됩니다. 접속 과정은 NodeA의 접속 과정과 동일하므로 여기서는 설명하지 않겠습니다.
방문자 패턴의 장점
● 좋은 확장성은 개체 구조의 요소를 수정하지 않고도 개체 구조의 요소에 새 기능을 추가할 수 있습니다.
● 재사용성이 우수하면 방문자가 전체 객체 구조에 공통된 기능을 정의할 수 있어 재사용성이 향상됩니다.
● 관련 없는 행위 분리 방문자를 이용하여 관련 없는 행위를 분리하고, 관련된 행위를 함께 캡슐화하여 방문자를 구성함으로써 각 방문자의 기능이 상대적으로 단일하도록 할 수 있습니다.
방문자 패턴의 단점
● 객체 구조를 변경하는 것은 어렵습니다. 객체 구조의 클래스가 자주 변경되는 상황에는 적합하지 않습니다. 객체 구조가 변경되므로 그에 따라 방문자의 인터페이스와 구현도 변경되어야 하므로 비용이 너무 많이 듭니다.
● 캡슐화 깨기 방문자 패턴은 일반적으로 방문자에게 내부 데이터를 공개하는 개체 구조와 개체의 캡슐화를 깨는 ObjectStructrue가 필요합니다.