Visitor Pattern

Visitor 디자인 패턴의 필요성과 그 구조에 대해 알아봅니다.읽는데 4분 정도 걸려요.

필요성

Composite pattern을 사용하면서 동적으로 어떤 기능을 추가해야 하는 경우가 있다.

이 경우 컴포넌트가 visitor의 접근 수락(accept) 하여 기능을 수행하는 패턴을 Visitor Pattern 라고 한다.

Visitor Pattern

예시를 위해 java코드로 html 파일을 작성하는 코드를 구현한다고 가정해보자.

VisitorPattern.java
public interface Element {
  public void accept(Visitor visitor);
}

public interface Visitor {
  public void visit(HtmlElement element);
  public void visit(HtmlParentElement parentElement);
}

우선 모든 방문 대상의 클래스를 하나의 Element 인터페이스로 묶기 위해 인터페이스를 만든다.
또한, 방문하여 수정을 가할 클래스는 Visitor 인터페이스로 묶기게 되는데, 해당 클래스의 visit 메서드는 모든 방문 대상의 구현 클래스를 방문할 수 있어야 한다.

HtmlTag.java
public abstract class HtmlTag implements Element {
  public String getTagName() {
    throw new UnsupportedOperationException();
  }
  public void setTagBody(String tagBody) {
    throw new UnsupportedOperationException();
  }
  // ...
}

구현할 방문 대상의 클래스는 모두 HtmlTag를 상속받게 되는데, 이 부분은 Composite pattern과 동일하다.

HtmlElement.java
public class HtmlElement extends HtmlTag {
  private String tagName;
  private String startTag;
  private String tagBody;

  public HtmlElement(String tagName) {
    this.tagName = tagName;
    this.tagName = '';
  }

  @override
  public String getTagName() {
    return tagName;
  }
  ...
  @override
  public void accept(Visitor visitor) {
    visitor.visit(this);
  }
}
HtmlParentElement.java
public class HtmlParentElement extends HtmlTag {
  private String tagName;
  private String startTag;
  private List<HtmlTag> childrenTag;

  public HtmlParentElement(String tagName) {
    this.tagName = tagName;
    this.childrenTag = new ArrayList<HtmlTag>();
  }

  @override
  public String getTagName() {
    return tagName;
  }
  ...
  @override
  public void accept(Visitor visitor) {
    visitor.visit(this);
  }
}

방문 대상의 구현 클래스를 HtmlTag(Element)를 상속받아 만들어준다.
이 역시 인터페이스를 HtmlTag로 통일하기 위함이므로 Composite pattern과 동일하다.

ClassNameVisitor.java
public class ClassNameVisitor implements Visitor {
  @override
  public void visit(HtmlElement element) {
    element.setStartTag(element.getStartTag().replace(">", " class='children'"))
  }

  @override
  public void visit(HtmlParentElement element) {
    element.setStartTag(element.getStartTag().replace(">", " class='parent'"))
  }
} 
StyleVisitor.java
public class StyleVisitor implements Visitor {
  @override
  public void visit(HtmlElement element) {
    element.setStartTag(element.getStartTag().replace(">", " style='width: 46px;'"))
  }

  @override
  public void visit(HtmlParentElement element) {
    element.setStartTag(element.getStartTag().replace(">", " style='width: 58px;'"))
  }
} 

그리고 수정을 가할 클래스를 Visitor를 상속받아 만들어준다.

.java
psvm() {
  HtmlTag parentTag = new HtmlParentElement("<div>");
  HtmlTag cTag1 = new HtmlElement("<p>");
  HtmlTag cTag2 = new HtmlElement("<p>");

  parentTag.addChildTag(cTag1);
  parentTag.addChildTag(cTag2);

  Visitor className = new ClassNameVisitor();
  VIsitor style = new StyleVisitor();

  parentTag.accept(className);
  parentTag.accept(style);
  cTag1.accept(className);
  cTag1.accept(style);
  cTag2.accept(className);
  cTag2.accept(style);
}

그럼 위와 같은 방식으로 HtmlTag를 수정할 수 있다.

이렇게 하면, Composite들은 accept 메서드만 제공할 뿐, 실제 기능의 추가와 변경은 visitor가 수행하게 된다.
즉, 동적으로 기능을 확장할 수 있고, 기능을 visitor로 분리하여 코드를 관리할 수도 있다.