자바 디자인 패턴 16 - Visitor
1. Visitor 패턴은..
복잡한 구조체 안을 돌아다니면서 어떤 일을 해야 할 경우가 있습니다. Visitor는 어떤 구조체에 대해 그 안을 돌아다니면서 어떤 일을 하는 것입니다. 이 때, 구조체 1개에 하는 일이 딱 1개라는 보장은 없습니다. 하나의 구조체에 대해 다양한 일들을 할 수 있습니다. 하고 싶은 일이 추가된다고 해서 구조체를 변경하는 것은 무리입니다. 이런 때는 Visitor를 추가하면 됩니다. 예제에서는 PC의 디렉토리-파일 구조에 대해 야동을 찾는 일을 하는 Visitor를 구현해보았습니다.
2. 예제
--------- Component, Composite, Leaf 등은 Composite 패턴 설명에 썼던 것을 거의 그대로 사용했습니다. 바뀐 부분은 색깔 처리했습니다. 처리한 부분만 보시면 됩니다.
package ch16_Visitor;
import java.util.ArrayList;
import java.util.List;
public abstract class Component implements Acceptor{
public void accept(Visitor visitor) {
visitor.visit(this);
}
private String componentName;
protected List<Component> children = new ArrayList<Component>();
public Component(String componentName) {
this.componentName = componentName;
}
public String getComponentName() {
return componentName;
}
public abstract void add(Component c);
public List<Component> getChildren(){
return children;
}
}
package ch16_Visitor;
public class Composite extends Component {
public Composite(String componentName) {
super(componentName);
}
@Override
public void add(Component c) {
children.add(c);
}
}
package ch16_Visitor;
public class Leaf extends Component{
public Leaf(String componentName) {
super(componentName);
}
@Override
public void add(Component c) {
throw new UnsupportedOperationException();
}
}
------------------ Visitor를 받아들일 수 있는 구조체 ----------------
package ch16_Visitor;
public interface Acceptor {
void accept(Visitor visitor);
}
------------------ Acceptor를 방문하는 Visitor----------------
package ch16_Visitor;
public interface Visitor {
void visit(Acceptor acceptor);
}
------------------ 야동 찾는 YadongFinder ----------------
package ch16_Visitor;
import java.util.ArrayList;
import java.util.List;
public class YadongFinder implements Visitor {
private List<String> yadongList = new ArrayList<String>();
private List<String> currentList = new ArrayList<String>();
public void visit(Acceptor acceptor) {
if (acceptor instanceof Composite) {
Composite composite = (Composite) acceptor;
currentList.add(composite.getComponentName());
List<Component> children = composite.getChildren();
for (Component component : children) {
component.accept(this);
}
currentList.remove(currentList.size()-1);
}else if (acceptor instanceof Leaf) {
Leaf leaf = (Leaf) acceptor;
doSomething(leaf);
}
}
protected void doSomething(Leaf leaf){
if (isYadong(leaf)) {
String fullPath = getFullPath(leaf);
yadongList.add(fullPath);
}
}
protected String getFullPath(Leaf leaf) {
StringBuilder fullPath = new StringBuilder();
for (String element : currentList) {
fullPath.append(element).append("\\");
}
return fullPath.append(leaf.getComponentName()).toString();
}
private boolean isYadong(Leaf leaf) {
return leaf.getComponentName().endsWith(".avi");
}
public List<String> getYadongList() {
return yadongList;
}
}
------------------ 테스트 클래스 ----------------
package ch16_Visitor;
public class Test {
public static void main(String[] args) {
Composite main = createComposite();
YadongFinder visitor = new YadongFinder();
visitor.visit(main);
for (String string : visitor.getYadongList()) {
System.out.println(string);
}
}
private static Composite createComposite() {
Composite main = new Composite("C:");
Composite sub1 = new Composite("Program Files");
Composite sub2 = new Composite("WINDOWS");
Composite sub11 = new Composite("Pruna");
Composite sub21 = new Composite("system32");
Composite sub111= new Composite("Incoming");
Leaf leaf1111 = new Leaf("강호동 닮은여자-짱이쁨.avi");
Leaf leaf1112 = new Leaf("EBS야동특강.avi");
Leaf leaf211 = new Leaf("야메떼-다이조부.avi");
Leaf leaf212 = new Leaf("이건 야동아님.jpg");
main.add(sub1);
main.add(sub2);
sub1.add(sub11);
sub2.add(sub21);
sub11.add(sub111);
sub111.add(leaf1111);
sub111.add(leaf1112);
sub21.add(leaf211);
sub21.add(leaf212);
return main;
}
}
---------------- 테스트 결과 -------------
C:\Program Files\Pruna\Incoming\강호동 닮은여자-짱이쁨.avi
C:\Program Files\Pruna\Incoming\EBS야동특강.avi
C:\WINDOWS\system32\야메떼-다이조부.avi
위의 예제에서 중요한 것은 Visitor의 visit(Acceptor) 와 Acceptor의 accept(Visitor) 입니다.
Visitor는 visit(Acceptor) 를 가지고 있고, Acceptor는 accept(Visitor) 를 가지고 있습니다. 둘의 차이가 헤깔립니다. 게다가 accept(Visitor) 를 구현해 놓은 것을 보면 아래와 같이 그냥 Visitor한테 자기 자신을 던져버리는 게 하는 일의 전붑니다.
public void accept(Visitor visitor) {
visitor.visit(this);
}
이렇게 해 놓은 이유는 구조체를 돌아다닐 수 있게 하기 위한 것입니다. 실제로 구조체를 돌아다니는 일은 Visitor에서 담당하게 됩니다. 만약 이렇게 해놓지 않았다면, Visitor 안에서 구조체를 돌기 위해 재귀적인 호출을 해야만 복잡한 구조를 다 돌 수 있습니다. YadongFinder의 visit(Acceptor) 를 보면, 재귀적인 호출은 없습니다. 일반적으로 accept(Visitor)의 구현은 위와 같으며 달라질 일이 거의 없습니다.
Visitor의 visit(Acceptor)나 Acceptor의 accept(Visitor) 중 하나는 구조체를 도는 역할을 해야합니다. 구조체를 도는 역할은 Visitor의 visit(Accept)에 맡기는 것이 좋습니다. 구조체를 돌면서 하는 일 뿐만 아니라 "구조체를 도는 방법"도 다른 게 할 수도 있기 때문입니다. 위의 예제에서 YadongFinder는 아무래도 엄마가 짠 것 같습니다. 만약에 아들이 짰다면 구조체를 돌 때 "C:\Program Files\Pruna\Incoming\" 과 같은 비밀스러운 디렉토리는 슬쩍 뛰어넘는 로직을 짤 수 있었겠지요.
그러나 일반적으로 순화하는 로직은 거의 바뀌지 않습니다. 위의 예제에서는 YadongFinder를 상속 받아 doSomething(Leaf) 만 override 하면 뭔가 새로운 Visitor를 만들 수 있습니다.
3. 기타
visit(Acceptor) 안 쪽에서 instance of 로 어떤 클래스인지를 찾아냅니다. 이것은 visit(Composite) 와 visit(Leaf)로 분리시켰더라면 굳이 instance of를 쓸 필요가 없었을 것입니다. 하지만, 그렇게되면 Acceptor라는 인터페이스가 무의미해집니다.
Visitor와 Acceptor는 매우 밀접하게 묶여있습니다. 하지만 사실 Visitor의 구현체와 구조체 사이도 꽤 끈끈하게 묶여있습니다. YadongFinder 안에서 instance of로 체크한 것은 전부 구조체에서 정의된 클래스들(Composite, Leaf) 입니다.
테스트 코드의 YadongFinder를 선언하는 부분을 보면,
YadongFinder visitor = new YadongFinder(); 와 같이 되어있습니다. 왜
Visitor visitor = new YadoingFinder(); 라고 인터페이스로 정의를 하지 않았을까요?
YadongFinder의 getYadongList() 부분이 포인트입니다. visitor는 구조체를 돌아다니면서직접 일을 할 수도 있습니다.( 예를들어, YadongFinder를 조금만 수정하면 YadongRemover 로 만들 수도 있습니다.) 하지만 돌아다니면서 뭔가를 수집하는 것과 같이 직접 구조체를 수정하지 않고 단지 정보만 수집하는 경우가 있는데, 그런 경우는 수집한 정보를 다시 뽑아서 사용할 수 있는 방법을 제공해야 합니다. Test 클래스에서 야동 리스트를 찍는 부분을 보시면 됩니다.