DesignPatternSmalltalkCompanion:Visitor
VISITOR (DP 331)
의도
객체 요소로부터 떨어진 클래스에서 객체 구조의 요소들에 수행할 오퍼레이션을 표현한 패턴이다. Visitor 패턴은 오퍼레이션이 처리할 요소의 클래스를 변경하지 않고도 새로운 오퍼레이션을 정의할 수 있게 한다.
구조
논의
이것은 객체지향 설계인가?
언뜻 보면 Visitor 패턴은 전형적인 객체지향 설계와는 상반된다. 전부는 아니겠지만 스몰토크 또는 다른 객체지향 언어로 프로그래밍을 하는 우리 중 대부분은 절차적 언어로 작업한 적이 있을 것이다. 객체지향 방식으로 일을 처리하는 방법을 배울 때 우리는 비활성 데이터에 절차지향적 코드를 실행하는 것은 "잘못된" 혹은 객체지향 설계에 관한 잘못된 사고방식이라고 배웠다. 문제는 행위에 대한 책임을 적절한 곳에 할당하는 것과 관련된다 (read, object). 객체지향 설계의 개념과 쓰기를 내면화(internalize)를 하고 나면 수동 엔티티로서 객체에서 작용하는 어떤 외부 코드보다는 그 객체의 행위 레퍼토리에 객체가 포함되는 서비스가 더 자연스러워 보인다. 이는 관련 메시지가 그 객체의 클래스 정의에 포함되어야 하며 객체는 그 과정에 능동 참가자여야 함을 암시한다. Wirfs-Brock, Wilkerson, Wiener (1990)가 주장한 바와 같이, "객체가 특정 정보를 유지해야 할 책임이 있다면 그 정보에 필요한 어떠한 오퍼레이션이든 그것을 실행할 책임 또한 할당하는 것이 옳다" (p.65).
Brad Cox (1986)는 영향력이 큰 그의 저서를 통해, 여러 타입의 객체들 상에 절차를 실행해야 할 경우, 수동 객체로서 객체 상에 작용하는 외적, 능동적 절차보다 객체 내부에 오퍼레이션을 포함하는 것이 더 나은 이유를 설명한다. 연관은 있으나 서로 다른 클래스에 오퍼레이션을 다형적으로 구현함으로써, 객체의 타입을 검사하여 그에 따라 다른 코드를 실행하는 case 문 또는 조건문을 피할 수 있다는 것이 요점이다. 구체적인 예를 들어보겠다.
여러 타입의 그래픽 객체를 그릴 수 있는 그림 편집기가 있다고 가정하자; 직사각형, 타원, 폴리라인을 그릴 수 있다. 그림 편집기가 객체를 수정해야 할 경우 절차적 프로그래밍 방식으로 다음과 같이 수행할 수 있다:
DrawingEditor>>refresh
"Don't do it this way!"
graphicElements do: [:anElement |
(anElement isMemberOf: Rectangle)
ifTrue: ["code to draw a rectangle"]
ifFalse:
[(anElement isMemberOf: Ellipse)
ifTrue: ["code to draw an ellipse"]
ifFalse:
[(anElement isMemberOf: PolyLine)
ifTrue: ["code to draw a polyline"]
...
이후에 새로운 타입의 그래픽 객체를 추가하고자 할 때 (예: 다각형) 우리는 그러한 조건 코드를 모두 찾아 새로운 조건문에서 붙여서 새로운 타입을 검사해야 한다.
좀 더 객체지향적 방식으로 구현하자면, 그래픽 객체를 나타내는 각 클래스에 draw 메서드를 정의하는 수도 있다. 각 클래스 내의 draw 메서드는 그 클래스의 객체를 어떻게 그리는지 정확히 안다. 따라서 다음과 같이 정의할 것이다:
Rectangle>>draw
"I'm a rectangle; here's the code to draw me."
...
Ellipse>>draw와 PolyLine>>draw를 비슷하게 부호화할 것이다. 이제 그림 편집기에서 수정 코드는 다음과 같다:
DrawingEditor>>refresh
graphicElements do: [:anElement | anElement draw]
새로운 그래픽 객체 클래스를 추가할 경우 수정 코드를 변경할 필요가 없다. 대신 각 그래픽 객체 클래스가 draw 메시지를 지원하기만 하면 된다.
하지만 이 접근법에도 몇 가지 결점이 있다. 첫째, 누구도 객체지향과 관련된 문헌에서 이 문제를 다루고 싶어 하지 않지만ㅡ누구도 다룬 적이 없다ㅡ모든 클래스 계층구조에 걸쳐 draw 코드를 분산시키는 것은 프로그램 이해 측면에서 문제가 된다. 객체지향 초보 프로그래머가 이 코드를 이해하기 위해 어떤 노력을 하는지 보라. 그들은 모든 조각을 찾아서 발생하는 일에 대해 일관성 있는 내적 모형을 구성하느라 어려움을 겪을 것이다. draw를 구현하는 모든 클래스가 하나의 계층구조 가지에 속한다면 수월할 테지만 (예: 추상 슈퍼클래스 geometric에 Rectangle이나 Ellipse와 같은 구체적 기하학 모양을 나타내는 서브클래스로 이루어진) 클래스가 불연속적인 경우 힘들 것이다.
두 번째 결점은, 그래픽 요소 클래스를 포함하는 오퍼레이션의 구현 및 책임을 각 클래스로 분배할 때 새로운 오퍼레이션의 추가가 더 힘들다는 점이다. 이는 모든 Geometric 클래스에 새로운 메서드를 추가해야 함을 의미한다. 따라서 이 해법은 기본적으로 오퍼레이션 집합이 정적일 경우 효과적이다.
Visitor 해법
여태까지 두 가지 가능한 접근법을 살펴보았다: 그림을 그리는 객체에 내재된 draw 오퍼레이션으로 각 그래픽 객체 클래스에 책임을 분산하는 방법과 그림 편집기에 조건문을 이용하는 방법이다. Visitor 패턴은 3번째 대안책을 제시한다. Visitor 해법에서는 모든 그림 코드가 (모든 유형의 그래픽 객체 그리기에 요구되는 코드) 모든 그래픽 객체 클래스로부터 분리되어 하나의 드로잉 Visitor 클래스에 위치한다. 그래픽 객체를 그려야 할 경우 드로잉 Visitor 인스턴스로 업무가 위임된다. 실제적 작업은 Visitor가 모두 수행하는데, 아마도 그래픽 객체로 정보를 요청할 것이다.
따라서 패턴은 절차적 프로그래밍 세계에서와 같이 마치 수동 엔티티인 것처럼 행동하는 객체 상에 실행되는 외부 코드를 갖고 있지만 사실은 별로 그렇지 않다: 그래픽 객체 상에서 실행되도록 적절한 코드를 호출하는 것은 객체 자신이다. 그림 편집기가 그래픽 객체를 그려야 할 경우 편집기는 Visitor가 아니라 그래픽 객체로 메시지를 전송한다. 그리고 나면 그래픽 객체는 단순히 Visitor로 메시지를 전송할 뿐이며, 이 메시지는 그래픽 객체의 클래스를 부호화하여 결국 Visitor에게 무슨 일을 해야 할지 알리는 셈이다. 다음 페이지에서 구체적 예를 살펴보자:
DrawingEditor>>refresh
"Redraw all the elements in the drawing"
| visitor |
visitor := DrawingVisitor for: self.
graphicElements do: [:aGraphic |
aGraphic acceptVisitor: visitor]
Rectangle>>acceptVisitor: aVisitor
aVisitor visitRectangle: self
Ellipse>>acceptVisitor: aVisitor
aVisitor visitEllipse: self
각 그래픽 객체에 의해 전송된 메시지는 클래스 자체에 대한 정보를 모두 포함한다는 사실을 주목하자. 우리는 DrawingVisitor에서 방문 가능한 클래스마다 하나의 메서드를 가진다: visitEllipse:, visitRectangle:, visitPolyline: 이 그것이다. 이 메서드 각각은 어떻게 구체적 클래스의 그래픽 객체를 그리는지 알고 있다.
그렇다면 우리가 얻은 것은 무엇인가? 우리는 구분된 그림 알고리즘ㅡ그림 편집기는 각 그래픽 객체로 단일 메시지를 전송한다ㅡ중에서 선택 시 case와 같은 문의 사용을 피한다. 그리고 모든 그림 코드를 여러 개의 위치보다는 (각 그래픽 객체 클래스에 다수의 draw 메서드) 하나의 위치에 모듈화하였다.
또한 다른 오퍼레이션들은 그림뿐만 아니라 그래픽 객체도 수반한다. 그래픽 요소에서 실행되는 새로운 오퍼레이션을 추가하고 싶다면, 그 오퍼레이션을 하나의 위치로 모으는 또 다른 Visitor 클래스를 부호화할 수 있다. 그래픽을 데이터베이스로 저장하는 법을 알고 있는 Visitor도 가질 수 있다; 변환 오퍼레이션ㅡ수평 또는 수직으로 기울기, 회전, 비스듬히 움직이기, 격자 선에 맞추기ㅡ을 선택된 그래픽 객체 집합에서 실행할 수도 있다; 컴퓨터지원 드로잉(CAD) 컨텍스트에서는 그래픽 객체가 부품 번호와 연관될 수 있으며, Visitor로 하여금 Bill of Materials의 인쇄와 조립된 제품의 비용 계산, Purchased Order 애플리케이션 및 Inventory 시스템으로 일부 정보를 전송할 수도 있다. 그 결과, 어떤 식으로든 그래픽 객체 클래스를 변경하지 않고 그래픽 객체 상에서 새로운 오퍼레이션을 추가할 수 있게 된다. 각 클래스가 acceptVisitor: 를 구현하고 나면 우리는 같은 방식으로 어떠한 종류의 Visitor든 호출할 수 있다:
CADEditor>>printBOM
| visitor |
visitor := BillOfMaterialsVisitor for: self.
graphicElements do: [:aGraphic |
aGraphic acceptVisitor: visitor]
클래스 외부에 위치한 클래스 상에서 오퍼레이션을 실행하는 또 다른 이유가 있다 (DP 333; Liu, 1996 참조). 여러 애플리케이션용으로 만들어진 애플리케이션 특정적 메서드를 클래스 자체 내에서 구현할 경우 클래스 정의를 복잡하게 만드는 위험을 감수해야 한다. 이는 클래스가 특히 일반적 목적의 (애플리케이션 특정적 또는 도메인 특정적이 아닌) 객체를 정의할 때 그러하다. 스몰토크 언어 문장에 대한 파스 트리의 노드를 정의하는 클래스 집합이 있다고 가정해보자 (AssignmentNode 클래스, VariableNode 클래스, MessageNode 클래스 등 비주얼웍스에서 사용되는 클래스). 이러한 클래스들은 주로 컴파일 과정에 사용된다. 스몰토크 컴파일러는 스몰토크 소스문을 노드 트리로 파싱하여 (분해) 스몰토크 가상머신용 바이트코드를 생성하기 위해 파스 트리로 반복된다. 하지만 누군가는 파스 노드의 트리를 스몰토크->C++ 번역 프로그램용으로 사용하기로 결정할 수도 있다. 또 스몰토크->자바 번역기로 구현하길 원하는 사람도 생길 것이다. 그렇다면 문제는 "이렇게 여러 애플리케이션용으로 만들어진 애플리케이션 특정적 코드를 상대적으로 단순한 노드 클래스에 두길 원하는가?" 가 된다. 대신 Smalltalk-to-C++ Visitor 또는 Smalltalk-to-Java Visitor에서 파스 트리의 각 노드를 방문하여 애플리케이션 특정적 오퍼레이션을 실행하는 것과 같은 오퍼레이션을 가질 수도 있다. (용도는 다르지만 비주얼웍스에서도 스몰토크의 파스 트리 상에 Visitor 패턴을 사용한다; 알려진 스몰토크 사용예 참조).
이중 디스패치
[디자인 패턴]편에서 지적하였듯 Visitor는 이중 디스패치(double dispatch) 형태를 구현한다. 이중 디스패치가 어떻게 작용하는지 설명하기 전에 이것이 해결하는 문제를 살펴보자. 가끔씩 메서드의 행위가 메서드를 구현하는 클래스 뿐 아니라 메서드의 아규먼트에 대한 클래스에 따라서도 좌우되는 상황이 있다. 특히 메서드가 하나의 아규먼트를 가진 경우를 살펴보자. 비주얼 스몰토크에서 Point 에 추가하기 위한 이미지 코드는 다음과 같다:
Point>>+ delta
"delta can be a Number or a Point."
^delta isPoint
ifTrue: [(x + delta x) @ (y + delta y)
ifFalse: [(x + delta) @ (y + delta)
이를 살펴보면 알고리즘이 + 메시지 수신자에 의존하기보다는 수신자와 (Point) 아규먼트 (Number 또는 Point) 모두의 함수임을 알 수 있다. 여기서 가능한 아규먼트 타입은 두 개의 클래스에 속한 인스턴스를 포함한다 (물론 그들의 서브클래스도 포함한다: Number는 추상 클래스이며 아규먼트는 사실상 Integer, Float 등이 될 것이다). 따라서 아규먼트의 클래스를 검사하기 위한 조건문을 가지고 그에 따라 서로 다른 행위를 실행하는 데에는 사실상 문제가 없다 (이러한 검사를 실행해선 안 된다고 믿는 순수주의자도 있긴 하지만). 하지만 가능한 아규먼트 타입이 많다고 가정해보자. 우리는 Number 또는 Point를 감싸는 Adapter를 가질 수 있다 (Adapter (105) 패턴 참조). Adapter로부터 내장 객체를 검색하기 위해서는 value 메시지를 전송해야 할 필요가 있다. 따라서 다음과 같은 코드가 필요하다:
Point>>+ delta
"delta can be a Number or a Point or an Adapter on a
Number or a Point."
| addend |
addend := delta isAdapter
ifTrue: [delta value]
ifFalse: [delta].
^addend isPoint
ifTrue: [(x + addend x) @ (y + addend y)
ifFalse: [(x + addend) @ (y + addend) ]
물론 아규먼트의 유형을 추가로 허용하기 때문에 + 메서드는 점점 더 복잡해진다. 그렇다면 이를 어떻게 극복할 수 있을까? 답은 이중 디스패치 방법을 사용하는 것이다. 두 번째 메시지를 + 메서드의 아규먼트로 발송하여 원래 수신자를 두 번째 메시지의 아규먼트로서 전달한다. + 메서드는 다음과 같은 모습을 띨 것이다:
Point>>+ delta
"delta can be anything that understands 'addPoint:'."
^delta addPoint: self
이제 Point>>+에게 인스턴스가 아규먼트로 나타나는 모든 클래스로 책임을 전가했다. Visitor 패턴에서와 같이, 재발송된 메시지가 그 아규먼트를 클래스에 어떻게 알리는지 주목하자: 어떠한 addPoint: 메서드 구현부에서든 우리는 파라미터가 Point임을 알고 있으므로 추가적 클래스 검사가 불필요하다. 따라서 두 번째 메서드의 코딩은 단순해진다.
이제 다음과 같은 추가 메서드가 필요하다:
Number>>addPoint: aPoint
^(self + aPoint x) @ (self + aPoint y)
Point>>addPoint: aPoint
^(self x + aPoint x) @ (self y + aPoint y)
Adapter>>addPoint: aPoint
^self value addPoint: aPoint
이중 디스패치 해법을 구현하기 위해 우리는 더 많은 메서드를 작성했지만 코드는 더 이상 쓰지 않았다. 더 중요한 것은 시스템을 확장하는 것이 훨씬 수월해졌다는 사실이다. 클래스 Zot의 인스턴스를 Point에 추가하고 싶을 경우 Point 클래스 내의 + 메서드를 전혀 변경할 필요가 없다. 대신 Zot>>addPoint: 를 구현하기만 하면 된다. 단 한 가지 잠재적 결점이 있다면ㅡ다시 말하지만 스몰토크 초보 프로그래머들에게 특히 문제가 된다ㅡ이중 디스패치를 사용하지 않을 경우 Point에 추가하기 위한 모든 코드는 Point>>+ 라는 하나의 위치에 존재해야 하는 반면, 이중 디스패치를 구현할 경우 코드는 여러 개의 클래스로 분산이 가능하다는 점이다; 따라서 일부 사용자들에게는 이 행위를 위치시켜 이해하기가 까다로울 수 있다.
이중 디스패치에 대한 개념을 다른 용어로 설명한 이들도 있다. Liu (1996)는 이 패턴을 Duet 또는 Pas de Deux라고 불렀다. Ingalls (1986)는 재발송된 메시지를 "릴레이 메시지" 라고 부른다. Ingalls는 또한 Visual 패턴이 처리하는 일반적인 문제, 즉 본래 메시지와 전달된 메시지가 다수의 클래스에 의해 다형적으로 구현되는 문제를 다룬다. 이중 디스패치에 관한 자세한 내용은 Liu의 훌륭한 저서와 Ingalls의 논문, Beck (1997), 그리고 [디자인 패턴] (DP 338-339)를 참조한다.
협력 방법
다음은 참가자 객체들 간 상호작용을 설명하는 협력 다이어그램이다. Visitor가 자신의 일을 수행하는 과정에서 자신의 Element로 메시지를 전송하는지 여부를 주목하자. 다음 그림에서 aVisitor는 ConcreteElementA 객체를 처리할 때 operationA 메시지를 전송하지만 ConcreteElementB 인스턴스를 작업할 경우 Visitor의 알고리즘이 “자립적”이므로 Element 내 행위를 불러오지 않아도 된다.
활용성
Visitor 패턴은 어떠한 상황에서 적용할까? 이에 대한 답은 많은 디자인 패턴에 흥미롭게 반영된 것으로 밝혀졌다. 때로는 설계에 특정 디자인 패턴을 포함하지 않을 때가 더 낫기도 하지만 애플리케이션이 발달하면서 그 패턴이 오히려 적용 가능해지거나 유익해지는 경우가 있다. 이것이 Visitor 패턴의 경우이다. Visitor 패턴을 설계에 적용할 것인지 결정 시 수반되는 문제를 살펴보자.
Visitor 패턴을 사용해선 안 되는 경우
Visitor 패턴은 실행 중인 (Element 클래스에서) 클래스 집합이 안정적일 때만 사용해야 한다; 즉 오퍼레이션 집합이 확장될지라도 새로운 오퍼레이션을 추가할 기회가 거의 없다. 따라서 패턴은 어느 정도 편향된 확장성을 허용한다. 이로 인해 새로운 오퍼레이션을 추가하기가 쉽다; 새로운 오퍼레이션을 추가 시 우리는 다수의 위치보다는 (실행 중인 클래스들에게 분산된 메서드) 하나의 위치에 (새 클래스) 추가한다. 반대로 새로운 Element 클래스를 추가하기는 어려워진다 (예: 새로운 Gemetric 서브클래스). Curve 클래스를 추가한다고 가정해보자. 가장 먼저 우리는 다음과 같은 acceptVisitor: 메서드를 확실히 추가할 필요가 있다:
Curve>>acceptVisitor: aVisitor
aVisitor visitCurve: self
이것은 쉽다. 그리고 나서 기존 Visitor 클래스가 visitCurve: 메서드를 추가할 때마다 우리는 재방문을 할 필요가 있다. 이들 각 메서드는 Curve 객체에 대한 적절한 알고리즘을 (Visitor 타입에 따라) 구현해야 한다; 예를 들어, DrawingVisitor>>visitCurve: 는 곡선을 그리기 위한 코드를 구현할 것이다. 이 과정은ㅡ다수의 구분된 장소에 여러 개의 메서드를 추가하는ㅡVisitor 패턴의 사용을 통해 피할 수 있는 일들 중 하나이다. 따라서 Visitor 패턴은 새로운 오퍼레이션을 추가하고 싶은 경우에만 적용 가능하지만 새로운 Element 클래스를 추가해야 할 때는 적용하기가 힘들다.
패턴이 적용되었는지 확인하는 방법은?
앞의 요점은 더 많은 고찰이 필요하다. 위 단락에서 우리는 Element 클래스 집합이 안정적일 때 Visitor 패턴을 선택해야 한다고 주장했지만, 이러한 Element 상에 새로운 오퍼레이션을 가진 시스템으로 향상시키길 원할 수도 있다. 사실 이에 상반되는 상황이 옳다는 언급을 한 적이 있다: 오퍼레이션을 각 Element 클래스에 분배하는 방법은 오퍼레이션 집합이 기본적으로 상수일 때 더 적절한 접근법이지만 추후에 새로운 Element 서브클래스를 추가하고자 하는 경우가 있다. 그렇다면 문제는 애플리케이션 또는 프레임워크를 처음 설계할 때 앞으로 어떤 설계 문제를 경험할 것인지 미리 알지 못하는 경우가 있다는 것이다. "시스템이 실행되어 여러 번의 확장을 거친 후에야 Visitor과 분산 알고리즘 중 어떤 것을 사용할지 명확해질 것이다" (Roberts, Brant, & Johnson, 1997a, p.15).
이는 다수의 디자인 패턴을 사용할 때 경우이다: 항상 특정 패턴을 우선하여 적용시킬 수는 없는 노릇이고, 시스템을 향상시키기 위해 기존 시스템에 설계 패턴을 새로 제공해야 하는 경우도 있다. 이는 디자인 패턴의 애플리케이션에 중요한 점이다: 설계는 시스템이 구축되기 전에 완성되는 것이 아니다. 설계는 보통 시스템의 수명에 걸쳐 발생하는 진행 중인 과정이다: 구현되기 전에, 구현 되는 동안 진행되며, 그 요구 사항은 관리 시, 향상 시에 더 많이 배운다. 이는 모두 디자인 패턴을 문제에 쉽게 적용시킬 수 있는 기회이다.
재컴파일을 피하라?
C++에서 Visitor 패턴을 사용하는 또 다른 이유는 Element 클래스에 새 메서드를 (새로운 오퍼레이션을 위해) 추가하는 것은 전체 클래스를 재컴파일해야 함을 의미하기 때문이다 (DP 331). 그리고 모든 Element 클래스에서 동일한 오퍼레이션을 필요로 하는 경우 우리는 모든 클래스를 재컴파일 해야 할 것이다. 이는 시간이 많이 소모되는 작업이다; 최악의 경우는 전체 클래스에 대한 소스 코드를 소유하지 않는 경우로써 재컴파일이 불가능하다.
이는 스몰토크에서는 전혀 문제가 되지 않는다. 스몰토크 대화형 개발환경의 코딩 도구가 증분 컴파일을 제공하기 때문이다. 새로운 메서드를 쓰고 저장할 때 메서드는 동일한 클래스 내에 이전에 컴파일된 다른 메서드에 영향을 미치지 않고 즉시 컴파일된다. 따라서 클래스에 새 메서드를 추가하더라도 재컴파일에 영향을 미치지 않음을 의미한다.
구현
Visitor 를 호출하는 대안적 방법에 관한 다음 단락 이외에도 알려진 스몰토크 사용예 단락에서 누가 객체 구조를 순회할 책임을 지는지를 다룬, Enumerating ProgramNodes Again 예제를 참조한다. 후자는 구현 문제지만 기존 Visitor 예제의 컨텍스트에서 표현하였다.
저장 단계
C++와 상반되는 또 다른 문제는 클래스 정보의 런타임 검색과 관련된다. 반복 대상이 되는 Element 객체로 acceptVisitor: 메시지를 전송하는 이유는 (Visitor에게 Element 상에서 실행할 작업을 알려주는 메시지를 직접 전송하는 대신) 그 Element가 어떤 Visitor 메서드를 불러올 것인지 결정할 수 있도록 하기 위함이다; 이러한 결정은 Element의 클래스를 바탕으로 한다. 즉 Visitor에서 어떤 메서드를 불러올지에 대한 결정은 Element로 위임된다. 따라서 클라이언트 애플리케이션은:
graphicalElements do: [:anElement ]
anElement acceptVisitor: drawingVisitor]
대신 다음을 실행한다:
graphicalElements do: [:anElement |
drawingVisitor operateOn: anElement ]
각 Element 클래스 내의 모든 acceptVisitor: 메서드가 하는 일은 Visitor 에게 하나의 메시지를 전송하는 일인데, 이 메시지는 Element 클래스를 그 이름에서 부호화하여 Visitor에게 "나를 위해 이 메서드를 사용하라" 라고 알려준다. 따라서 Rectangle>>acceptVisitor: 는 visitRectangle: 메시지를 전송하여 Visitor에게 Rectangle 메서드를 사용할 것을 알리고, visitEllipse: 는 Ellipse에 대한 메서드를 불러온다. 하지만 스몰토크에서는 런타임 시 Element의 클래스를 결정할 수 있으므로 두 가지 방법 중 하나를 이용해 문제를 단순화시킬 수 있다.
패턴 나머지는 있는 그대로 놔두고 클라이언트가 (우리 예제에서는 DrawingEditor) Element들을 하나씩 열거하여 각각에게 Visitor 객체를 전달할 수 있도록 한다. 하지만 acceptVisitor: 코드를 훨씬 더 쉽게 만들 수도 있다. 여태까지 우리가 부호화한 것을 살펴보면 추상적 Element 슈퍼클래스는 acceptVisitor: 를 구현하여 오류 신호를 보내거나 (subclassResponsibility 또는 implementedBySubclass) 아무 일도 하지 않으며, 각 구체적 Element 서브클래스는 특정 콜백 메시지를 (예: visitRectangle) 하드코딩한다. 대신 슈퍼클래스에서 단일 acceptVisitor: 메서드를 부호화할 수도 있다:
Geometric >>acceptVisitor: aVisitor
aVisitor
perform: ('visit', self class symbol, ':') asSymbol
with: self
하위계층구조의 최상위 클래스에서 이 코드를 이용함으로써 Geometric의 서브클래스들은 어떠한 특수화된 "accept visitor" 메서드도 구현할 필요가 없어진다. 콜백 메서드 선택기는 일반적으로 구현되어 여전히 수신자의 클래스를 부호화하고 올바른 Visitor 메서드를 호출한다.
이러한 접근법의 첫 번째 결점은 하드코딩된 메시지보다 처리가 느리다는 것이다. 문자열 연결을 사용하고, String을 Symbol로 변환하며, perform:with: 를 이용해 결과 메시지를 전송하는 오퍼레이션들은 모두 상대적으로 속도가 느리다. 하지만 유연한 편에 속하며, 새로운 클래스가 추가 시 "accept visitor" 메서드를 부호화하지 않아도 (또는 잊어버리기도) 된다.
더 중요한 점은 이러한 특성의 코드가 프로그램의 이해 측면에서 볼 때 까다롭다는 점이다. 다른 패턴에서도 논한 바 있지만 반복(reiterate)을 위해 Geometric>>acceptVisitor: 가 어떤 메시지를 전송하는지는 코드를 읽어서는 알 수 없으며, 일반적으로 스몰토크 대화형 개발환경의 매우 유용한 도구들은 없어졌다. Geometric>>acceptVisitor: 메서드에 "메시지" 툴을 사용할 경우, 결과 리스트는 메서드가 전송한 실제 visit . . . 메시지를 포함하지 않을 것이다; 예를 들어 DrawingVisitor>>visitEllipse: 의 "senders"를 보면 Geometric>>acceptVisitor: 를 찾을 수 없을 것이다.
이 일반 접근법을 사용하기로 결정하였다면 두 번째 대안 방법이 있는데, acceptVisitor: 메서드와 Element-Visitor 양방향 프로토콜을 모두 생략하는 방법이다. 클라이언트가 Element를 반복하여 그들에게 acceptVisitor: 메시지를 전송하는 대신 Visitor 패턴을 이용해 스스로 메서드 호출을 결정하도록 만들 수도 있다. 가장 먼저 클라이언트 코드는 다음과 같이 약간 변경될 것이다:
DrawingEditor>>refresh
| drawingVisitor |
drawingVisitor := DrawingVisitor for: self.
"Iterate over the graphics, passing each
TO the drawing visitor:"
graphicalElements do: [:anElement |
drawingVisitor operateOn: anElement]
그리고 나서 Visitor 클래스에 (혹은 Visitor 전체 계층구조의 추상적 슈퍼클래스에) 하나의 일반 operateOn: 메서드가 필요하다:
GeometricVisitor>>operateOn: anElement
self
perform: (visit, anElement class symbol, ':') asSymbol
with: anElement
예제 코드
draw 예제로 다시 돌아가, 두 개의 관련 계층구조가 있다. 첫 번째는 그래픽 객체 클래스의 집합으로서 다음 다이어그램과 같다:
이러한 그래픽 클래스 상에 실행하기 위한 Visitor 클래스 집합이 있다:
이러한 클래스에 대한 정의의 예제는 다음과 같다:
Object subclass: #Geometric
instanceVariableNames:
'lineStyle lineWidth lineColor fillColor'
classVariableNames: ''
poolDictionaries: ''
Geometric subclass: #PolyLine
instanceVariableNames: 'vertices'
classVariableNames: ''
poolDictionaries: ''
Geometric subclass: #Rectangle
instanceVariableNames: 'leftTop rightBottom'
classVariableNames: ''
poolDictionaries: ''
Object subclass: #GeometricVisitor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
GeometricVisitor subclass: #DrawingVisitor
instanceVariableNames: 'pen'
classVariableNames: ''
poolDictionaries: ''
DrawingVisitor>>pen: aPen
"Set up the pen I will use to do my drawing"
pen := aPen
DrawingVisitor가 pen 인스턴스 변수들을 포함한다는 사실을 주목한다. 그것의 클라이언트는 그림을 그리기 위해 사용할 실제 Pen을 전달한다. (비주얼 스몰토크의 경우) GraphicsMedium과 일부 패인은 (예: GraphPane) 그리기 펜을 포함하고 있으므로 DrawingVisitor를 Bitmap, Printer, 그 외 구체적 GraphicsMedium 서브클래스 뿐 아니라 GraphPane의 인스턴스와 그 서브클래스에 그림을 그리는데 재사용할 수 있다. 이는 하나의 Visitor 상에 실행되는 클래스들이 동일한 하위계층구조에 있을 필요가 없음을 보여준다.
그림 편집기가 일부의 또는 전체의 그래픽 객체에 오퍼레이션을 실행해야 하는 경우 제어의 흐름은 다음과 같다:
- 그림 편집기는 특정 오퍼레이션과 연관된 적절한 Visitor 클래스를 인스턴스화한다 (예: 드로잉 오퍼레이션의 경우 DrawingVisitor 인스턴스가 생성된다).
- 그림 편집기는 관련된 그래픽 요소들을 반복하여 각 요소에게 acceptVisitor: 메시지와 함께 Visitor 객체를 아규먼트로 취해 전송한다. 따라서 다음을 갖게 된다:
Object subclass: #DrawingEditor instanceVariableNames: 'graphicalElements graphPane' classVariableNames: '' poolDictionaries: '' DrawingEditor>>refresh | visitor | visitor := DrawingVisitor new. visitor pen: self graphPane pen. graphicalElements do: [:anElement | anElement acceptVisitor: visitor]
- 각 그래픽 요소의 acceptVisitor: 메서드는 한 가지 일만 한다: Visitor 객체를 다시 호출하는 일이다. Visitor 로 전송된 메시지는 메시지 이름에 그래픽 객체의 정확한 클래스를 부호화하므로 Visitor 내에서 그에 상응하는 관련 메서드를 호출한다. 메시지의 아규먼트는 그래픽 객체 자체이기 때문에 두 객체는 일시적으로 단단히 결합된다; 따라서 Visitor는 그래픽 객체로 메시지를 전송함으로써 Visitor의 오퍼레이션에 요구되는 그래픽 객체에 관한 정보는 무엇이든 얻을 수 있다:
Geometric>>acceptVisitor: aVisitor self implementedBySubclass "Visual Smalltalk" "self subclassResponsibility" "VisualWorks, IBM" Rectangle>>acceptVisitor: aVisitor aVisitor visitRectangle: self Ellipse>>acceptVisitor: aVisitor aVisitor visitEllipse: self PolyLine>>acceptVisitor: aVisitor aVisitor visitPolyLine: self
- Visitor는 적절한 알고리즘을 수행한다 (여기서는 드로잉이 되겠다). 다음 예는 비주얼 스몰토크에서 제공하는 Pen 메시지를 사용한다:
DrawingVisitor>>visitRectangle: aRectangle "Use the Rectangle's properties to set properties of my drawing Pen; then, draw the rectangle." Pen setPenStyle: aRectangle lineStyle "dashed/solid/etc." color: aRectangle lineColor width: aRectangle lineWidth; setFillColor: aRectangle fillColor; place: aRectangle leftTop; rectangleFilled: aRectangle rightBottom
DrawingVisitor>>visitPolyLine:과 DrawingVisitor>>visitEllipse: 도 이와 비슷하게 부호화한다.
알려진 스몰토크 사용예
코드 생성하기
비주얼웍스에서 스몰토크 메서드를 컴파일 할 시에는 먼저 ProgramNode의 트리로 파싱된다. 트리 루트는 MethodNode이다. MethodNode에게 CodeStream 상으로 코드를 전송할 것을 요청하면 이는 하위컴포넌트 노드의 재귀적 순회를 시작하여 전체 파스 트리를 전체적으로 순회하는 결과를 낳는다. 이러한 반복 과정에서 각 노드는 CodeStream 상으로 바이트코드를 보내달라는 요청을 받는다. 이러한 요청 결과, 노드는 노드 타입을 부호화한 메시지를 CodeStream으로 전송하게 된다. 호출된 노드 특정적인 CodeStream 메서드는 실제 코드를 생성한다.
ProgramNodes 를 다시 열거하기
ProgramNodes를 수반하는 또 다른 예제: ReadBeforeWrittenTester (ProgramNodeEnumerator의 서브클래스) 는 메서드 값이 설정되기 전에 메서드가 변수를 참조하는지 결정하기 위해 메서드의 파스 트리에 있는 ProgramNode를 반복한다. ReadBeforeWrittenTester는 각 ProgramNode로 (node nodeDo: self) 일반 메시지를 전송하며, 각 ProgramNode 서브클래스는 이 메시지를 구현하여 메시지를 ReadBeforeWrittenTester로 다시 이중 전달한다; 두 번째 메시지는 ProgramNode 객체의 클래스를 부호화한다ㅡ예를 들어:
AssignmentNode>>nodeDo: anEnumerator
^anEnumerator doAssignment: self ...
VariableNode>>nodeDo: anEnumerator
^anEnumerator doVariable: self ...
이 예제를 살펴보면 한 가지 질문, 즉 누가 객체 구조를 순회할 책임을 지는지에 관한 질문이 제기된다. 여기서는 제 3자 대신 Visitor가ㅡReadBeforeWrittenTesterㅡElement 구조를 하나씩 열거한다. 드로잉 예제에서는 그림 편집기가ㅡ객체 구조의 클라이언트ㅡ그래픽 요소들을 하나씩 열거하여 각 요소로 Visitor를 전달하였다; ReadBeforeWrittenTester 구현부에서는 ReadBeforeWrittenTester가 Visitor의 역할과 객체 구조의 순회 업무를 수행하여 각 Element로 "accept visitor"를 전송한다. 객체 구조 자체가 "순회자" 역할을 하기도 한다; 이 사례는 코드 생성하기의 알려진 사용예에서 살펴보았다.
스몰토크 리팩토링 브라우저
스몰토크 리팩토링 브라우저는 (Roberts, Brant, & Johnson, 1997a, 1997b) 시스템이 완벽해지는 과정에서 프로그래머가 실행하는 여러 프로그램 변환을 (리팩토링) 부분적으로 또는 전체적으로 자동화시킨다. 리팩토링은 하나 또는 다수의 클래스를 변경하여 재사용이 가능하고, 확장가능하며, 이해가 쉽도록 만드는 목적을 가진다. 리팩토링은 새로운 행위의 추가를 수반하진 않지만 디자인의 변환이나 기존 행위의 재배치를 수반한다. 예를 들어, 일반 리팩토링은 두 개 또는 그 이상의 형제 클래스에 중복된 전체 메서드나 코드를 권한다. 흥미롭게도 리팩토링 브라우저가 지원하는 변환 중 하나는 분산 오퍼레이션을 (다수의 Element 클래스에서 구현되는 오퍼레이션) Visitor 패턴 구현으로 변환하는 과정을 포함한다 (378페이지의 ‘패턴이 적용되었는지 확인하는 방법은?’ 단락을 참조).
Visitor 패턴은 리팩토링 브라우저의 구현부에서 여러 개의 장소에 사용된다. 브라우저의 리팩토링은 결국 (아마도 다수의) 메서드의 소스 코드를 변경하는 결과를 낳는다. 이러한 변경은 메서드의 텍스트로 된 소스의 변경으로 인해 직접적으로 영향을 받기보다는, 그 소스를 컴파일한 후 파스 트리를 텍스트로 역 파일하여 생성된 파스트리를 먼저 변경하였을 때 영향을 받는다. 여기에는 두 개의 Visitor 객체가 포함된다. 가장 먼저, 코드 변환은 패턴 매칭을 바탕으로 실행된다. 예를 들어, rename-메서드 리팩토링의 경우ㅡ메서드 length를 size로 이름을 변경한다고 치자ㅡ변환은 길이를 전송하는 기존 메서드를 모두 발견, 변경한 후 size 메시지를 대신 전송하는 과정을 포함한다. 이를 위해선 length 메시지를 검색하는 모든 메서드를 스캔해야 한다ㅡ메서드의 파스 트리를 다니면서 각 ProgramNode에게 이 패턴과 일치하는지 묻는 것은 Visitor 가 할 일이다. 메서드를 일치시킨 후 그 파스 트리는 변경되며, 두 번째 Visitor가 그 트리를 순회하면서 메서드의 텍스트적 소스 코드를 형성한다.
(스몰토크 리팩토링 브라우저를 위한 코드는 다음 주소에서 얻을 수 있다: http://st-www.cs.uiuc.edu/users/brant/Refactory/ ).
GraphicsContext 상에 VisualComponents 표시하기
비주얼웍스에서는 VisualComponent 계층구조 (예: PixelArray, Image), Geometric 계층구조 (예: Rectangle, LineSegment, Polyline, Circle), 그 외 비주얼 클래스 (예: CharacterArray, Mask) 내에 위치한 클래스의 인스턴스들에게 자신을 GraphicsContext 상에 (스크린 또는 프린터) 표시하도록 요청할 경우, 이 인스턴스들은 GraphicsContext로 작업을 요청한다. 실제 작업은 비주얼 객체로 전달되는 (메시지 아규먼트로서) GraphicsContext에 분배된다. GraphicsContext로 전송된 메시지는 그림을 그리고자 하는 비주얼의 클래스를 그 서명에 포함시킨다. GraphicsContext 하위계층구조 내 서로 다른 클래스들은 이러한 이중 디스패치된 메시지들에 대해 서로 다른 행위로 응답한다. 여기 몇 가지 구체적 예를 들자면 다음과 같다:
Image>>displayOn: aGraphicsContext at: aPoint
aGraphicsContext displayImage: self at: aPoint
Mask>>displayOn: aGraphicsContext at: aPoint
aGraphicsContext displayMask: self at: aPoint
PixelArray>>displayOn: aGraphicsContext at: aPoint
aGraphicsContext displayPixelArray: self at: aPoint
Rectangle>>displayFilledOn: aGraphicsContext
aGraphicsContext displayRectangle: self
관련 패턴
Visitor 패턴과 확실히 연관된 몇 가지 패턴이 있다. Visitor는 Elements의 구조에 대한 반복 과정 도중에 호출된다ㅡ따라서 Iterator (273) 패턴은 Visitor와 협동하여 주로 사용된다. 언어적 파스를 표현하는 것이 트리 구조인 경우도 종종 있지만ㅡ이런 경우 Visitor는 Interpreter (261) 패턴 구현에 참가한다ㅡ반복되는 (iterated) 구조는 Composite (137) 패턴일 가능성이 크다.