방문자 패턴
객체의 구조와 객체에 수행되는 작업을 분리한다. 기존 클래스를 변경하지 않고도 새로운 작업을 정의할 수 있게 한다.
구조
- Visitor - 방문자 클래스들이 구현해야 할 인터페이스. 각 구체적인 요소 클래스(Element)를 방문(visit)하는 메소드를 선언한다.
- Concrete Visitor - Visitor 인터페이스를 구현하는 클래스. 구체적인 요소(Element)들에 대한 방문 동작을 구현한다. 각 요소에 대해 수행할 작업을 정의한다.
- Element - 요소 클래스들이 구현해야 할 인터페이스. 이 인터페이스는 Accept 메소드를 선언하여, 방문자 객체가 요소 객체를 방문할 수 있게 한다.
- Concrete Element - Element 인터페이스를 구현하는 클래스. 실제 데이터와 작업을 포함한다. accept 메소드에서는 자신을 인자로 하여 방문자의 visit 메소드를 호출한다.
- Object Structure - 요소 객체들의 집합. 방문자가 방문할 수 있는 구조를 제공한다.
기본 인터페이스
// Visitor
public interface IVisitor
{
void VisitConcreteElementA(ConcreteElementA elementA);
void VisitConcreteElementB(ConcreteElementB elementB);
}
// ConcreteVisitor1
public class ConcreteVisitor1 : IVisitor
{
public void VisitConcreteElementA(ConcreteElementA elementA)
{
Console.WriteLine($"{elementA.GetType().Name} visited by {this.GetType().Name}");
}
public void VisitConcreteElementB(ConcreteElementB elementB)
{
Console.WriteLine($"{elementB.GetType().Name} visited by {this.GetType().Name}");
}
}
// ConcreteVisitor2
public class ConcreteVisitor2 : IVisitor
{
public void VisitConcreteElementA(ConcreteElementA elementA)
{
// 다른 구현이 가능합니다.
}
public void VisitConcreteElementB(ConcreteElementB elementB)
{
// 다른 구현이 가능합니다.
}
}
// Element
public interface IElement
{
void Accept(IVisitor visitor);
}
// ConcreteElementA
public class ConcreteElementA : IElement
{
public void Accept(IVisitor visitor)
{
visitor.VisitConcreteElementA(this);
}
}
// ConcreteElementB
public class ConcreteElementB : IElement
{
public void Accept(IVisitor visitor)
{
visitor.VisitConcreteElementB(this);
}
}
class Program
{
static void Main(string[] args)
{
// 각 요소 인스턴스 생성
ConcreteElementA elementA = new ConcreteElementA();
ConcreteElementB elementB = new ConcreteElementB();
// 방문자 인스턴스 생성
ConcreteVisitor1 visitor1 = new ConcreteVisitor1();
ConcreteVisitor2 visitor2 = new ConcreteVisitor2();
// 방문자를 각 요소에 적용
elementA.Accept(visitor1);
elementB.Accept(visitor1);
// 결과:
// "ConcreteElementA visited by ConcreteVisitor1"
// "ConcreteElementB visited by ConcreteVisitor1"
elementA.Accept(visitor2);
elementB.Accept(visitor2);
// 결과: ConcreteVisitor2의 구현에 따라 달라질 수 있습니다.
}
}
왜 사용해야 할까?
방문자 패턴은 프로그래밍에서 복잡한 객체 구조에 다양한 연산을 수행하고자 할 때 유용한 디자인 패턴이다. 이 패턴의 핵심 아이디어는 객체 구조와 객체에 대한 작업을 분리하는 것이다. 즉, 객체의 클래스를 변경하지 않고도 새로운 연산을 객체에 추가할 수 있다.
방문자 패턴을 이해하기 위해 일상적인 상황을 상상해보자. 박물관을 방문하는 상황을 생각해보면 좋을 것 같다. 박물관에는 다양한 전시물이 있고, 이 전시물들은 박물관의 구조를 이룬다. 이제 박물관에는 다양한 배경지식을 가진 방문자들이 찾아온다. 이 방문자들은 예술가, 역사학자, 어린이 등 다양하다.
각각의 방문자는 전시물을 자신만의 방식으로 해석하고 경험한다. 예술가는 예술적 관점에서, 역사학자는 역사적 관점에서 전시물을 살펴볼 것이다. 어린이는 또 다른 시각에서 전시물을 경험하겠지. 이렇게 각각의 방문자는 전시물에 다른 "작업"을 수행한다.
박물관의 전시물 자체는 변하지 않는다. 변하는 것은 방문자와 그들이 전시물을 경험하는 방식이다.
이제 이 예를 프로그래밍의 세계로 옮겨보자. 프로그래밍에서, "박물관"은 객체의 집합 또는 복잡한 데이터 구조를 나타낸다. 이 구조 안에는 여러 "전시물"이 있는데, 이것들은 프로그램에서 다루는 다양한 객체들이다.
"방문자"는 이 데이터 구조에 대해 특정 작업을 수행하는 역할을 한다. 방문자 객체는 다양한 타입의 요소들을 '방문'하며, 각 요소에 특정 작업을 수행한다. 예를 들어, 데이터를 저장, 분석, 보고하는 등의 작업을 수행할 수 있다.
방문자 패턴의 장점은 새로운 작업을 추가하려면 새로운 방문자를 만들기만 하면 된다는 것이다. 이는 기존의 객체 구조를 변경하지 않고도 새로운 기능을 추가할 수 있게 해준다. 또한, 데이터 구조와 작업 로직을 분리함으로써, 각각을 독립적으로 변경하고 재사용할 수 있다. 이는 프로그램을 더 유연하고 관리하기 쉽게 만들어 준다.
프로그래밍에서의 한 예시를 들어보겠다.
게임 월드 내 다양한 객체에 대한 상호작용
게임 월드에는 다양한 객체들이 존재한다(예: 캐릭터, 건물, 자연 환경 등). 게임 플레이어 또는 AI가 이 객체들과 상호작용할 때, 각 객체에 대해 수행해야 하는 동작이 다를 수 있다. 예를 들어, 플레이어가 건물에 접근했을 때는 건물을 조사하거나 사용하는 로직이, 나무에 접근했을 때는 나무를 채취하는 로직이 필요할 수 있다. 여기서 각 객체는 'Element'가 되며, 상호작용 로직은 'Visitor'가 된다. 이 방식으로 게임 월드 내 다양한 객체들과의 상호작용을 유연하게 관리할 수 있다.
1. Visitor
public interface IGameElementVisitor
{
void VisitBuilding(Building building);
void VisitTree(Tree tree);
void VisitCharacter(Character character);
}
Visitor는 게임 내 다양한 객체들과의 상호작용을 정의하는 방문자들이 구현해야 하는 메소드들을 제공한다. 이 코드에서 IGameElementVisitor는 VisitBuilding, VisitTree, VisitCharacter 등의 메소드를 통해 구체적인 객체 유형에 대한 상호작용 로직을 정의한다.
2. Concrete Visitor
public class InteractionVisitor : IGameElementVisitor
{
public void VisitBuilding(Building building)
{
// 건물과의 상호작용 로직
Console.WriteLine("Interacting with a building.");
}
public void VisitTree(Tree tree)
{
// 나무와의 상호작용 로직
Console.WriteLine("Interacting with a tree.");
}
public void VisitCharacter(Character character)
{
// 다른 캐릭터와의 상호작용 로직
Console.WriteLine("Interacting with another character.");
}
}
Concrete Visitor는 Visitor 인터페이스를 구현하는 실제 클래스이다. InteractionVisitor 클래스는 게임 내 다양한 요소(예: 건물, 나무, 캐릭터)와의 구체적인 상호작용을 정의한다.
3. Element Interface
public interface IGameElement
{
void Accept(IGameElementVisitor visitor);
}
Element는 Visitor가 Element 객체를 방문할 수 있게 한다. 위의 Element 인터페이스는 게임 내의 모든 상호작용 가능한 요소가 구현해야 하는 인터페이스다. Accept 메소드를 통해 방문자 객체를 받아들여 해당 방문자에 정의된 상호작용을 수행한다. 이를 통해, 각 요소는 방문자에 따라 다른 방식으로 상호작용할 수 있다.
4. Concrete Element
public class Building : IGameElement
{
public void Accept(IGameElementVisitor visitor)
{
visitor.VisitBuilding(this);
}
}
public class Tree : IGameElement
{
public void Accept(IGameElementVisitor visitor)
{
visitor.VisitTree(this);
}
}
public class Character : IGameElement
{
public void Accept(IGameElementVisitor visitor)
{
visitor.VisitCharacter(this);
}
}
IGameElement 인터페이스를 구현하는 구체적인 클래스들이다. Building, Tree, Character 클래스는 게임 내의 건물, 나무, 캐릭터 등을 나타낸다. 각 클래스는 Accept 메소드를 오버라이드하여, 자신의 유형에 맞는 방문자 메소드를 호출한다.
ex) Building 클래스는 VisitBuilding 메소드를 호출한다.
결과
class Program
{
static void Main(string[] args)
{
IGameElement building = new Building();
IGameElement tree = new Tree();
IGameElement character = new Character();
IGameElementVisitor visitor = new InteractionVisitor();
// 각 게임 요소에 방문자를 전달하여 상호작용을 수행한다.
// Accept 메소드는 방문자에 따라 적절한 상호작용을 실행한다.
building.Accept(visitor);
tree.Accept(visitor);
character.Accept(visitor);
// 이 과정에서 building, tree, character 객체는
// InteractionVisitor에 정의된 방식대로 각각 다르게 반응한다.
}
}
장점
- 기존 코드를 변경하지 않고 새로운 코드를 추가할 수 있다.(OCP 보장)
- 추가 기능을 한 곳에 모아둘 수 있다.(SRP 보장)
단점
- 새로운 Element를 추가하거나 제거할 때 모든 Visitor 코드를 변경해야 한다.
전체 예시 코드
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
23. [Design Pattern] 템플릿 메소드 패턴(Templete Method) (0) | 2024.02.28 |
---|---|
22. [Design Pattern] 전략 패턴(Strate) (0) | 2024.02.27 |
21. [Design Pattern] 옵저버 패턴(Observer) (1) | 2024.02.26 |
20. [Design Pattern] 상태 패턴(State) (0) | 2024.02.20 |
19. [Design Pattern] 메멘토(Memento) 패턴 (0) | 2024.02.19 |