☁ 패턴들의 개념과 각 패턴을 적용해서 간단한 예시를 🕹게임 캐릭터를 만들 듯이 C#으로 구현해 보았다.
참고 | GoF 디자인 패턴 총정리
GoF 디자인 패턴의 생성, 구조, 행위 패턴에 대한 총정리는 아래 포스팅을 참고하자!
[CS/Design Pattern] GoF 디자인 패턴 정리
행위 패턴 (Behavioral Pattern)
행위 패턴은 객체나 클래스 사이의 상호작용과 책임 분배 방법을 정의한 패턴이다.
행위 패턴에는 11가지 패턴이 있다.
- 책임 연쇄 (Chain of Responsibility) 패턴
- 커맨드 (Command) 패턴
- 인터프리터 (Interpreter) 패턴
- 반복자 (Iterator) 패턴
- 중재자 (Mediator) 패턴
- 메멘토 (Memento) 패턴
- 옵서버 (Observer) 패턴
- 상태 (State) 패턴
- 전략 (Strategy) 패턴
- 템플릿 메서드 (Template Method) 패턴
- 방문자 (Visitor) 패턴
이 패턴들은 객체 간 상호작용을 효율적으로 관리하고, 코드의 유연성과 확장성을 높여준다.
1. 책임 연쇄 (Chain of Responsibility)
책임 연쇄 패턴은 객체(핸들러)가 요청을 처리하지 못하면 요청이 다음 객체 넘어가는 패턴이다.
즉, 요청을 처리할 수 있는 객체(핸들러)들이 고리(Chain)로 묶여 있어 요청이 해결될 때까지 책임을 넘기는 방식으로, 여러 객체가 요청을 처리할 기회를 가져야 할 때 유용하다.
장점
- 요청을 처리할 객체를 명확히 알지 않아도 된다.
- 요청을 보내는 객체와 처리하는 객체 간의 결합도를 줄일 수 있다.
- 단일 책임 원칙 (SRP)
- 각 핸들러는 하나의 책임만 갖는다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 핸들러 클래스를 추가할 때, 기존 코드 변경 없이 쉽게 확장할 수 있다.
단점
- 요청을 끝까지 처리하지 못하는 상황이 생길 수 있다.
- 디버깅이 어려울 수 있다.
예시 코드 | 책임 연쇄 (Chain of Responsibility)
- 게임에서 종족(고블린, 오크) 핸들러가 여러 종족의 요청을 어떻게 처리하는지 테스트 코드를 작성해 보았다.
- 클라이언트에서는 모든 종족에 대한 요청을 GoblinHandler에 보낸다. '고블린' 요청이 아닐 시, 요청을 처리하지 않고 연결해 둔 OrcHandler에게 전달한다.
- '드래곤' 요청은 모든 핸들러를 거쳐갔지만, 처리를 담당하는 핸들러가 없으므로 끝까지 요청이 처리되지 못했다.
//.# 핸들러 인터페이스
interface IHandler
{
IHandler SetNext(IHandler handler);
void Handle(string request);
}
//.# 베이스 핸들러 클래스
abstract class Handler : IHandler
{
private IHandler _nextHandler;
public IHandler SetNext(IHandler handler)
{
_nextHandler = handler;
return handler;
}
public virtual void Handle(string request)
{
if (_nextHandler != null)
{
_nextHandler.Handle(request);
}
}
}
//.# 특정 핸들러 구현 (고블린, 오크)
class GoblinHandler : Handler
{
public override void Handle(string request)
{
if (request == "고블린")
{
Console.WriteLine("Goblin Handler: 처리 완료 - " + request);
}
else
{
base.Handle(request);
}
}
}
class OrcHandler : Handler
{
public override void Handle(string request)
{
if (request == "오크")
{
Console.WriteLine("Orc Handler: 처리 완료 - " + request);
}
else
{
base.Handle(request);
}
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
var goblinHandler = new GoblinHandler();
var orcHandler = new OrcHandler();
goblinHandler.SetNext(orcHandler); // 요청을 넘길 다음 핸들러 연결!
Console.WriteLine("'고블린' 요청");
goblinHandler.Handle("고블린");
Console.WriteLine("'오크' 요청");
goblinHandler.Handle("오크");
Console.WriteLine("'드래곤' 요청");
goblinHandler.Handle("드래곤");
}
}
2. 커맨드 (Command)
커맨드 패턴은 요청을 객체의 형태로 캡슐화하여, 요청에 사용되는 각종 명령어들을 클래스로 분리하는 패턴이다.
또한, 요청의 매개변수화, 큐잉, 로깅 등을 가능하게 하며, 요청을 예약하거나, 대기열에 넣거나, 되돌리는(Undo) 기능을 구현할 때 유용하다.
장점
- 요청을 큐(Queue)에 저장하거나 실행 및 취소가 가능하다.
- 객체의 매개변수화가 가능하다.
- 단일 책임 원칙 (SRP)
- 각 명령은 하나의 책임만 가진다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 명령을 추가할 때 기존 코드 변경 없이 확장이 가능하다.
단점
- 수신자와 호출자를 연결해야 하기 때문에 코드가 더 복잡해질 수 있다.
예시 코드 | 커맨드 (Command)
- 게임 캐릭터의 행동(점프, 공격 등)을 명령 객체로 처리하는 코드에 적용해 보았다.
- 클라이언트에서는 JumpCommand를 생성하여 Game객체에 설정하고, 명령을 실행한다.
//.# 명령 인터페이스
interface ICommand
{
//. 명령을 실행하는 메서드
void Execute();
}
//.# 리시버(수신자) 클래스 : 실제 명령을 수행
class Player
{
public void Jump()
{
Console.WriteLine("👀플레이어가 점프했습니다.");
}
}
//.# 구체적인 명령 클래스 : ICommand를 구현하며, 플레이어가 점프하도록 "요청"
class JumpCommand : ICommand
{
private Player _player;
public JumpCommand(Player player)
{
_player = player;
}
public void Execute()
{
_player.Jump();
}
}
//.# 인보커(호출자) 클래스 : 명령을 저장하고 실행
class Game
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Player player = new Player();
ICommand jumpCommand = new JumpCommand(player);
Game game = new Game();
game.SetCommand(jumpCommand);
game.PressButton(); // "👀플레이어가 점프했습니다."
}
}
3. 인터프리터 (Interpreter)
인터프리터 패턴은 언어에 문법 표현을 정의하는 패턴이다.
문법 규칙을 클래스로 표현하고, 이를 기반으로 특정 입력을 해석하여 실행한다.
주로 SQL쿼리 처리, 통신 프로토콜, 언어 해석기 등에서 사용된다.
장점
- 복잡한 문법을 단순하게 구현할 수 있다.
- 단일 책임 원칙 (SRP)
- 각 표현식 클래스는 하나의 책임만 가진다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 표현식(문법)을 추가할 때 기존 코드 변경 없이 확장이 가능하다.
단점
- 문법이 복잡해질수록 클래스 수가 많아질 수 있다.
예시 코드 | 인터프리터 (Interpreter)
- 클라이언트에서 AddExpression를 사용하여 두 숫자를 더한 결과를 해석한다.
//.# 표현식 인터페이스
interface IExpression
{
//. 표현식을 해석하는 메서드
int Interpret();
}
//.# 숫자 표현식 클래스 : 주어진 숫자를 반환
class NumberExpression : IExpression
{
private int _number;
public NumberExpression(int number)
{
_number = number;
}
public int Interpret()
{
return _number;
}
}
//.# 더하기 표현식 클래스 : 좌측과 우측 표현식을 더함
class AddExpression : IExpression
{
private IExpression _left;
private IExpression _right;
public AddExpression(IExpression left, IExpression right)
{
_left = left;
_right = right;
}
public int Interpret()
{
return _left.Interpret() + _right.Interpret();
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
IExpression expression = new AddExpression(new NumberExpression(5), new NumberExpression(10));
Console.WriteLine(expression.Interpret()); // 15
}
}
4. 반복자 (Iterator)
반복자 패턴은 자료구조와 같이 접근이 잦은 객체에 대해 동일한 인터페이스를 사용하는 패턴이다.
즉, 집합 객체의 내부 구현을 노출하지 않고 요소들에 접근할 수 있으며, 여러 종류의 집합에 대해 일관된 방식으로 반복할 수 있다.
장점
- 객체들 사이의 의존성과 결합도를 낮춘다.
- 내부 표현을 노출하지 않고 컬렉션을 순회할 수 있다.
- 서로 다른 컬렉션 구조에 동일한 인터페이스 제공한다.
- 단일 책임 원칙 (SRP)
- 반복자(Iterator)는 컬렉션의 순회를 책임진다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 반복자(Iterator)를 추가할 때 기존 코드 변경 없이 확장이 가능하다.
단점
- 컬렉션 구조가 변경되면 반복자(Iterator)도 변경해야 한다.
예시 코드 | 반복자 (Iterator)
- 게임에서 플레이어 목록(Player Collection)에 순차적으로 접근하는 코드에 적용해 보았다.
- 클라이언트에서는 PlayerCollection에 플레이어를 추가하고, PlayerIterator를 사용하여 컬렉션을 순회한다.
//.# 반복자(Iterator) 인터페이스 : 컬렉션(Aggregate)을 순회하는 메서드들을 정의
interface IIterator
{
bool HasNext();
object Next();
}
//.# 컬렉션(Aggregate) 인터페이스 : 반복자(Iterator)를 생성하는 메서드를 정의
interface IAggregate
{
IIterator CreateIterator();
}
//.# 구체적인 컬렉션 클래스 : 플레이어 목록을 저장하는 컬렉션
class PlayerCollection : IAggregate
{
private List<string> _players = new List<string>();
public void AddPlayer(string player)
{
_players.Add(player);
}
public IIterator CreateIterator()
{
return new PlayerIterator(this);
}
public int Count
{
get { return _players.Count; }
}
public string this[int index]
{
get { return _players[index]; }
}
}
//.# 구체적인 반복자 클래스 : PlayerCollection을 순회함
class PlayerIterator : IIterator
{
private PlayerCollection _collection;
private int _index;
public PlayerIterator(PlayerCollection collection)
{
_collection = collection;
_index = 0;
}
public bool HasNext()
{
return _index < _collection.Count;
}
public object Next()
{
return _collection[_index++];
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
PlayerCollection players = new PlayerCollection();
players.AddPlayer("플레이어1");
players.AddPlayer("플레이어2");
players.AddPlayer("플레이어5");
IIterator iterator = players.CreateIterator();
while (iterator.HasNext())
{
Console.WriteLine(iterator.Next());
// 출력:
// 플레이어1
// 플레이어2
// 플레이어5
}
}
}
5. 중재자 (Mediator)
중재자 패턴은 수많은 객체들 간의 복잡한 상호작용을 캡슐화하여 객체로 정의하는 패턴이다.
즉, 객체들 간의 직접적인 통신을 방지하고 중재자 객체를 통해 간접적으로 상호작용할 수 있도록 하므로, 객체 간 결합도를 낮추고 재사용성을 높이는 데 유용하다.
장점
- 객체 간의 결합도를 낮춤.
- 단일 책임 원칙 (SRP)
- 중재자는 객체 간의 통신을 한 곳에서 간단하게 관리한다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 객체를 추가할 때 중재자를 변경하지 않고 확장이 가능하다.
단점
- 중재자 자체가 복잡해질 수 있다.
예시 코드 | 중재자 (Mediator)
- 게임에서 플레이어들이 메시지를 주고받는 코드에 적용해 보았다.
- 클라이언트에서는 ChatMediator를 생성하고, 여러 플레이어를 추가하여 중재자를 통해 메시지를 전송한다.
//.# 중재자(Mediator) 인터페이스 : 메시지를 전송하고 플레이어를 추가하는 메서드를 정의
interface IChatMediator
{
void SendMessage(string message, Player player);
void AddPlayer(Player player);
}
//.# 중재자(Mediator) 클래스 : 중재자 역할을 하며, 플레이어 목록을 관리하고 메시지를 전달함
class ChatMediator : IChatMediator
{
private List<Player> _players = new List<Player>();
public void AddPlayer(Player player)
{
_players.Add(player);
}
public void SendMessage(string message, Player player)
{
foreach (var p in _players)
{
if (p != player)
{
p.Receive(message);
}
}
}
}
//.# 플레이어 클래스 : 중재자를 통해 다른 플레이어와 통신함
class Player
{
private IChatMediator _mediator;
public string Name { get; private set; }
public Player(string name, IChatMediator mediator)
{
Name = name;
_mediator = mediator;
_mediator.AddPlayer(this);
}
public void Send(string message)
{
Console.WriteLine($"🔸{Name}가 보냄: {message}");
_mediator.SendMessage(message, this);
}
public void Receive(string message)
{
Console.WriteLine($"🔹{Name}가 받음: {message}");
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
IChatMediator mediator = new ChatMediator();
Player player1 = new Player("플레이어1", mediator);
Player player2 = new Player("플레이어2", mediator);
Player player3 = new Player("플레이어3", mediator);
player1.Send("안녕하세요! 플레이어1입니다!");
// 출력:
// 🔸플레이어1가 보냄: 안녕하세요! 플레이어1입니다!
// 🔹플레이어2가 받음: 안녕하세요! 플레이어1입니다!
// 🔹플레이어3가 받음: 안녕하세요! 플레이어1입니다!
}
}
6. 메멘토 (Memento)
메멘토 패턴은 복원을 위해 특정 시점에서 객체 내부 상태를 객체화해서 저장하는 패턴이다.
즉, 객체의 상태를 저장소(메멘토)에 저장하고, 나중에 이를 사용하여 객체를 이전 상태로 복원할 수 있다.
Ctrl + Z와 같은 되돌리기(Undo) 기능을 개발할 때 유용하다.
장점
- 객체의 상태를 손쉽게 저장 및 복원할 수 있다.
- 객체의 내부 상태를 외부에 노출하지 않는다.
- 단일 책임 원칙 (SRP)
- 메멘토는 객체의 상태 저장만을 책임진다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 상태를 추가할 때 메멘토 클래스를 변경하지 않고 확장이 가능하다.
단점
- 메멘토들을 자주 생성하면 많은 메모리를 사용할 수 있다.
예시 코드 | 메멘토 (Memento)
- 게임에서 "전투를 마친" 캐릭터의 체력 상태를 "전투 전"으로 복원하는 코드에 적용해 보았다.
//.# 메멘토 클래스 : 플레이어의 상태(체력)를 저장
class Memento
{
public int Health { get; private set; }
public Memento(int health)
{
Health = health;
}
}
//.# 플레이어 클래스 : 상태를 저장하고 복원하는 메서드를 제공
class Player
{
public int Health { get; set; }
public Memento SaveState()
{
return new Memento(Health);
}
public void RestoreState(Memento memento)
{
Health = memento.Health;
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Player player = new Player { Health = 100 };
Console.WriteLine($"🔸현재 체력: {player.Health}💚");
Memento savedState = player.SaveState(); // 메멘토에 저장!
player.Health -= 50; // 체력 잃음!
Console.WriteLine($"🏹전투 후 체력: {player.Health}💛");
player.RestoreState(savedState);
Console.WriteLine($"💾복원 후 체력: {player.Health}💚");
// 출력:
// 🔸현재 체력: 100💚
// 🏹전투 후 체력: 50💛
// 💾복원 후 체력: 100💚
}
}
7. 옵서버 (Observer)
옵서버 패턴은 객체의 변화를 상속되어 있는 다른 객체들에게 변화된 상태를 알리는 패턴이다.
"구독" 메커니즘을 이용할 수 있도록 하며, 주로 분산된 시스템 간에 이벤트를 생성/발행/수신해야 할 때 유용하다.
🔎 TMI
구독을 당하는, 상태변화를 관찰당하는 객체를 "주제"라고 부르고, "주제"를 구독하여 변화에 대한 알림을 받는 객체들을 "옵서버"라 부른다.
장점
- 상태 변화에 대한 알림을 자동으로 처리해 준다.
- 객체 간의 결합도를 낮춘다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 옵서버를 추가할 때 기존 코드 변경 없이 확장이 가능하다.
단점
- 많은 옵서버가 있을 경우 성능 저하 문제가 발생할 수 있다.
예시 코드 | 옵서버 (Observer)
- 게임에서 게임 상태(게임 시작, 게임 일시정지)가 변할 때, 구독한 플레이어에게 알림을 보내는 기능을 구현해 보았다.
- 플레이어3은 구독을 안 했으므로 알림을 받지 못한다.
//.# 옵서버 인터페이스 : 갱신 메서드 Update를 정의
interface IObserver
{
void Update();
}
//.# 주제 인터페이스 : 옵서버를 등록/해제하고 알림을 보내는 메서드를 정의
interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
//.# 구체적인 주제 클래스 : 상태를 가지고 있으며, "상태 변화 시 옵서버들에게 알림"
class Game : ISubject
{
private List<IObserver> _observers = new List<IObserver>();
private string _gameState;
public string GameState
{
get { return _gameState; }
set
{
_gameState = value;
Notify();
}
}
//. 등록 (구독)
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
//. 해제
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
//. 옵서버들에게 알림
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update();
}
}
}
//.# 구체적인 옵서버 클래스 : 게임 상태 변화를 통보받아 처리
class Player : IObserver
{
private string _name;
private Game _game;
public Player(string name, Game game)
{
_name = name;
_game = game;
}
public void Update()
{
Console.WriteLine($"🔸{_name}가 새로운 게임 상태를 받았습니다: {_game.GameState}");
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Game game = new Game();
Player player1 = new Player("플레이어1", game);
Player player2 = new Player("플레이어2", game);
Player player3 = new Player("플레이어3", game);
game.Attach(player1); // 구독
game.Attach(player2); // 구독
//. 주제의 상태 변경
game.GameState = "❕게임 시작";
game.GameState = "⏸게임 일시정지";
// 출력:
// 🔸플레이어1가 새로운 게임 상태를 받았습니다: ❕게임 시작
// 🔸플레이어2가 새로운 게임 상태를 받았습니다: ❕게임 시작
// 🔸플레이어1가 새로운 게임 상태를 받았습니다: ⏸게임 일시정지
// 🔸플레이어2가 새로운 게임 상태를 받았습니다: ⏸게임 일시정지
}
}
8. 상태 (State)
상태 패턴은 객체의 "상태"에 따라 동일한 동작을 다르게 처리하는 패턴이다.
즉, 객체가 다양한 상태를 가질 때 각 상태마다 특정한 행위를 정의할 수 있으므로, 캐릭터(객체)의 상태(대기, 걷기, 달리기 등)와 같이 현재 상태에 따라 다르게 행동하는 객체가 있을 때 유용하다..
장점
- 상태별 행동을 캡슐화하여, 객체의 상태 관리를 단순화할 수 있다.
- 단일 책임 원칙 (SRP)
- 각 상태는 하나의 행동을 책임진다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 상태를 추가할 때 기존 코드 변경 없이 확장이 가능하다.
단점
- 상태가 많아질 경우 클래스 수가 증가할 수 있다.
예시 코드 | 상태 (State)
- 게임에서 캐릭터의 상태(건강, 부상, 사망)에 따라 다른 행위를 수행하는 코드를 작성해 보았다.
- 클라이언트는 플레이어의 상태를 변화시키며, 상태에 따른 행동을 수행한다.
//.# 상태 인터페이스 : 상태 전환을 처리하는 메서드 Handle을 정의
interface IState
{
void Handle(Player player);
}
//.# 플레이어 클래스 : 현재 상태를 가지며, 상태 전환 요청을 처리
class Player
{
public IState State { get; set; }
public Player(IState state)
{
State = state;
}
public void Request()
{
State.Handle(this);
}
}
//.# 구체적인 상태 클래스 : HealthyState, WoundedState, DeadState 클래스는 각각의 상태를 나타내며, 상태 전환 로직을 구현
class HealthyState : IState
{
public void Handle(Player player)
{
Console.WriteLine("💛플레이어가 건강합니다.");
player.State = new WoundedState(); // 부상 상태로 전환 요청
}
}
class WoundedState : IState
{
public void Handle(Player player)
{
Console.WriteLine("🏹플레이어가 부상당했습니다.");
player.State = new DeadState(); // 사망 상태로 전환 요청
}
}
class DeadState : IState
{
public void Handle(Player player)
{
Console.WriteLine("💀플레이어가 죽었습니다.");
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Player player = new Player(new HealthyState());
player.Request(); // 💛플레이어가 건강합니다.
player.Request(); // 🏹플레이어가 부상당했습니다.
player.Request(); // 💀플레이어가 죽었습니다.
}
}
9. 전략 (Strategy)
전략 패턴은 동일한 계열의 알고리즘들을 캡슐화하여 상호교환하는 패턴이다.
클라이언트는 독립적으로 원하는 알고리즘을 선택하여 사용할 수 있다.
즉, 다양한 알고리즘을 정의하고, 클라이언트가 런타임에 이를 선택하여 사용하도록 할 때 유용하다.
장점
- 클라이언트가 런타임에 알고리즘을 쉽게 교체할 수 있다.
- 코드의 중복을 줄일 수 있다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 전략을 추가할 때 기존 코드 변경 없이 확장이 가능하다.
단점
- 클라이언트가 전략을 이해하고 선택해야 한다.
예시 코드 | 전략 (Strategy)
- 게임에서 플레이어가 여러 공격 전략(검 사용, 활 사용 등)을 펼치는 코드에 적용해 보았다.
- 컨텍스트 클래스(아래 예시에서는 Player클래스를 가리킴)는 알고리즘을 실행해야 할 때마다 연결된 전략 객체의 실행 메서드를 호출한다. 컨텍스트 클래스는 알고리즘이 어떻게 실행되는지와 자신이 어떤 유형의 전략과 함께 작동하는지를 모른다.
- 클라이언트는 플레이어의 공격 전략을 변경하며, 각 전략에 따른 공격을 수행한다.
//.# 전략 인터페이스 : 공격 행위를 정의
interface IAttackStrategy
{
void Attack();
}
//.# 구체적인 전략 클래스 : 각각의 공격 방식을 구현
class SwordAttack : IAttackStrategy
{
public void Attack() { Console.WriteLine("🔪검으로 공격!"); }
}
class BowAttack : IAttackStrategy
{
public void Attack() { Console.WriteLine("🏹활로 공격!"); }
}
//.# 컨텍스트 클래스 : 현재 공격 전략을 가지고 있으며, 공격 요청을 처리함
class Player
{
private IAttackStrategy _attackStrategy;
public void SetAttackStrategy(IAttackStrategy attackStrategy)
{
_attackStrategy = attackStrategy;
}
public void Attack()
{
_attackStrategy.Attack();
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Player player = new Player();
player.SetAttackStrategy(new SwordAttack());
player.Attack(); // 🔪검으로 공격!
//.# 전략 변경
player.SetAttackStrategy(new BowAttack());
player.Attack(); // 🏹활로 공격!
}
}
10. 템플릿 메서드 (Template Method)
템플릿 메서드 패턴은 상위 클래스에서 알고리즘의 구조를 정의하고, 하위 클래스에서 구체적으로 처리하는 패턴이다.
다시 말해, 유사한 서브 클래스들을 묶어 공통된 내용을 상위 클래스에서 정의함으로써, 코드의 양을 줄이고 유지보수를 용이하게 한다.
알고리즘의 구조는 고정되어 있지만, 일부 구체적인 단계는 서브 클래스에서 다르게 구현하고 싶을 때 유용하다.
장점
- 알고리즘의 구조를 쉽게 재사용한다.
- 서브클래스에서 세부 단계를 구현할 수 있다.
- 단일 책임 원칙 (SRP)
- 템플릿 메서드는 알고리즘의 뼈대를 정의한다.
- 개방-폐쇄 원칙 (OCP)
- 알고리즘의 세부 단계를 변경할 때 템플릿 메서드를 변경하지 않고 확장이 가능하다.
단점
- 알고리즘이 고정되어 유연성이 떨어질 수 있다.
예시 코드 | 템플릿 메서드 (Template Method)
- 게임 진행 순서를 차례대로 구현하는 코드에 적용해 보았다.
- 클라이언트에서는 PuzzleGame 객체를 생성하고, Play 메서드를 호출해서 게임을 차례대로 진행한다.
//.# 템플릿 메소드 클래스 : 게임 진행의 템플릿 메서드 Play를 정의
abstract class Game
{
public void Play()
{
Initialize();
StartPlay();
EndPlay();
}
protected abstract void Initialize();
protected abstract void StartPlay();
protected abstract void EndPlay();
}
//.# 구체적인 클래스 : 초기화, 시작, 종료 단계의 구체적인 구현
class PuzzleGame : Game
{
protected override void Initialize() { Console.WriteLine("🔄퍼즐 게임 초기화"); }
protected override void StartPlay() { Console.WriteLine("✅퍼즐 게임 시작"); }
protected override void EndPlay() { Console.WriteLine("⏹퍼즐 게임 종료"); }
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Game game = new PuzzleGame();
game.Play();
// 🔄퍼즐 게임 초기화
// ✅퍼즐 게임 시작
// ⏹퍼즐 게임 종료
}
}
11. 방문자 (Visitor)
방문자 패턴은 클래스들의 데이터 구조에서 처리 기능을 분리하고, 분리된 처리기능은 각 클래스를 "방문"하여 수행하는 패턴이다.
객체 구조와 처리 작업의 결합을 없애고 유지보수성을 높이는 데 사용된다.
장점
- 단일 책임 원칙 (SRP)
- 방문자(Visitor)는 객체에 대한 새로운 기능을 추가한다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 기능을 추가할 때 기존 객체 구조를 변경하지 않고 확장이 가능하다.
단점
- 객체 구조가 자주 변경되면 방문자 클래스를 수정해야 한다.
예시 코드 | 방문자 (Visitor)
- 게임에서 방문자를 이용해 플레이어와 적의 체력을 출력하는 코드에 적용해 보았다.
- 클라이언트는 HealthCheckVisitor를 생성하여 플레이어와 적의 체력을 확인한다.
//.# 방문자(Visitor) 인터페이스 : 각 요소에 대해 방문할 메서드를 정의
interface IVisitor
{
void Visit(Player player);
void Visit(Enemy enemy);
}
//.# 방문자 클래스 : 플레이어와 적의 체력을 확인하는 방문자 구현
class HealthCheckVisitor : IVisitor
{
public void Visit(Player player)
{
Console.WriteLine($"😀플레이어의 체력: {player.Health}");
}
public void Visit(Enemy enemy)
{
Console.WriteLine($"👽적의 체력: {enemy.Health}");
}
}
//.# 방문 가능 인터페이스 : 방문자를 받아들이는 메서드 Accept를 정의
interface IVisitable
{
void Accept(IVisitor visitor);
}
//.# 구체적인 요소 클래스 : 방문이 가능한 요소들로, 방문자를 받아들임
class Player : IVisitable
{
public int Health { get; set; }
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
class Enemy : IVisitable
{
public int Health { get; set; }
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
//.# 클라이언트
class Program
{
static void Main(string[] args)
{
Player player = new Player { Health = 100 };
Enemy enemy = new Enemy { Health = 80 };
IVisitor healthCheck = new HealthCheckVisitor();
player.Accept(healthCheck); // 😀플레이어의 체력: 100
enemy.Accept(healthCheck); // 👽적의 체력: 80
}
}
참고
https://refactoring.guru/ko/design-patterns/behavioral-patterns
'💾 Computer Science > Software Engineering' 카테고리의 다른 글
[CS/Design Pattern] GoF 디자인 패턴 | 구조 패턴(Structural Pattern) 7가지 정리 (0) | 2024.07.15 |
---|---|
[CS/Design Pattern] GoF 디자인 패턴 | 생성 패턴(Creational Pattern) 5가지 정리 (0) | 2024.07.10 |
[CS/Design Pattern] GoF 디자인 패턴 정리 (0) | 2024.07.09 |