1. 상태 패턴이란?
- 객체가 상태에 따라 행위를 다르게 할 때, 직접 상태를 체크하여 상태에 따른 행위를 호출하는 것이 아니라 상태를 객체화하여 필요에 따라 다르게 행동하도록 위임하는 디자인 패턴.
- 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화하여 상태가 행동을 할 수 있도록 위임하는 패턴.
- 객체의 특정 상태를 클래스로 선언하고, 클래스에서는 해당 상태에서 할 수 있는 행위들을 메서드로 정의한다.
- 상태란, 객체가 가질 수 있는 어떤 조건이나 상황을 의미한다.
2. 상태 패턴을 사용하는 경우
- 상태 전이를 위한 로직이 지나치게 복잡한 경우
- 현재 상태에 따라 다르게 행동하는 객체가 있는 경우
- 상태들의 수가 많고, 상태별로 코드가 자주 변경되는 경우
3. 상태 패턴의 장점
- 객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해 낼 수 있고, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.
- 상태(State)에 따른 동작을 개별 클래스로 옮겨서 관리 할 수 있다.
- 상태(State)와 관련된 모든 동작을 각각의 상태 클래스에 분산시킴으로써, 코드 복잡도를 줄일 수 있다.
- 특정 상태와 관련된 코드를 별도의 클래스로 구성하여 단일 책임 원칙을 준수할 수 있다.
- 기존 상태(State) 클래스나 컨텍스트를 변경하지 않고 새로운 상태를 도입할 수 있어, 개방 폐쇄 원칙을 준수할 수 있다.
- 하나의 상태 객체만 사용하여 상태 변경을 하므로 일관성 없는 상태 주입을 방지하는데 도움이 된다.
4. 상태 패턴의 단점
- 상태 별로 클래스를 생성하므로, 관리해야할 클래스의 수가 증가한다.
- 상태 클래스의 갯수가 많고, 규칙이 자주 변경된다면, Context의 상태 변경 코드가 복잡해질 수 있다.
- 객체에 적용할 상태가 몇가지 밖에 없거나 거의 상태 변경이 이루어지지 않는 경우 패턴을 적용하는 것이 과도할 수 있다.
5. 코드 구현
1) 구조
- State 인터페이스
: 상태를 추상화한 고수준 모듈
- Concrete State
: 구체적인 각각의 상태를 클래스로 표현.
: State 역할로 결정되는 인터페이스(API)를 구체적으로 구현.
: 다음 상태가 결정되면 Context에 상태 변경을 요청하는 역할 수행.
- Context
: State를 이용하는 시스템.
: 시스템 상태를 나타내는 State 객체를 합성(Composition)하여 가지고 있음.
: 클라이언트로부터 요청받으면 State 객체에 행위 실행을 위임함.
2) 소스코드(C#)
- 패턴 적용 전
using System;
using System.Collections.Generic;
using System.Threading;
namespace StudyCSharp
{
class Player
{
// 상태를 나타내는 enum 타입
public enum STATE
{
Idle, // 기본 상태
Attack, // 공격 상태
Defense, // 방어 상태
Die, // 죽은 상태
}
// 상태를 저장하는 변수
public STATE myState;
public Player()
{
// 초기상태 저장
Console.WriteLine(">>>플레이어 생성<<<");
this.myState = STATE.Idle;
}
// 상태 변경
void ChangeState(STATE state)
{
this.myState = state;
}
// 데미지를 입음
public void OnDamage()
{
if (myState == STATE.Idle)
{
Console.WriteLine(">>>플레이어 죽음<<<");
ChangeState(STATE.Die);
}
else if (myState == STATE.Attack)
{
Console.WriteLine("상대와 같이 죽음");
ChangeState(STATE.Die);
}
else if (myState == STATE.Defense)
{
Console.WriteLine("막음, 방어 모드 Off");
ChangeState(STATE.Idle);
}
else if (myState == STATE.Die)
{
Console.WriteLine("이미 죽은 상태");
}
}
// 공격 키 입력
public void OnAttack()
{
if (myState == STATE.Idle)
{
Console.WriteLine("공격 개시");
ChangeState(STATE.Attack);
}
else if (myState == STATE.Attack)
{
Console.WriteLine("이미 공격 중");
}
else if (myState == STATE.Defense)
{
Console.WriteLine("공격 상태로 변경");
ChangeState(STATE.Attack);
}
else if (myState == STATE.Die)
{
Console.WriteLine("죽은 상태이므로 공격 불가");
}
}
public void OnDefense()
{
Console.WriteLine("방어 모드 On");
myState = STATE.Defense;
}
// 현재 상태 출력
public void curStatePrint()
{
if (myState == STATE.Idle)
{
Console.WriteLine("기본(Idle) 상태");
}
else if (myState == STATE.Attack)
{
Console.WriteLine("공격(Attack) 상태");
}
else if (myState == STATE.Defense)
{
Console.WriteLine("방어(Defense) 상태");
}
else if (myState == STATE.Die)
{
Console.WriteLine("죽은(Die) 상태");
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("-------------------------");
Player player = new Player();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 공격 진행 : Idle -> Attack
player.OnAttack();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 방어 상태로 변경 : Attack -> Defense
player.OnDefense();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 데미지 입음 -> 막음 -> 방어모드 해제 : Defense -> Idle
player.OnDamage();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 한 번 더 데미지 입음 -> 죽음 : Idle -> Die
player.OnDamage();
player.curStatePrint();
Console.WriteLine("-------------------------");
}
}
}
- 실행 결과
- 패턴 적용 후
using System;
namespace StudyCSharp
{
interface PlayerState
{
void OnAttack(PlayerContext pc);
void OnDamage(PlayerContext pc);
string print();
}
class IdleState : PlayerState
{
#region 싱글톤
private static class SingleInstanceHolder
{
private static IdleState _instance;
public static IdleState Instance
{
get
{
if (_instance == null)
_instance = new IdleState();
return _instance;
}
}
}
public static IdleState getInstance()
{
return SingleInstanceHolder.Instance;
}
#endregion
public void OnAttack(PlayerContext pc)
{
Console.WriteLine("공격 개시");
//pc.ChangeState(new AttackState()); -> 이렇게 하면 매번 새 인스턴스를 생성시켜 메모리를 낭비하게 될 수 있음 -> 싱글톤 패턴 사용
pc.ChangeState(AttackState.getInstance());
}
public void OnDamage(PlayerContext pc)
{
Console.WriteLine(">>>플레이어 죽음<<<");
pc.ChangeState(DieState.getInstance());
}
public string print()
{
return "기본(Idle) 상태";
}
}
class AttackState : PlayerState
{
#region 싱글톤
private static class SingleInstanceHolder
{
private static AttackState _instance;
public static AttackState Instance
{
get
{
if (_instance == null)
_instance = new AttackState();
return _instance;
}
}
}
public static AttackState getInstance()
{
return SingleInstanceHolder.Instance;
}
#endregion
public void OnAttack(PlayerContext pc)
{
Console.WriteLine("이미 공격 중");
}
public void OnDamage(PlayerContext pc)
{
Console.WriteLine("상대와 같이 죽음");
pc.ChangeState(DieState.getInstance());
}
public string print()
{
return "공격(Attack) 상태";
}
}
class DefenseState : PlayerState
{
#region 싱글톤
private static class SingleInstanceHolder
{
private static DefenseState _instance;
public static DefenseState Instance
{
get
{
if (_instance == null)
_instance = new DefenseState();
return _instance;
}
}
}
public static DefenseState getInstance()
{
return SingleInstanceHolder.Instance;
}
#endregion
public void OnAttack(PlayerContext pc)
{
Console.WriteLine("공격 상태로 변경");
pc.ChangeState(AttackState.getInstance());
}
public void OnDamage(PlayerContext pc)
{
Console.WriteLine("막음, 방어모드 Off");
pc.ChangeState(IdleState.getInstance());
}
public string print()
{
return "방어(Defense) 상태";
}
}
class DieState : PlayerState
{
#region 싱글톤
private static class SingleInstanceHolder
{
private static DieState _instance;
public static DieState Instance
{
get
{
if (_instance == null)
_instance = new DieState();
return _instance;
}
}
}
public static DieState getInstance()
{
return SingleInstanceHolder.Instance;
}
#endregion
public void OnAttack(PlayerContext pc)
{
Console.WriteLine("죽은 상태이므로 공격 불가");
}
public void OnDamage(PlayerContext pc)
{
Console.WriteLine("이미 죽은 상태");
}
public string print()
{
return "죽은(Die) 상태";
}
}
class PlayerContext
{
PlayerState myState;
public PlayerContext()
{
Console.WriteLine(">>>플레이어 생성<<<");
myState = new IdleState();
}
public void ChangeState(PlayerState state)
{
myState = state;
}
public void OnDefense()
{
Console.WriteLine("방어 모드");
ChangeState(new DefenseState());
}
public void OnAttack()
{
myState.OnAttack(this);
}
public void OnDamage()
{
myState.OnDamage(this);
}
public void curStatePrint()
{
Console.WriteLine(myState.print());
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("-------------------------");
PlayerContext player = new PlayerContext();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 공격 진행 : Idle -> Attack
player.OnAttack();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 방어 상태로 변경 : Attack -> Defense
player.OnDefense();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 데미지 입음 -> 막음 -> 방어모드 해제 : Defense -> Idle
player.OnDamage();
player.curStatePrint();
Console.WriteLine("-------------------------");
// 한 번 더 데미지 입음 -> 죽음 : Idle -> Die
player.OnDamage();
player.curStatePrint();
Console.WriteLine("-------------------------");
}
}
}
- 실행 결과
<참고 사이트>