본문 바로가기

Design Pattern

[Java-Pattern] Visitor

 

Composite 패턴과 조금 유사한 형태이다. 데이터 구조와 처리를 분리시킨 패턴으로 데이터 구조안을 돌아다니는 주체인 방문자를 나타내는 클래스를 준비해서 그 클래스에게 처리를 맡기는 패턴이다. 새로운 처리를 추가하고 싶을때는 새로운 방문자를 만들면 되는 것. 그리고 데이터 구조는 문을 두드리는 방문자를 받아들이면 된다.


디렉토리 구조를 다시 예제 프로그램으로..

8개의 클래스로 분류된다.

1.Visitor - 파일이나 디렉토리를 방문하는 방문자를 나타내는 추상 클래스

2.Acceptor - Visitor 클래스의 인스턴스를 받아들이는 데이터 구조를 나타내는 인터페이스

3.ListVisit - Visitor의 하위 클래스로 파일이나 디렉토리의 일람을 나타내는 클래스

4.Entry - 파일과 디렉토리의 상위 클래스가 되는 추상 클래스 (Acceptor 인터페이스 구현)

5.File - 파일을 나타내는 클래스

6.Directory - 디렉토리를 나타내는 클래스

7.FileTreatmentException - 파일에 대해서 add한경우 발생하는 예외 클래스

8.Main - 동작 테스트용 클래스



Visitor Class

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}

visit이라는 메소드가 오버로드 되어있다. 이 메소드는 하위 클래스에서 각각의 인스턴스에 대해

알맞은 처리를 하도록 구현된다.


Acceptor Interface

public interface Acceptor {
    public abstract void accept(Visitor v);
}

방문자를 받아들이는 인터페이스로, 이를 구현한 클래스(File,Directory)의 인스턴스들은 이 메소드를 사용하여 방문자를 받아들인다.


Entry Class

import java.util.Iterator;

public abstract class Entry implements Acceptor {
    public abstract String getName();                                   // 이름을 얻는다.
    public abstract int getSize();                                      // 사이즈를 얻는다.
    public Entry add(Entry entry) throws FileTreatmentException {       // 엔트리를 추가
        throw new FileTreatmentException();
    }
    public Iterator iterator() throws FileTreatmentException {    // Iterator의 생성
        throw new FileTreatmentException();
    }
    public String toString() {                                          // 문자열 표현
        return getName() + " (" + getSize() + ")";
    }
}

Visitor 패턴에 적용시킬 수 있게 Acceptor 인터페이스를 구현하고 있으며, 이는 Entry의 하위클래스들이 acceptor()메소드를 구현하도록 한다. add메소드는 디렉토리에 대해서만 유효하기 때문에 디렉토리 클래스에서는 오버라이드 해주고, 파일 클래스에서는 상속된 이 메소드를 그대로 사용함으로서 파일에서 add가 일어날 경우 예외를 발생시키도록 해준다.


public class File extends Entry {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

accept메소드가 드디어 나왔다. 인자는 Visitor이다. accept메소드를 받아서 visit메소드를 호출하는데 자기 자신을 인자로 넘기고 있다. 나중에 보충 설명을 하도록 하겠다.


Directory Class

import java.util.Iterator;
import java.util.Vector;

public class Directory extends Entry {
    private String name;                    // 디렉토리의 이름
    private Vector dir = new Vector();      // 디렉토리 엔트리의 집합
    public Directory(String name) {         // 생성자
        this.name = name;
    }
    public String getName() {               // 이름을 얻는다.
        return name;
    }
    public int getSize() {                  // 사이즈를 얻는다.
        int size = 0;
        Iterator it = dir.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            size += entry.getSize();
        }
        return size;
    }
    public Entry add(Entry entry) {         // 엔트리의 추가
        dir.add(entry);
        return this;
    }
    public Iterator iterator() {      // Iterator의 생성
        return dir.iterator();
    }
    public void accept(Visitor v) {         // 방문자를 받아들임
        v.visit(this);
    }
}

accept메소드가 존재한다.


ListVisit Class

import java.util.Iterator;

public class ListVisitor extends Visitor {
    private String currentdir = "";                         // 현재 주목하고 있는 디렉토리명
    public void visit(File file) {                  // 파일을 방문했을 때 호출된다.
        System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {   // 디렉토리를 방문했을 때 호출된다.
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this);
        }
        currentdir = savedir;
    }
}

visit메소드가 구현되어 있다. 여기까지만 보고 아~ 하는 사람도 있을지 모르겠다. currentdir필드는 현재 주목하고 있는 디렉토리 명이다. visit(File)메소드가 파일을 방문했을 때에 File클래스의 accept메소드 내에서 호출된다. 인수 file은 방문한 File 클래스의 인스턴스이다. accept메소드와 visit메소드는 서로 재귀적 호출 관계에 있는 것이다.


또한, visit(Directory)메소드를 보면 Iterator를 취득하여 각 엔트리에 대해서 accept메소드를 호출하고 있다. 이 accept메소드에서 다시 visit을 호출하고.... etc...


Main Class

Directory rootdir = new Directory("root");
            Directory bindir = new Directory("bin");
            Directory tmpdir = new Directory("tmp");
            Directory usrdir = new Directory("usr");
            rootdir.add(bindir);
            rootdir.add(tmpdir);
            rootdir.add(usrdir);
            bindir.add(new File("vi", 10000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());             

일부분만 보았다.

우선 root-bin-vi,latex

           -tmp

           -usr

이렇게 되어있는 구조이다. 최초 rootdir의 accept를 호출하고 있으며, 인자는 생성한 Visitor이다. rootdir의 accept는 visit메소드를 호출하며 인자로 자기 자신을 넘긴다. visit메소드에서는

/root를 출력하고, iterator를 사용 root의 entry를 뒤져본다. bindir이 나오게 되며 while문 안에서 bindir의 accept를 호출하고, 다시 bindir에서는 visit을 호출하며, 위 처럼 동작을 반복한다.

/root/bin을 출력하고 다시 bin의 entry를 뒤져보고, 이번엔 파일 vi가 나오며, vi의 accept를 호출하고, 다시 visit이 호출되고, /root/bin/vi를 출력하고, 다시 bin을 뒤져서 latex가 나오고....

블라블라...


ListVisit메서드는 하나만 생성이 되며 Accept메소드가 호출될때 인자로 넘어가는 ListVisit의 인스턴스도 오로지 하나이다. (그래야 작업이 이어질것 아닌가..)

어느 계층에서건 entry가 발견이 되면 accept메서드를 호출하게 되며, accpet메서드에서 다시 visit메서드를 호출함으로서 자신에 대한 처리를 하고 , 다시 바톤을 accept를 호출했던 while문 쪽으로 넘기는 것이다. (다음 entry를 검사할 수 있게..)


이것이 장점은 기능의 확장이 쉽다는 것이다. 데이터구조와 로직을 완전히 분리 시켰기 때문이다. 한 작업에 대해 그 작업을 하는 ListVisit의 인스턴스는 딱 하나만 생성이 되는 것을 이용하여 다른 작업 (html파일찾기등)을 추가하고 싶을때에는 다른 수정 필요없이 다른 기능을 하는 Visitor클래스만 작성하면 되는 것이다.