Design Pattern, Visitor

2022. 2. 13. 13:06ETC/Design Patterns

 

Object Behavioral Pattern

Visitor Pattern

 

-----------------    INDEX     -----------------

 

Visitor Pattern ?

Pros & Cons

Structure

Sequence Diagram

Sample Code: Java

Consequences

Multiple Element

Accumulating state

Breaking encapsulation

OCP

관련 패턴

 

----------------------------------------------

 

 

Represent an operation to be performed on the elements of an object structure.
Visitor lets you define a new operation without changing the classes of the elements on which it operates.

- GoF Design Patterns

 

 

방문자 패턴은 알고리즘을 객체 구조에서 분리시키는 디자인 패턴입니다.

 

GoF에서 소개하는 위의 인용구를 번역하면 아래와 같습니다.

"객체의 구성요소에 대해 수행할 작업을 대리합니다.

Visitor는 객체의 속성을 연산하는 새로운 작업을 클래스의 변경없이 정의할 수 있습니다."

 

 

Visitor 패턴은 구성 요소들을 가진 객체들과, 해당 구성 요소를 가지고 연산하는 복잡한 알고리즘이 있을 때,

연산하는 알고리즘을 따로 분리하여 구성 요소를 가진 객체에게 방문하여 연산합니다.

이 때, 구성 요소를 가진 객체를 Element라고 하고, 알고리즘을 따로 분리하여 Element를 방문하는 개체가 Visitor입니다.

 

 

Pros & Cons

 

방문자 패턴은 기존 객체에서 행위 동작을 분리하고 새로운 행위를 추가할 수 있는 유용한 패턴입니다.

기존 객체를 직접 수정하기 어려울 경우, 방문자 패턴을 통해 행동을 대신 처리할 수 있다는 장점도 있습니다.

또, 많은 연산으로 클래스를 더럽히고 싶지 않을 때도 유용합니다.

 

하지만, 방문자 패턴의 동작은 간단하지만 클래스가 서로 복잡하게 연결되어 있어 이해하기 어렵습니다.

그래서 방문자 패턴을 제대로 이해하고 코드를 확인하는 것을 추천드립니다.

 

 

먼저, 구조를 살펴보고 예시를 통해 개념을 잡아보도록 하겠습니다.

 

 

Structure

GoF Design Patterns - Visitor

 

Visitor

- 객체 구조에 있는 ConcreteElement의 각 클래스에 대한 방문하여 수행할 작업을 선언합니다.

- 작업의 이름과 서명은 방문자에게 방문 요청을 보내는 클래스를 식별합니다.

이를 통해 방문자는 방문 요소의 구체적인 종류를 결정할 수 있습니다. 방문자는 특정 인터페이스를 통해 요소에 직접 액세스할 수 있습니다.

 

ConcreteVisitor

- 방문자가 선언한 각 작업을 구현합니다. 각 연산은 구조체의 해당 객체 클래스에 대해 정의된 알고리즘의 일부를 구현합니다. ConcreteVisitor는 알고리즘의 컨텍스트를 제공하고 로컬 상태를 저장합니다. 이 상태는 종종 객체 구조를 순회하는 동안 결과를 누적합니다.

 

Element

- Visitor를 매개변수로 받는 Accept(Visitor) 작업을 정의합니다.

 

ConcreteElement

- Visitor를 매개변수로 받는 Accept(Visitor) 작업을 구현합니다.

 

ObjectStructure

- 요소를 열거할 수 있습니다.
- Visitor가 구성 요소를 방문할 수 있도록 high-level의 인터페이스를 제공할 수 있습니다.
- 복합체 (Composite) 또는 list나 set과 같은 Collection를 사용할 수 있습니다.

 

 

 

Sequence Diagram

 

Sequence Diagram

 

시퀀스 다이어그램으로 흐름을 파악해볼게요.

가장 먼저, ObjectStructure(와 Element를 구현한 ConcreteElement)와 Visitor 객체를 생성합니다.

Element의 Accept 메서드를 호출하는데, 이때 생성한 Visitor 객체를 인자로 넘겨줍니다.

ObjectStructure를 구현한 ConcreteElement에서 accept를 오버라이딩하고,

해당 ConcreteElement를 방문할 때 수행할 내용으로 Visitor의 Operation의 호출합니다.

Visitor의 operation이 실행되고, 해당 작업이 종료됩니다.

 

 

코드를 통해 확실히 이해해보도록 할게요.

 

 

Sample Code: Java

하나의 물품을 조회하고, 장바구니에 담고 총액을 계산하는 로직을 구현해봅시다.

물품이 가진 요소는 아주 다양할거에요. 다양한 알고리즘 또한 가질 수 있습니다.

 

알고리즘을 따로 분리하고자 Visitor 패턴을 사용해봅시다.가장 먼저, 물품들의 기본적인 추상형을 정의해봅시다.

 

📌  ObjectStructure

public class Product {
    protected String name;
    protected int price;
    protected int num;
    protected final int DEFAULT_TAX = 10;

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }

    public int getNum() {
        return num;
    }
}

 

ObjectStructure는 위와 같이 단순히 구성요소만을 가진 enum이라고 할 수 있습니다.

 

 

📌  Element

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

 

element는 accept 메서드를 정의합니다.

해당 클래스를 통해 방문을 할 수 있는 객체를 제작할 수 있기 때문에 Visitable으로 명명했습니다.

 

 

📌  ConcreteElement

public class Cart extends Product implements Visitable {
    public Cart(String name, int price, int num) {
        this.name = name;
        this.price = price;
        this.num = num;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.order(this);
    }
    
    public String list() {
        String order = this.name;
        order += " - " + this.num + "개: " + this.price + "₩";
        return order;
    }

    public int getTax(int tax) {
        return (this.price * this.num) * tax/100;
    }
}

 

위와 같이 방문을 허용하는 accept 메소드를 구현합니다.

accept에서는 알고리즘을 가지고 있는 Visitor의 operation을 호출합니다.

쉽게 생각하면, 외부에 정의한 알고리즘을 가져오는 동작입니다.

 

list(), getTax()와 같이 이외의 로직을 따로 선언할 수도 있습니다.

 

 

📌  Visitor

public interface Visitor {
    String order(Visitable visitable);
}

 

visitor에서는 operation을 정의합니다.

이 operation은 ConcreteElement의 요소를 가지고 연산하는 로직을 담습니다.

 

 

📌  ConcreteVisitor

public class Visitant implements Visitor {
    private int total;
    private int num;

    public Visitant() {
        System.out.println("주문을 처리합니다.");
        this.total = 0;
        this.num = 0;
    }

    public int getTotal() {
        return total;
    }

    @Override
    public String order(Visitable visitable) {
        System.out.println("[ 상품 내역 ]");

        if (visitable instanceof Cart ) {
            Cart cart = (Cart) visitable;

            int total = cart.getPrice() * cart.getNum();
            this.num += cart.getNum();
            this.total += total;

            return cart.list() +
                    "\n주문 개수 : " + num +
                    ", 합계 : " + total;
        }
        throw new IllegalStateException();
    }
}

 

operation인 order를 구현합니다.

operation에서는 Element인 visitable의 구성요소들을 가지고 연산을 하는 것을 확인할 수 있습니다.

 

 

📌  Client

public class Client {
    public static void main(String[] args) {
        // 하나의 element
        System.out.println("\n====== Cart ======");
        Cart cart = new Cart("coffee", 4500, 2);
        System.out.println(cart.accept(new Visitant()));

    	// 다수의 element
        System.out.println("\n====== Cart List ======");
        Cart[] list = {
                new Cart("noodle", 900, 2),
                new Cart("icecream", 1500, 1),
                new Cart("drink", 2800, 1)
        };
        Visitant visitant = new Visitant();

        for (Cart elem : list) {
            System.out.println(elem.accept(visitant));
        }

        System.out.println("\n주문 합계: " + visitant.getTotal());
    }

}

 

 

위와 같이 하나의 Element에 접근을 할 수도, 여러개에 접근할 수도 있습니다.

출력물을 확인해보겠습니다.

 

 

📌  Output

====== Cart ======
주문을 처리합니다.
[ 상품 내역 ]
coffee - 2개: 4500₩
주문 개수 : 2, 합계 : 9000

====== Cart List ======
주문을 처리합니다.
[ 상품 내역 ]
noodle - 2개: 900₩
주문 개수 : 2, 합계 : 1800
[ 상품 내역 ]
icecream - 1개: 1500₩
주문 개수 : 3, 합계 : 1500
[ 상품 내역 ]
drink - 1개: 2800₩
주문 개수 : 4, 합계 : 2800

주문 합계: 6100

 

 

 

Consequences

✔️ Multiple Element

방문자 패턴은 데이터와 처리 행위를 분리하는데, 공통된 로직을 별도의 객체로 분리함으로써 행위를 보다 쉽게 추가할 수 있습니다.

공통된 로직을 분리하는데 또 다른 이유는 다수의 원소 객체를 처리하기 위해서 입니다.

 

다수의 원소 객체를 처리하기 위해 반복자 패턴과 결합해서 동작할 수 있으며,

여러개의 원소 객체와 하나의 방문자 객체와 하나의 방문자 객체로 데이터를 처리할 수 있습니다.

 

 

✔️ Accumulating State

위의 예시에서 Visitor에서 total을 관리하는 것을 확인하셨나요?

방문객들은 객체 구조의 각 요소를 방문하면서 상태를 축적accumulate할 수 있습니다.

위의 특징 중 다수의 Element에 대한 작업을 수행할 때 굉장히 잘 활용할 수 있겠죠.

 

방문자가 없으면 순회 작업을 수행할 때 추가 인수로 전달되거나 전역 변수로 나타날 수도 있습니다.

 

 

✔️ Breaking Encapsulation

방문자 패턴은 방문하는 외부 객체에 자신의 모든 데이터와 행위의 접근을 허용합니다.

따라서, 내부 상태에 접근하는 작업을 강제로 제공하여 객체지향의 장점인 캡슐화와 데이터 은닉을 활용할 수 없게 방해할 수 있습니다.

 

 

OCP

Open-Closed Principle

개방-폐쇄 원칙은 객체지향 설계 원칙으로,

소프트웨어 객체 확장은 열려 있어야 하고 수정은 닫혀 있어야 한다는 프로그래밍 원칙입니다.

즉, 클래스를 설계할 때 확장만 허용한다는 것이며 확장을 위해 기존 코드를 수정해서는 안된다는 뜻입니다.

 

방문자는 객체지향의 OCP 원칙을 반영한 패턴이고, 방문자 패턴은 데이터 처리 행위를 위해 객체를 분리합니다.

 

 

 

관련 패턴

C- Creational Patterns  |  S - Structural Patterns  |  B - Behavioral Patterns

 

S: Composite

방문자 패턴의 데이터 구조는 복합 구조이고 내부 구조가 복합체 composite 패턴과 유사합니다.

 

 

B: Iterator

데이터 구조는 다수의 객체로 되어있습니다.

방문자 패턴은 데이터 구조에 방문하여 처리를 위임하는데, 이를 위해 반복자 패턴을 응용하여 사용할 수 있습니다.

 

 

B: Interpreter

방문자 패턴은 인터프리터 패턴과도 함께 사용하곤 합니다.

구문을 분석하기 위해 트리를 탐색할 때 해당 구문의 처리를 방문자 패턴으로 실행하는 경우가 발생할 수 있습니다.

 

 

 

 

 

 

그럼 지금까지 Visitor Pattern에 대해 알아보았습니다.

오타나 잘못된 내용이 있다면 댓글로 남겨주세요!

감사합니다 ☺️ 

 

 

 

모든 Design Patterns 모아보기

 

Design Patterns

안녕하세요. GoF 디자인 패턴을 정리하고자 합니다. 디자인 패턴은 일주일 전부터 공부를 시작했는데, 스스로 설명하듯 적는게 익히는데 도움이 클 것같아 정말 오랜만에 시리즈로 포스팅하려

gngsn.tistory.com

 

 

 

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

Design Pattern, Memento  (0) 2022.02.17
Design Pattern, State  (0) 2022.02.16
Design Pattern, Iterator  (0) 2022.02.10
Design Pattern, Command  (0) 2022.02.06
Design Pattern, Decorator  (0) 2022.02.03