봄맞이 세일
비지터

Go로 작성된 비지터

비지터는 기존 코드를 변경하지 않고 기존 클래스 계층구조에 새로운 행동들을 추가할 수 있도록 하는 행동 디자인 패턴입니다.

제 설명글 [비지터와 이중 디스패치]{비지터와 이중 디스패치}에서 왜 단순히 비지터들을 메서드 오버로딩으로 대체할 수 없는지 알아보세요.

개념적인 예시

비지터 패턴은 실제로 구조체​(struct)​를 수정하지 않고도 구조체에 행동을 추가할 수 있도록 합니다. 당신이 다음과 같은 다른 모양들의 구조체​(structs)​들을 가진 lib의 관리자라고 가정해 봅시다:

  • 직사각형
  • 삼각형

위의 각 모양의 구조체​(struct)​는 공통 모양 인터페이스를 구현합니다.

회사 사원들이 당신의 멋진 lib을 사용하기 시작하면 그들은 여러 기능들을 요청할 것입니다. 이 중 가장 간단한 요청을 검토해 봅시다: 회사 부서가 모양 구조체들에 get­Area​(면적 가져오기) 행동을 추가해달라고 요청했습니다.

이 문제를 해결하기 위한 여러 옵션이 있습니다.

가장 먼저 떠오르는 옵션은 get­Area 메서드를 모양 인터페이스에 직접 추가한 다음 각 모양 구조체​(struct)​에서 구현하는 것입니다. 좋은 옵션인 듯싶으나 단점이 있습니다. 당신은 라이브러리의 관리자로서 누군가가 다른 행동을 요청할 때마다 당신의 귀중한 코드가 손상되는 위험을 감수하고 싶지 않을 것입니다. 그럼에도 불구하고 당신은 다른 팀들이 당신의 라이브러리를 어떻게든 확장하기를 원합니다.

두 번째 옵션은 기능을 요청하는 부서가 직접 행동을 구현하도록 하는 것이나 요청된 행동은 비공개 코드에 의존할 수 있으므로 항상 가능한 옵션이 아닙니다.

세 번째 옵션은 비지터 패턴을 사용하여 위의 문제를 해결하는 것이며, 다음과 같이 비지터 인터페이스를 정의하는 것으로 시작합니다:

type visitor interface {
   visitForSquare(square)
   visitForCircle(circle)
   visitForTriangle(triangle)
}

visit­For­Square­(square), visit­For­Circle­(circle), visit­For­Triangle­(triangle) 함수들은 사각형, 원 및 삼각형에 각각 기능들을 추가할 수 있도록 합니다.

비지터 인터페이스에 단일 메서드 visit­(shape)가 있을 수 없는 이유가 궁금하나요? 그 이유는 Go 언어가 메서드 오버로딩을 지원하지 않기 때문에 이름은 같지만 매개변수들이 다른 메서드들을 가질 수 없도록 하기 때문입니다.

이제 두 번째 중요한 부분은 모양 인터페이스에 accept 메서드를 추가하는 것입니다.

func accept(v visitor)

모든 모양 구조체들​(structs)​은 이 메서드를 정의해야 하며, 코드는 다음과 비슷하게 작성됩니다:

func (obj *square) accept(v visitor){
    v.visitForSquare(obj)
}

잠시만요, 방금 기존 모양 구조체들​(structs)​을 수정하고 싶지 않다고 언급하지 않았나요? 불행히도 비지터 패턴을 사용할 때 모양 구조체를 수정해야 하긴 합니다. 그러나 이 수정은 단 한 번만 수행됩니다.

get­Num­Sidesget­Middle­Coordinates같은 다른 행동들을 추가하는 경우 우리는 모양 구조체들​(structs)​에 추가 변경 없이 같은 accept­(visitor) 함수를 사용합니다.

최종적으로 모양 구조체들​(structs)​은 한 번만 수정하면 되며, 다른 행동들을 추가해 달라는 모든 향후 요청은 같은 accept 함수를 사용하여 처리할 수 있습니다. 팀에서 get­Area 행동을 요청하면 단순히 비지터 인터페이스의 구상 구현을 정의한 후 해당 구현에 면적 계산 로직을 작성하여 요청된 행동을 추가할 수 있습니다.

shape.go: 요소

package main

type Shape interface {
	getType() string
	accept(Visitor)
}

square.go: 구상 요소

package main

type Square struct {
	side int
}

func (s *Square) accept(v Visitor) {
	v.visitForSquare(s)
}

func (s *Square) getType() string {
	return "Square"
}

circle.go: 구상 요소

package main

type Circle struct {
	radius int
}

func (c *Circle) accept(v Visitor) {
	v.visitForCircle(c)
}

func (c *Circle) getType() string {
	return "Circle"
}

rectangle.go: 구상 요소

package main

type Rectangle struct {
	l int
	b int
}

func (t *Rectangle) accept(v Visitor) {
	v.visitForrectangle(t)
}

func (t *Rectangle) getType() string {
	return "rectangle"
}

visitor.go: 비지터

package main

type Visitor interface {
	visitForSquare(*Square)
	visitForCircle(*Circle)
	visitForrectangle(*Rectangle)
}

areaCalculator.go: 구상 비지터

package main

import (
	"fmt"
)

type AreaCalculator struct {
	area int
}

func (a *AreaCalculator) visitForSquare(s *Square) {
	// Calculate area for square.
	// Then assign in to the area instance variable.
	fmt.Println("Calculating area for square")
}

func (a *AreaCalculator) visitForCircle(s *Circle) {
	fmt.Println("Calculating area for circle")
}
func (a *AreaCalculator) visitForrectangle(s *Rectangle) {
	fmt.Println("Calculating area for rectangle")
}

middleCoordinates.go: 구상 비지터

package main

import "fmt"

type MiddleCoordinates struct {
	x int
	y int
}

func (a *MiddleCoordinates) visitForSquare(s *Square) {
	// Calculate middle point coordinates for square.
	// Then assign in to the x and y instance variable.
	fmt.Println("Calculating middle point coordinates for square")
}

func (a *MiddleCoordinates) visitForCircle(c *Circle) {
	fmt.Println("Calculating middle point coordinates for circle")
}
func (a *MiddleCoordinates) visitForrectangle(t *Rectangle) {
	fmt.Println("Calculating middle point coordinates for rectangle")
}

main.go: 클라이언트 코드

package main

import "fmt"

func main() {
	square := &Square{side: 2}
	circle := &Circle{radius: 3}
	rectangle := &Rectangle{l: 2, b: 3}

	areaCalculator := &AreaCalculator{}

	square.accept(areaCalculator)
	circle.accept(areaCalculator)
	rectangle.accept(areaCalculator)

	fmt.Println()
	middleCoordinates := &MiddleCoordinates{}
	square.accept(middleCoordinates)
	circle.accept(middleCoordinates)
	rectangle.accept(middleCoordinates)
}

output.txt: 실행 결과

Calculating area for square
Calculating area for circle
Calculating area for rectangle

Calculating middle point coordinates for square
Calculating middle point coordinates for circle
Calculating middle point coordinates for rectangle

다른 언어로 작성된 비지터

C#으로 작성된 비지터 C++로 작성된 비지터 자바로 작성된 비지터 PHP로 작성된 비지터 파이썬으로 작성된 비지터 루비로 작성된 비지터 러스트로 작성된 비지터 스위프트로 작성된 비지터 타입스크립트로 작성된 비지터