비지터 패턴
개요
텍스트와 이미지로 이루어진 문서에 출력, 통계, 검사 기능이 계속 붙는다고 하자. 각 노드에 기능별 메서드를 늘리는 대신, 연산 하나를 별도 객체로 모을 수 있다.
비지터(Visitor), 즉 방문자는 요소 종류별 처리 메서드를 가진 객체다. 요소가 방문자를 받아 자기 타입에 맞는 처리를 호출하므로, 방문자는 텍스트와 이미지를 다르게 다룰 수 있다.
UML
TextNode와 ImageNode가 IDocNode를 구현하고 방문자를 받는다. TextVisitor와 CountVisitor는 같은 IVisitor를 구현하지만 각각 출력과 집계를 맡는다.
역사
GoF는 1994년 Design Patterns에서 Visitor를 행위 패턴으로 정리했다. 객체 구조를 이루는 요소의 클래스와 그 위에서 수행하는 연산을 분리하는 구조다.1
왜 사용하는가?
새 출력이나 통계를 추가할 때 기존 요소의 코드를 매번 바꾸지 않고 방문자를 더할 수 있다. 한 연산에 필요한 요소별 처리가 방문자 하나에 모인다.
대신 새 요소 종류가 생기면 IVisitor와 기존 방문자들을 함께 고쳐야 한다. 요소 종류는 안정적이고 연산 종류가 자주 늘어나는 상황에 맞는다.
예제
문서의 각 요소를 TextVisitor로 출력한다.
using System;
using System.Collections.Generic;
using System.Linq;
IDocNode[] nodes = { new TextNode("hello"), new ImageNode("cover") };
var visitor = new TextVisitor();
foreach (IDocNode node in nodes) node.Accept(visitor);
foreach (string line in visitor.Lines) Console.WriteLine(line);
interface IDocNode { void Accept(IVisitor visitor); }
interface IVisitor
{
void Visit(TextNode node);
void Visit(ImageNode node);
}
sealed record TextNode(string Value) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed record ImageNode(string Name) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed class TextVisitor : IVisitor
{
public List<string> Lines { get; } = new();
public void Visit(TextNode node) => Lines.Add(node.Value);
public void Visit(ImageNode node) => Lines.Add("[image: " + node.Name + "]");
}
sealed class CountVisitor : IVisitor
{
public int Texts { get; private set; }
public int Images { get; private set; }
public void Visit(TextNode node) => Texts++;
public void Visit(ImageNode node) => Images++;
}
hello
[image: cover]호출부의 node는 IDocNode 타입이어서 Visit(node)로는 TextNode·ImageNode용 메서드를 고를 수 없다. 각 Accept()가 자기 구체 타입의 this를 Visit()에 전달해 해당 처리를 연결하므로 호출부가 요소마다 분기할 필요가 없다.
범위 넓힌 예제
요소 클래스는 그대로 두고 CountVisitor를 적용해 텍스트 노드와 이미지 노드 수를 센다.
using System;
using System.Collections.Generic;
using System.Linq;
IDocNode[] nodes = { new TextNode("hello"), new ImageNode("cover"), new TextNode("world") };
var visitor = new CountVisitor();
foreach (IDocNode node in nodes) node.Accept(visitor);
Console.WriteLine("text nodes: " + visitor.Texts);
Console.WriteLine("image nodes: " + visitor.Images);
interface IDocNode { void Accept(IVisitor visitor); }
interface IVisitor
{
void Visit(TextNode node);
void Visit(ImageNode node);
}
sealed record TextNode(string Value) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed record ImageNode(string Name) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed class TextVisitor : IVisitor
{
public List<string> Lines { get; } = new();
public void Visit(TextNode node) => Lines.Add(node.Value);
public void Visit(ImageNode node) => Lines.Add("[image: " + node.Name + "]");
}
sealed class CountVisitor : IVisitor
{
public int Texts { get; private set; }
public int Images { get; private set; }
public void Visit(TextNode node) => Texts++;
public void Visit(ImageNode node) => Images++;
}
text nodes: 2
image nodes: 1순회는 호출부의 foreach가 맡고, 방문자는 각 요소에 할 일을 맡는다. Composite 트리에 붙일 때도 자식 순회 책임을 어디에 둘지 먼저 정해야 중복 방문을 피할 수 있다.
같이 보기
각 예제는 독립된 .NET 10 콘솔 프로그램이다.