Visitor Pattern

2021. 12. 11. 15:43Computer Sciences/Design Patterns

개요

방문자 패턴(Visitor Pattern)은 데이터 구조와 처리를 분리하는 패턴이다. 무슨 말인지 잘 와닿지 않을 것이다. 시나리오를 통해 알아보도록 하자.

시나리오

사용자가 내 컴퓨터의 디렉토리를 순회할 수 있는 프로그램을 만들어달라고 한다. 우리는 이를 재귀를 통해서 해결할 수 있다. 대략적으로 코딩하면 다음과 같을 것이다.

// 디렉토리면 하위 디렉토리를 다시 순회하고 아니면 파일 정보 출력
if(file.isDirectory()) {
	Iterator iter = file.iterator();
	iter.search();
} else {
	System.out.println(file.toString());	
}

그런데 이젠 사용자가 각 파일에 대해 순회하면서 이름만 가져오는 기능도 원한다. 그렇다면 file.getName()과 같은 메서드로 이름을 가져오고 순회 클래스를 새로 만들어서 작성해야 할 것이다. 나쁜 냄새가 스멀스멀 올라온다.

문제

  1. 기능이 변경될 때마다 코드를 수정해야한다.
  2. 기능이 추가되면 클래스를 새로 작성해야 하는 경우도 발생한다.

해결

우리는 데이터 구조와 처리에 대해 로직을 분리함으로써 이 문제를 해결한다. 순회하고자 하는 요소들은 accpet()라는 메서드를 구현한다. 이 메서드는 외부에서 Visitor라고 하는 객체를 받고 해당 객체에서 작성된 방법대로 요소들에 대해 처리한다. 이말인즉슨 Visitor는 요소들에 대해 알고 있다는 것이 된다. 즉 캡슐화 관점에서 이 패턴은 썩 좋은 방법은 아니다. 하지만 클라이언트는 Visitor만 알고 있으면 원하는 방법대로 데이터들을 순회할 수 있게 된다. 따라서 복합 객체는 건드리지 않고 Visitor만 수정하거나 추가하여 사용할 수 있다.

글로만 보면 패턴을 이해하는데 어렵다(나만 그런가?). 따라서 코드를 직접 작성해보고 이해하도록 하자.

아래 예제에서는 ListVisitor만 생성해서 각 파일의 정보를 toString()으로 출력했지만 NameVisitor, SizeVisitor, 또는 이외에 각종 상태들에 대해 순회하는 Visitor를 만들어서 사용할 수도 있다.

Class Diagram

예제 코드

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

    public abstract void visit(Directory directory);
}
public interface Element {
    void accept(Visitor v);
}
public abstract class Entry implements Element {
    public abstract String getName();

    public abstract int getSize();

    public Entry add(Entry entry) throws FileTreatmentException {
        throw new FileTreatmentException();
    }

    public Iterator<Entry> iterator() {
        throw new FileTreatmentException();
    }

    public String toString() {
        return getName() + "(" + getSize() + ")";
    }
}
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;
    }

    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }

}
import java.util.ArrayList;
import java.util.Iterator;

public class Directory extends Entry {

    private String name;
    private ArrayList<Entry> dir = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getSize() {
        int size = 0;
        Iterator<Entry> iter = dir.iterator();
        while (iter.hasNext()) {
            Entry entry = iter.next();
            size += entry.getSize();
        }
        return size;
    }

    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }

    public Entry add(Entry entry) {
        dir.add(entry);
        return this;
    }

    public Iterator<Entry> iterator() {
        return dir.iterator();
    }

}
public class FileTreatmentException extends RuntimeException {

    public FileTreatmentException() {
    }

    public FileTreatmentException(String msg) {
        super(msg);
    }
}
import java.util.Iterator;

public class ListVisitor extends Visitor {

    private String currentDir = "";

    @Override
    public void visit(File file) {
        System.out.println(currentDir + "/" + file);
    }

    @Override
    public void visit(Directory directory) {
        System.out.println(currentDir + "/" + directory);
        String saveDir = currentDir;
        currentDir = currentDir + "/" + directory.getName();
        Iterator<Entry> it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = it.next();
            entry.accept(this);
        }
        currentDir = saveDir;
    }

}
public class Main {

    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            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", 100000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());
            System.out.println();
            System.out.println("Making user entries...");
            Directory Kim = new Directory("Kim");
            Directory Lee = new Directory("Lee");
            Directory Park = new Directory("Park");
            usrdir.add(Kim);
            usrdir.add(Lee);
            usrdir.add(Park);
            Kim.add(new File("diary.html", 100));
            Kim.add(new File("Composite.java", 200));
            Lee.add(new File("memo.txt", 300));
            Park.add(new File("game.doc", 400));
            Park.add(new File("junk.mail", 500));
            rootdir.accept(new ListVisitor());
        } catch (FileTreatmentException e) {
            e.printStackTrace();
        }
    }
}

장단점

장점

  • 구조 자체를 변경시키지 않으면서 복합 객체 구조에 새로운 기능을 추가할 수 있다.
  • 비교적 쉽게 새로운 기능을 추가할 수 있다.
  • Visitor에서 수행하는 기능와 관련된 코드를 한 곳에 집중시킬 수 있다.

단점

  • 복합 객체의 캡슐화가 깨진다.
  • 컬렉션 내의 모든 항목을 접근하기 때문에 복합 구조를 변경하기 어려워진다.

'Computer Sciences > Design Patterns' 카테고리의 다른 글

Memento Pattern  (0) 2021.12.11
Flyweight Pattern  (0) 2021.12.11
Prototype Pattern  (0) 2021.12.11
14. Chain of Responsibility Pattern  (0) 2021.11.14
13. Bridge Pattern  (0) 2021.11.14