디자인 패턴(구)/행위 패턴

방문자(Visitor) 패턴이란?

공대키메라 2022. 4. 30. 17:55

1. 방문자 패턴이란?

의도

객체 구조를 이루는 원소에 대해 수행할 연산을 표현합니다. 연산을 적용할 원소의 클래스를 변경하지 않고도 새로운 연산을 정의할 수 있게 합니다. 

 

즉, 우리가 클래스를 많이 바꾸는것 없이 다양한 타입의 클래스를 메소드로 추가할 수 있다.

사용 시기

  • 다른 인터페이스를 가진 클래스가 객체 구조에 포함되어 있으며, 구체 클래스에 따라 달라진 연산을 이들 클래스의 객체에 대해 수행하고자 할 때
  • 객체 구조를 정의한 클래스는 거의 변하지 않지만, 전체 구조에 걸쳐 새로운 연산을 추가하고 싶을 때
  • 각각 특징이 있고, 관련되지 않은 많은 연상이 한 객체 구조에 속해있는 객체들에 대해 수행될 필요가 있으며, 연산으로 클래스들을 더럽히고 싶지 않을 때

구조

  • Visitor : 객체 구조 내에 있는 각 ConcreteElement 클래스를 위한 Visit() 연산을 선언
  • ConcreteVisitor : Visitor 클래스에 선언된 연상을 구현한다. 각 연산은 구조 내에 있는 객체의 대응 클래스에 정의된 일부 알고리즘을 구현.
  • Element : 방문자를 인자로 받아들이는 Accept() 연산을 정의

2. 구현

다음 구현 내용은 소개하는 사이트에서 가져온 내용이다. (여기 클릭)

 

이 예제의 컨셉은 세금 계산이다

 

필수재, 술, 담배같은 항목에 시기에 따른 세금 값을 계산하는 것이다. 

 

 

 위 그림에서 Visitor 인터페이스 우리가 본 구조에 Visitor Interface에 대응한다. 

 

여기서 Element는 방문자를 인자로 받아들이는 Accept() 연산을 정의하며, Visitable이 이에 해당한다. 그리고 Visitable을 구현한 구현체로는 Neccesity가 있다. 

 

ConcreteVisitor는 Visitor 클래스에 선언된 연상을 구현한다. TaxVisisitor가 이와 대응된다.

 

그러면 코드를 적어보겠다.

Visitor.java

public interface Visitor {

    public double visit(Liquor liquorItem);
    public double visit(Tobacco tobaccoItem);
    public double visit(Necessity necessityItem);

}

우선 Visitor 인터페이스를 정의했다. 

 

이곳에서 객체 구조 내에 있는 각 ConcreteElement 클래스를 위한 Visit() 연산을 선언한다. 

 

우리가 만들 객체는 Liquor, Tobacco, Necessity다

 

이거는 벗어난 이야기인데 여기서 Tobacco가 난 타바코라고 한국에서 읽어서 Tabacco라고 알았는데 충격이다... 그리고 발음을 들어보니 타바코라고 안하고 매우 약한 발음으로 토(약음) 바아코라고 한다. (우씨 진짜 콩글리시 다 xxxx ^^)

Visitable.java

public interface Visitable {
    public double accept(Visitor visitor);
}

Visitable.java는 Element 에 해당한다.

TaxVisitor.java

import java.text.DecimalFormat;

public class TaxVisitor implements Visitor{

    DecimalFormat df = new DecimalFormat("#.##");

    public TaxVisitor(){

    }

    @Override
    public double visit(Liquor liquorItem) {
        System.out.println("Liquor item : Price with Tax");
        return Double.parseDouble(df.format((liquorItem.getPrice() * .18) + liquorItem.getPrice()));
    }

    @Override
    public double visit(Tobacco tobaccoItem) {
        System.out.println("tobacco Item : Price with Tax");
        return Double.parseDouble(df.format((tobaccoItem.getPrice() * .32) + tobaccoItem.getPrice()));
    }

    @Override
    public double visit(Necessity necessityItem) {
        System.out.println("Necessity Item : Price with Tax");
        return Double.parseDouble(df.format((necessityItem.getPrice() * 0) + necessityItem.getPrice()));
    }
}

TaxVisisitor Visitor 클래스에 선언된 연산을 구현한다.

Liquor.java

public class Liquor implements Visitable{

    private double price;

    public Liquor(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public double accept(Visitor visitor) {
        return visitor.visit(this);
    }
}

Necessity.java

public class Necessity implements Visitable{

    private double price;

    public Necessity(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public double accept(Visitor visitor) {
        return visitor.visit(this);
    }

}

Tobacco.java

public class Tobacco implements Visitable{

    private double price;

    public Tobacco(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public double accept(Visitor visitor) {
        return visitor.visit(this);
    }
}

 

위 세개의 객체 Necessity, Liquor, Tobacco 는 ConcreteElement이다.

VisitorTest.java 

public class VisitorTest {
    public static void main(String[] args) {
        TaxVisitor taxVisitor = new TaxVisitor();
        TaxHolidayVisitor taxHolidayVisitor = new TaxHolidayVisitor();

        Necessity milk = new Necessity(3.47);
        Liquor vodka = new Liquor(11.99);
        Tobacco cigars = new Tobacco(19.99);

        System.out.println(milk.accept(taxVisitor) + "\n");
        System.out.println(vodka.accept(taxVisitor) + "\n");
        System.out.println(cigars.accept(taxVisitor) + "\n");

        System.out.println("TAX HOLIDAY PRICES");
        System.out.println(milk.accept(taxHolidayVisitor) + "\n");
        System.out.println(vodka.accept(taxHolidayVisitor) + "\n");
        System.out.println(cigars.accept(taxHolidayVisitor) + "\n");
    }
}

 

출력 결과 보기 클릭!

더보기
Necessity Item : Price with Tax
3.47

Liquor item : Price with Tax
14.15

tobacco Item : Price with Tax
26.39

TAX HOLIDAY PRICES
Necessity Item : Price with Tax
3.47

Liquor item : Price with Tax
13.19

tobacco Item : Price with Tax
25.99


Process finished with exit code 0

장점

  • 기존 코드를 변경하지 않고 새로운 코드를 추가할 수 있다. 
  • 추가 기능을 한 곳에 모아둘 수 있다. 

단점

  • 복잡하다.
  • 새로운 Element를 추가하거나 제거할 때 모든 Visitor 코드를 변경해야 한다

간단하게 Visitor패턴에 대해 알아봤다.

 

Visitor 패턴은 글쎄... 그렇게 실용적이지는 않은 것 같다. 가장 큰 단점으로는 Element(예제에서는 ) 추가시 ConcreteElement에 해당하는 (구조에서는 ElementA, ElementB) Necessity, Liquor, Tobacco 객체를 전부 수정해야한다. 

 

하지만 의도만 동일하다면 다른 방식으로 구현도 가능한데, 분기를 태워서 한 객체 내에서 Necessity, Liquor, Tobacco 에 해당하는 입력에 따라 각자에 맞게 작동하게끔 하는 것이다. class를 여러개 만들지 말고 한 곳에서 말이다. 

 

이상한점 잇으면 피드백은 언제든 환용!