Visitor 패턴

2020-05-25

Visitor 패턴

Visitor 패턴은 비슷한 객체의 그룹에서 어떠한 동작을 수행 해야 할 때 사용됩니다. Visitor 패턴을 이용하면 수행 로직을 다른 클래스에 둘 수 있습니다.

예를 들어 각각 다른 아이템들을 담는 장바구니 있다고 생각해 봅시다. 각각 상품들은 아이템으로서의 동일한 역할을 하지만, 카테고리와 가격은 다를 수 있습니다. 해당 아이템을 구매하고 싶다면 담기 버튼을 눌러 장바구니로 이동하면 지불해야 할 총 금액이 계산이 됩니다. 예제에선 장바구니에 담긴 아이템 클래스들의 가격 계산하는 로직을 만들고 이 로직을 visitor 패턴을 사용해 다른 클래스로 옮겨 보겠습니다.

Visitor 패턴 사용해보기

Visitor 패턴을 구현하기 위해선, 먼저 장바구니에 쓰여질 아이템의 각기다른 타입을을 만들어야 합니다.

ItemElement.java

public interface ItemElement {
    int accept(ShoppingCartVisitor visitor);
}

accept() 함수는 Visitor 클래스를 인자로 받습니다.

Visitor 클래스는 밑에서 만들 예정입니다.

Book.java

public class Book implements ItemElement{

    private int price;
    private String isbnNumber;

    public Book(int price, String isbnNumber) {
        this.price = price;
        this.isbnNumber = isbnNumber;
    }

    public int getPrice() {
        return price;
    }

    public String getIsbnNumber() {
        return isbnNumber;
    }

    @Override
    public int accept(ShoppingCartVisitor visitor) {
        return visitor.visit(this);
    }
}

Fruit.java

public class Fruit implements ItemElement{

    private int pricePerKg;
    private int weight;
    private String name;

    public Fruit(int pricePerKg, int weight, String name) {
        this.pricePerKg = pricePerKg;
        this.weight = weight;
        this.name = name;
    }

    public int getPricePerKg() {
        return pricePerKg;
    }

    public int getWeight() {
        return weight;
    }

    public String getName() {
        return name;
    }

    @Override
    public int accept(ShoppingCartVisitor visitor) {
        return visitor.visit(this);
    }
}

두 개의 클래스 모두 ItemElement 인터페이스를 구현해야 하기 때문에 accept() 메소드를 구현했습니다.

이제 각각의 다른 아이템 타입이 있으므로 Visitor 인터페이스를 구현해 보겠습니다.

ShoppingCartVisitor.java

public interface ShoppingCartVisitor {
    int visit(Book book);
    int visit(Fruit fruit);
}

visitor 인터페이스를 구현하여 아이템마다 가격을 계산하는 자신만의 로직을 구현해보겠습니다.

ShoppingCartVisitorImpl.java

package com.donghyeon.designpattern.visitor;

public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
    //책 계산기
    @Override
    public int visit(Book book) {
        int cost=0;
        // book 값이 50달러 이상이면 5달러 할인
        if(book.getPrice() > 50) {
            cost = book.getPrice() - 5;
        } else{
            cost = book.getPrice();
        }
        System.out.println("책 번호 :: " + book.getIsbnNumber() + " 가격 = " + cost);

        return cost;
    }

    //과일 계산기
    @Override
    public int visit(Fruit fruit) {
        int cost = fruit.getPricePerKg()*fruit.getWeight();
        System.out.println(fruit.getName() + " 가격 = "+cost);
        return cost;
    }
}

테스트

VisitorPatternTests.java

public class VisitorPatternTests {
    public static void main(String[] args) {
        ItemElement[] items = new ItemElement[]{new Book(20, "1234"),new Book(100, "5678"),
                new Fruit(10, 2, "바나나"), new Fruit(5, 5, "사과")};

        int total = calculatePrice(items);
        System.out.println("총 금액 = "+total);
    }

    private static int calculatePrice(ItemElement[] items) {
        ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
        int sum=0;
        for(ItemElement item : items){
            sum = sum + item.accept(visitor);
        }
        return sum;
    }
}

결과

 번호 :: 1234 가격 = 20
 번호 :: 5678 가격 = 95
바나나 가격 = 20
사과 가격 = 25
 금액 = 160

UML

Visitor 패턴의 장점

Visitor 패턴의 장점은 만약 상품의 계산 로직이 변경되었다면, visitor 구현 클래스에만 변경이 일어 납니다.

또 다른 장점으로는, 현재 Book,Fruit 클래스만 있지만, 또 다른 클래스를 추가하고 싶다면 손쉽게 추가할 수 있습니다. 오직 visitor 인터페이스와 구현체에만 변화가 일어나고, 다른 클래스에는 일어나지 않습니다.

Vistor 패턴의 한계

Visitor 패턴의 단점은 패턴을 만들 때 visit() 메소드의 리턴 타입이 정해져 있어야 합니다.그렇지 않으면 인터페이스의 모든 구현체를 변경해야 할 수 도 있습니다.

또 다른 단점으로는 visitor 인터페이스를 구현한 클래스들의 수가 많아진다는 단점이 있습니다.