방문자 패턴

기술블로그·2022년 7월 2일
0

Design Patterns

목록 보기
2/4

Refactoring Guru

문제


당신이 지도 정보 서비스를 개발한다고 가정해 봅시다. 지도 정보는 정점과 간선으로 이뤄진 그래프 구조로 표현되겠죠. 코드레벨에서 보자면 정점 타입은 클래스, 특정 정점은 객체로 나타낼 수 있습니다.

만약 그래프를 XML 포맷으로 추출하고 싶다면 어떤 방법을 사용할 수 있을까요? 단순히 생각했을 때 정점 클래스에 추출 메서드를 구현하는 방법이 있습니다. 다형성을 잘 사용한다면 구현 클래스와 추상 클래스의 커플링을 만들지 않고 우아하게 코드를 작성할 수 있을 겁니다.

하지만 이 방법엔 몇가지 문제가 있습니다.

  • 클래스를 변경함으로써 예측할 수 없는 문제 발생 가능
  • 클래스의 메인 역할은 지도정보를 담고 있는 것인데, XML 포맷으로 추출하는 메서드를 포함하는 것이 이질적
  • 다른 포맷으로 추출하는 기능을 더 추가한다면 클래스의 유지보수성 저하

"클래스의 역할과 약간은 동떨어졌지만 클래스와 매우 연관된 어떤 기능"을 추가하고 싶은데, 클래스의 메서드로 추가하면 문제가 생기는 상황입니다. 문제 재정의를 해본다면 다음과 같습니다.

클래스의 특정 동작(behavior)에 대한 책임을 클래스 외부에서 담당할 수 있을까?

좀 더 쉬운말로 퉁친다면 아래와 같습니다.

클래스의 메서드를 어떻게 클래스 밖으로 뺄 수 있을까?


해결

그림에서 element 인터페이스는 정점의 추상클래스에 해당하고 Concrete Element는 정점의 구현 클래스에 해당합니다. (방금 예시에선 Place는 element, Building|Apartment|Park 등은 concrete element에 해당합니다.)

accept메서드는 정점 클래스에서 정의하는 메서드입니다. 정점 클래스에서 변동이 생기는 유일한 부분은 Concrete Element에서의 메서드 구현부와 element 인터페이스의 accept메서드 선언부가 전부입니다. 클라이언트 입장에서 메서드 호출(ex, XML으로 추출)에 해당합니다.

  • Building 타입 객체를 XML 타입으로 추출한다면 empireStateBuilding.accept(exportXML)
  • Park 타입 JSON 타입으로 추출한다면 lotteWorld.accept(exportJSON)

visitor 클래스는 element 클래스의 동작에 대한 책임이 있는 클래스입니다. 여러가지 추출 방식이 Concrete visitor(exportXML|exportJSON)에 구현돼 있습니다. Concrete visitor은 각각 모든 Concrete element(ex, Building|Apartment|Park)마다 다른 방식의 메서드를 구성할 수 있습니다.

우리 예시에 맞게 그림에 낙서를 해본다면 다음과 같습니다.

구현

파일구조

./main
├── place.go
├── building.go
├── client.go
├── exportJSON.go
├── exportVisitor.go
├── exportXML.go
└── park.go

place.go: element

package main

type place interface {
	getType() string
	accept(exportVisitor)
}

building.go: concrete element

package main

type building struct {
	lat float32
	lon float32
}

func (b *building) accept(v exportVisitor) {
	v.visitForBuilding(b)
}

func (b *building) getType() string {
	return "Building"
}

park.go: concrete element

package main

type park struct {
	lat float32
	lon float32
}

func (p *park) accept(v exportVisitor) {
	v.visitForPark(p)
}

func (p *park) getType() string {
	return "Park"
}

exportVisitor.go: visitor

package main

type exportVisitor interface {
	visitForBuilding(*building)
	visitForPark(*park)
}

exportXML.go: concrete visitor

package main

import (
	"fmt"
)

type exportXML struct {
	xml interface{} //any
}

func (x *exportXML) visitForBuilding(b *building) {
	fmt.Println("exporting building to XML")
}

func (x *exportXML) visitForPark(p *park) {
	fmt.Println("exporting park to XML")
}

exportJSON.go: concrete visitor

package main

import (
	"fmt"
)

type exportJSON struct {
	json interface{} //any
}

func (j *exportJSON) visitForBuilding(b *building) {
	fmt.Println("exporting building to JSON")
}

func (j *exportJSON) visitForPark(p *park) {
	fmt.Println("exporting park to JSON")
}

client.go: client code

package main

import "fmt"

func main() {
	empireStateBuilding := &building{lat: 100.0, lon: 32.0}
	lotteWorld := &park{lat: 101.0, lon: 34.0}

	exportJSON := &exportJSON{}
	empireStateBuilding.accept(exportJSON)
	lotteWorld.accept(exportJSON)

	fmt.Println()
	exportXML := &exportXML{}
	empireStateBuilding.accept(exportXML)
	lotteWorld.accept(exportXML)
}

실행 결과

exporting building to JSON
exporting park to JSON

exporting building to XML
exporting park to XML
profile
Software Engineer

0개의 댓글