전체 페이지뷰

2017년 4월 17일 월요일

State pattern

Strategy 패턴과 State 패턴은 유사합니다. 스트래티지 패턴은 알고리즘을 바꿔서 사용하게 도와주지만 스테이트 패턴은 내부 상태를 바꿈으로써 객체의 행동을 바꾸도록 해 준다는 것이 다를 뿐입니다.
그것을 설명하기 위해서 먼저 간단한 뽑기 기계를 생각해 봅니다.

기계는 간단합니다.  동전을 넣고 손잡이를 돌려서 속에 있는 구슬을 파는 흔히 보는 뽑기 기계입니다.
(너무 허접해서 그림은 그리기 싫었는데 스토리 전개 상 너무 중요해서 역시 어쩔 수가 없었네요...ㅠㅠ)

이 다이어그램은 상태 다이어그램입니다. 동전 없음은 기계의 시작 상태가 될 것이고, 동전을 넣으면 동전 있음 상태로 바뀌게 됩니다. 그리고 손잡이를 돌리면 구슬 판매 상태로 바뀌며 구슬을 내보낼 때 남은 구슬이 있다면 동전 없음 상태로 가게되고, 구슬이 다 팔려서 남은 것이 없다면 구슬 매진 상태가 됩니다.

상태 기계의 기초


이제 이 다이어그램을 가지고 어떻게 실제 코드를 작성할 수 있을지 알아봅니다.

1. 상태를 모아봅니다.
상태는 총 4개: 동전없음, 동전있음, 구슬매진, 구슬판매

2. 현재 상태를 저장하기 위한 인스턴스 변수를 만들고 각 상태 값을 정의합니다.
const int SOLD_OUT = 0;
const int NO_QUARTER = 1;
const int HAS_QUARTER = 2;
const int SOLD = 3;
int state = SOLD_OUT;
cs
순서 대로 구슬매진, 동전없음, 동전있음, 구슬판매를 나타냅니다. 그리고 현재 상태를 저장하기 위해 state라는 인스턴스를 만들고 일단 초기 상태는 매진으로 둡니다.

3. 이 시스템에서 일어날 수 있는 행동을 모아봅니다:
동전 투입, 손잡이 돌림, 동전 반환, 구슬 내보냄

4. 이제 상태 기계 역할을 하는 클래스를 만듭니다. 각 행동 구현시에는 조건문을 사용해서 상태에 따른 행동을 결정하도록 합니다.

코드 구현

위의 조건에 맞게 직접 코드를 구현해 보도록 하겠습니다.
        public class GambleMachine
        {
            const int SOLD_OUT = 0;
            const int NO_QUARTER = 1;
            const int HAS_QUARTER = 2;
            const int SOLD = 3;
            int state = SOLD_OUT;
            int count = 0;
            public GambleMachine(int count)
            {
                this.count = count;
                if (count > 0)
                {
                    state = NO_QUARTER;
                }
            }
            public void InsertQuarter()
            {
                if (state == HAS_QUARTER)
                {
                    Console.WriteLine("동전은 한 개만 넣어 주세요.");
                }
                else if (state == NO_QUARTER)
                {
                    state = HAS_QUARTER;
                    Console.WriteLine("동전을 넣으셨습니다.");
                }
                else if (state == SOLD_OUT)
                {
                    Console.WriteLine("매진되었습니다. 다음 기회에...");
                }
                else if (state == SOLD)
                {
                    Console.WriteLine("잠시만 기다려 주세요. 구슬이 나가는 중입니다.");
                }
            }
            public void EjectQuarter()
            {
                if (state == HAS_QUARTER)
                {
                    Console.WriteLine("동전이 반환됩니다.");
                    state = NO_QUARTER;
                }
                else if (state == NO_QUARTER)
                {
                    Console.WriteLine("동전을 넣어주세요.");
                }
                else if (state == SOLD)
                {
                    Console.WriteLine("이미 구슬을 뽑으셨습니다.");
                }
                else if (state == SOLD_OUT)
                {
                    Console.WriteLine("동전을 넣지 않으셨습니다. 동전이 반환되지 않습니다.");
                }
            }
            public void TurnCrank()
            {
                if (state == SOLD)
                {
                    Console.WriteLine("손잡이는 한 번만 돌려주세요.");
                }
                else if (state==NO_QUARTER)
                {
                    Console.WriteLine("동전을 넣어주세요.");
                }
                else if (state == SOLD_OUT)
                {
                    Console.WriteLine("매진되었습니다.");
                }
                else if (state == HAS_QUARTER)
                {
                    Console.WriteLine("손잡이를 돌렸습니다.");
                    state = SOLD;
                    Dispense();
                }
            }
            public void Dispense()
            {
                if (state == SOLD)
                {
                    Console.WriteLine("구슬이 나가고 있습니다.");
                    count -= 1;
                    if (count == 0)
                    {
                        Console.WriteLine("더 이상 구슬이 없습니다.");
                        state = SOLD_OUT;
                    }
                    else
                    {
                        state = NO_QUARTER;
                    }
                }
                else if (state == NO_QUARTER)
                {
                    Console.WriteLine("동전을 넣어주세요.");
                }
                else if (state == SOLD_OUT)
                {
                    Console.WriteLine("매진입니다.");
                }
                else if (state == HAS_QUARTER)
                {
                    Console.WriteLine("구슬이 나갈 수 없습니다.");
                }
            }
            public override string ToString()
            {
                StringBuilder result = new StringBuilder();
                result.Append("\nMighty Gumball, Inc.");
                result.Append("\nC#으로 돌아가는 2017년식 뽑기 기계\n");
                result.Append("남은 개수: " + count + " 개\n");
                
                if (state == SOLD_OUT)
                {
                    result.Append("매진");
                }
                else if (state == NO_QUARTER)
                {
                    result.Append("동전 투입 대기 중");
                }
                else if (state == HAS_QUARTER)
                {
                    result.Append("손잡이 돌리기 대기 중");
                }
                else if (state == SOLD)
                {
                    result.Append("구슬 나오길 기다리는 중");
                }
                result.Append("\n");
                return result.ToString();
            }
        }
cs


상태 판정이 복잡하긴 하지만 그렇게 어려운 코드는 아닙니다. 테스트를 해 보도록 하겠습니다.
        static void Main(string[] args)
        {
            GambleMachine gumballMachine = new GambleMachine(5);
            Console.WriteLine(gumballMachine.ToString());
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
            Console.WriteLine(gumballMachine.ToString());
            gumballMachine.InsertQuarter();
            gumballMachine.EjectQuarter();
            gumballMachine.TurnCrank();
            Console.WriteLine(gumballMachine.ToString());
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
            gumballMachine.EjectQuarter();
            Console.WriteLine(gumballMachine.ToString());
            gumballMachine.InsertQuarter();
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
            Console.WriteLine(gumballMachine.ToString());
        }
cs

결과)

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 5 개
동전 투입 대기 중

동전을 넣으셨습니다.
손잡이를 돌렸습니다.
구슬이 나가고 있습니다.

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 4 개
동전 투입 대기 중

동전을 넣으셨습니다.
동전이 반환됩니다.
동전을 넣어주세요.

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 4 개
동전 투입 대기 중

동전을 넣으셨습니다.
손잡이를 돌렸습니다.
구슬이 나가고 있습니다.
동전을 넣으셨습니다.
손잡이를 돌렸습니다.
구슬이 나가고 있습니다.
동전을 넣어주세요.

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 2 개
동전 투입 대기 중

동전을 넣으셨습니다.
동전은 한 개만 넣어 주세요.
손잡이를 돌렸습니다.
구슬이 나가고 있습니다.
동전을 넣으셨습니다.
손잡이를 돌렸습니다.
구슬이 나가고 있습니다.
더 이상 구슬이 없습니다.
매진되었습니다. 다음 기회에...
매진되었습니다.

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 0 개
매진

계속하려면 아무 키나 누르십시오 . . .


역시 또 변경 요청


역시 그냥 넘어갈 리가 없죠. 이 단계에서는 별 문제없이 작동하는 코드입니다만 새로운 요청이 들어왔습니다. 열번에 한번 꼴로 찬스 기능, 그러니까 구슬이 두 개 나오도록 하는 게임 기능을 넣어달라고 합니다.

그렇게 하려면 상태 변수에 WINNER를 추가하고, 내부에 들어간 조건문을 거의 다 손을 보아서 WINNER도 판정해야 합니다. 손이 너무 많이 갑니다. 지금 만들어진 코드는 객체지향이라고 보기엔 어려우며, 상태 전환이 조건문 속에 숨어 쉽게 보이지 않고, 바뀌는 부분이 캡슐화 되어 있지도 않습니다.

대대적인 리팩토링이 필요할 듯 싶습니다.

새로운 디자인

지금까지 배워온 디자인 원칙을 지켜 상태 객체들을 별도의 코드에 집어 넣고 어떤 행동이 일어나면 현재 상태 객체에서 필요한 작업을 수행하게 하는 쪽으로 다시 코드를 작성해 보려고 합니다.

그러기 위해서는,

  1. 뽑기 기계와 관련된 모든 행동에 대한 메소드가 들어 있는 State 인터페이스를 정의해야 합니다.
  2. 그 다음, 기계의 어떤 상테에 대해서 상태 클래스를 구현해야 합니다. 기계가 어떤 상태에 있다면 그 상태에 해당하는 상태 클래스가 모든 작업을 책임져야 합니다.
  3. 조건문을 모두 없애고 상태 클래스에 모든 작업을 위임합니다.



우선 모든 상태 클래스에서 구현할 State 인터페이스를 만들고 각 상태에 따른 행동을 구현하기 위해 각 상태 클래스의 메소드가 할 일을 생각해 봅시다.

InsertQuarter: HasQuarterState로 전환
EjectQuarter: 동전을 넣어달라는 메세지 출력
TurnCrank: 동전이 없다는 메세지 출력
Dispense: 동전을 넣어야 한다는 메세지 출력


InsertQuarter: 동전을 두개 넣을 수 없다는 메세지 출력
EjectQuarter:  동전 반환하고 NoQuarterState로 전환
TurnCrank: SoldState로 전환
Dispense: 구슬이 나가지 않았다는 메세지 출력


InsertQuarter: 구슬이 나가는 중이니 기다리라는 메세지 출력
EjectQuarter: 이미 손잡이를 돌렸다는 메세지 출력
TurnCrank: 손잡이는 한번만 돌려달라는 메세지 출력
Dispense: 구슬을 하나 내보내고, 구슬 개수를 확인해 그 값이 0보다 크면 NoQuarterState, 0이면 SoldOutState로 전환

InsertQuarter: 매진되었다는 메세지 출력
EjectQuarter: 동전을 넣지 않았음을 알림
TurnCrank: 구슬이 없다는 메세지 출력
Dispense: 구슬이 나가지 않았음을 알림


덧붙이게 될 WinnerState에 대해서도 생각해 봅니다.

InsertQuarter: 구슬이 나가고 있으니 기다리라는 메세지 출력
EjectQuarter: 이미 손잡이를 돌렸다는 메세지 출력
TurnCrank: 구슬이 나가는 중이니 기다리라는 메세지 출력
Dispense: 구슬을 두개 내보냄. 남은 구슬이 한개 이상이면 NoQuarterState, 아니면 SoldOutState로 전환


State 인터페이스

public interface State
{
    void InsertQuarter();
    void EjectQuarter();
    void TurnCrank();
    void Dispense();
}
cs

전체 상태를 대표하는 인터페이스 입니다. 이것을 기반으로 각 상태를 구현해 보도록 합니다.

NoQuarterState

public class NoQuarterState : State
{
    GumballMachine gumballMachine;
    public NoQuarterState(GumballMachine gumballMachine)
    {
        this.gumballMachine = gumballMachine;
    }
     public void InsertQuarter()
    {
        Console.WriteLine("동전을 넣으셨습니다.");
        gumballMachine.SetState(gumballMachine.GetHasQuarterState());
    }
    public void EjectQuarter()
    {
        Console.WriteLine("동전을 넣어주세요.");
    }
    public void TurnCrank()
    {
        Console.WriteLine("동전을 넣어주세요.");
    }
    public void Dispense()
    {
        Console.WriteLine("동전을 넣어주세요.");
    }
}
cs

생성자를 통해 기계에 대한 레퍼런스가 전달됩니다. InsertQuarter()에서 HasQuarterState로 전환하는 아직 작성하지 않은 메소드를 사용했습니다.


GumballMachine 수정

상태 클래스들을 다 작성하기 전에 뽑기 기계도 고쳐야 잘 작동하는지 알 수 있을 것입니다. 인스턴스 변수들을 상태 객체로 바꿔야 하겠습니다.

 public class GumballMachine
{
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    State state;
    int count = 0;
    public GumballMachine(int numberGumballs)
    {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        state = soldOutState;
        this.count = numberGumballs;
        if (numberGumballs > 0)
        {
            state = noQuarterState;
        }
    }
    public void InsertQuarter()
    {
        state.InsertQuarter();
    }
    public void EjectQuarter()
    {
        state.EjectQuarter();
    }
    public void TurnCrank()
    {
        state.TurnCrank();
        state.Dispense();
    }
    public void SetState(State state)
    {
        this.state = state;
    }
            
    public void ReleaseBall()
    {
        Console.WriteLine("구슬이 나오는 중입니다.");
        if(count != 0)
        {
            count -= 1;
        }
    }
    public int GetCount()
    {
        return count;
    }
    public State GetState()
    {
        return state;
    }            
    public State GetSoldOutState()
    {
        return soldOutState;
    }
    public State GetNoQuarterState()
    {
        return noQuarterState;
    }
    public State GetHasQuarterState()
    {
        return hasQuarterState;
    }
    public State GetSoldState()
    {
        return soldState;
    }           
            
    public override string ToString()
    {
        StringBuilder result = new StringBuilder();
        result.Append("\nMighty Gumball, Inc.");
        result.Append("\nC#으로 돌아가는 2017년식 뽑기 기계\n");
        result.Append("남은 개수: " + count + " 개\n");
        result.Append("기계 상태: " + state);
        result.Append("\n");
        return result.ToString();
    }            
}
cs

상태 객체들을 선언해주고 일단 처음에는 SoldOutState로 설정해 주었습니다. 메소드는 이제 각 상태 클래스들이 책임지도록 하여 대폭 간소화 되었습니다. 그리고, 현재 상태를 처리하기 위한 GetCount, GetState, SetState 등의 메소드와 각 상태 객체를 가져오기 위한 게터 메소드들이 첨가되었습니다. ReleaseBall 메소드에서 구슬을 내보내고 숫자를 하나 줄이는 코드가 들어갑니다.

이제 나머지 상태들을 마저 작성해 보도록 합니다.

HasQuarterState

public class HasQuarterState :State
{
    GumballMachine gumballMachine;
 
    public HasQuarterState(GumballMachine gumballMachine)
    {
        this.gumballMachine = gumballMachine;
    }
 
    public void InsertQuarter()
    {
        Console.WriteLine("동전은 한 개만 넣어주세요.");
    }
 
    public void EjectQuarter()
    {
        Console.WriteLine("동전이 반환됩니다.");
        gumballMachine.SetState(gumballMachine.GetNoQuarterState());
    }
 
    public void TurnCrank()
    {
        Console.WriteLine("손잡이를 돌리셨습니다.");
        gumballMachine.SetState(gumballMachine.GetSoldState());
    }
 
    public void Dispense()
    {
        Console.WriteLine("구슬이 나갈 수 없습니다.");
    }
 
    public override string ToString()
    {
        return "손잡이를 돌리길 기다리는 중";
    }
}
cs

방식은 모두 비슷합니다. 다만 상태에 따라 메소드가 해야할 일이 다를 뿐입니다.
SoldState, SoldOutState도 마찬가지로 작성해 줍니다(아래에 전체 코드가 소개됩니다)

스테이트 패턴의 정의

지금까지 한 일을 정리해 보면,
  • 각 상태의 행동을 별개의 클래스로 바꿈
  • if 선언문들을 없앰
  • 각 상태를 변경하는 방법에 대해서는 닫혀 있도록 하면서도, GumballMachine 자체는 새로운 상태 클래스를 추가하는 확정에 대해서 열려 있도록 고침(OCP)
  • 처음 회사에서 제시해 주었던 다이어그램과 가까우면서 더 이해하기 쉬운 코드 베이스와 클래스 구조 만듬
과 같습니다. 그리고 이것이 바로 State Pattern입니다.

스테이트 패턴을 이용하면 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있습니다.


Context라는 클래스는 우리의 GumballMachine에 해당하며 여러 가지 내부 상태가 있을 수 있습니다. State에는 모든 구상 상태 클래스의 공통 인터페이스를 정의하고, 이를 구현한 구상 상태 클래스는 Context로부터 전달된 요청을 처리합니다.

당첨 기능 추가


이제 열번에 한번 꼴로 구슬을 두 개 주는 게임을 추가해 보도록 합니다. 먼저 GumballMachine 클래스에 상태를 추가하고 WinnerState에 대한 게터 메소드도 구현합니다.

public class GumballMachine
{
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    State winnerState;

    State state;
    int count = 0;

    public GumballMachine(int numberGumballs)
    {              
          //
         winnerState = new WinnerState(this);
         //
    }

    // 게터 메소드
}

그리고 WinnerState를 구현해 봅니다.
public class WinnerState : State
{
    GumballMachine gumballMachine;
 
    public WinnerState(GumballMachine gumballMachine)
    {
        this.gumballMachine = gumballMachine;
    }
 
    public void InsertQuarter()
    {
        Console.WriteLine("잠깐 기다려 주세요. 구슬이 나가는 중입니다.");
    }
 
    public void EjectQuarter()
    {
        Console.WriteLine("이미 구슬을 뽑으셨습니다.");
    }
 
    public void TurnCrank()
    {
        Console.WriteLine("손잡이는 한번만 돌려주세요.");
    }
 
    public void Dispense()
    {
        Console.WriteLine("축하합니다! 구슬을 하나 더 받을 수 있습니다.");
        gumballMachine.ReleaseBall();
        if (gumballMachine.GetCount() == 0)
        {
            gumballMachine.SetState(gumballMachine.GetSoldOutState());
        }
        else
        {
            gumballMachine.ReleaseBall();
            if (gumballMachine.GetCount() > 0)
            {
                gumballMachine.SetState(gumballMachine.GetNoQuarterState());
            }
            else
            {
                Console.WriteLine("더 이상 구슬이 없습니다.");
                gumballMachine.SetState(gumballMachine.GetSoldOutState());
            }
        }
    }
 
    public override string ToString()
    {
        return "Winner이므로 구슬 하나 추가 중";
    }
}
cs


당첨 기능 구현


10%의 확률로 당첨 여부를 결정하여 WinnerState로 전환되는 기능을 추가해야 합니다. 손잡이를 돌릴 때 작동해야 하므로 HasQuarterState에 추가하도록 하겠습니다.
public class HasQuarterState :State
{
    Random randomWinner = new Random(DateTime.Now.Millisecond);
    GumballMachine gumballMachine;
 
    public HasQuarterState(GumballMachine gumballMachine)
    {
        this.gumballMachine = gumballMachine;
    }
 
   //
 
    public void TurnCrank()
    {
        Console.WriteLine("손잡이를 돌리셨습니다.");
        int winner = randomWinner.Next(10);
        if ((winner==0&& (gumballMachine.GetCount() > 1))
        {
            gumballMachine.SetState(gumballMachine.GetWinnerState());
        }
        else
        {
            gumballMachine.SetState(gumballMachine.GetSoldState());
        }                
    }
 
   //
 
}
cs

랜덤 기능을 추가하여 당첨 여부를 확인합니다. 그리고 랜덤 기능이 작동할 수 있을 정도로만 테스트를 간단하게 작성합니다.
static void Main(string[] args)
{
    GumballMachine gumballMachine = new GumballMachine(5);
 
    Console.WriteLine(gumballMachine.ToString());
 
    gumballMachine.InsertQuarter();
    gumballMachine.TurnCrank();
 
    Console.WriteLine(gumballMachine.ToString());
 
    gumballMachine.InsertQuarter();           
    gumballMachine.TurnCrank();
    gumballMachine.InsertQuarter();
    gumballMachine.TurnCrank();
 
    Console.WriteLine(gumballMachine.ToString());
}
cs

결과)

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 5 개
기계 상태: 동전 투입 기다리는 중

동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
구슬이 나오는 중입니다.

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 4 개
기계 상태: 동전 투입 기다리는 중

동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
구슬이 나오는 중입니다.
동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
축하합니다! 구슬을 하나 더 받을 수 있습니다.
구슬이 나오는 중입니다.
구슬이 나오는 중입니다.

Mighty Gumball, Inc.
C#으로 돌아가는 2017년식 뽑기 기계
남은 개수: 1 개
기계 상태: 동전 투입 기다리는 중

계속하려면 아무 키나 누르십시오 . . .

대략 마친 것 같군요. 책에는 구슬을 추가하는 Refill()을 구현해 보라는 연습문제가 있지만 이쯤에서 대략 마치도록 하겠습니다. 역시 지금까지 작성한 전체 코드는 아래에 있습니다.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace StatePattern
{
    class Program
    {
        // 상태 인터페이스
        public interface State
        {
            void InsertQuarter();
            void EjectQuarter();
            void TurnCrank();
            void Dispense();
        }
 
        // 각 상태 클래스                
        public class NoQuarterState : State
        {
            GumballMachine gumballMachine;
 
            public NoQuarterState(GumballMachine gumballMachine)
            {
                this.gumballMachine = gumballMachine;
            }
 
            public void InsertQuarter()
            {
                Console.WriteLine("동전을 넣으셨습니다.");
                gumballMachine.SetState(gumballMachine.GetHasQuarterState());
            }
 
            public void EjectQuarter()
            {
                Console.WriteLine("동전을 넣어주세요.");
            }
 
            public void TurnCrank()
            {
                Console.WriteLine("동전을 넣어주세요.");
            }
 
            public void Dispense()
            {
                Console.WriteLine("동전을 넣어주세요.");
            }
 
            public override string ToString()
            {
                return "동전 투입 기다리는 중";
            }
        }
 
        public class HasQuarterState :State
        {
            Random randomWinner = new Random(DateTime.Now.Millisecond);
            GumballMachine gumballMachine;
 
            public HasQuarterState(GumballMachine gumballMachine)
            {
                this.gumballMachine = gumballMachine;
            }
 
            public void InsertQuarter()
            {
                Console.WriteLine("동전은 한 개만 넣어주세요.");
            }
 
            public void EjectQuarter()
            {
                Console.WriteLine("동전이 반환됩니다.");
                gumballMachine.SetState(gumballMachine.GetNoQuarterState());
            }
 
            public void TurnCrank()
            {
                Console.WriteLine("손잡이를 돌리셨습니다.");
                int winner = randomWinner.Next(10);
                if ((winner==0&& (gumballMachine.GetCount() > 1))
                {
                    gumballMachine.SetState(gumballMachine.GetWinnerState());
                }
                else
                {
                    gumballMachine.SetState(gumballMachine.GetSoldState());
                }                
            }
 
            public void Dispense()
            {
                Console.WriteLine("구슬이 나갈 수 없습니다.");
            }
 
            public override string ToString()
            {
                return "손잡이를 돌리길 기다리는 중";
            }
        }
 
        public class SoldState :State
        {
            GumballMachine gumballMachine;
 
            public SoldState(GumballMachine gumballMachine)
            {
                this.gumballMachine = gumballMachine;
            }
 
            public void InsertQuarter()
            {
                Console.WriteLine("잠깐 기다려 주세요. 구슬이 나가는 중입니다.");
            }
 
            public void EjectQuarter()
            {
                Console.WriteLine("이미 구슬을 뽑으셨습니다.");
            }
 
            public void TurnCrank()
            {
                Console.WriteLine("손잡이는 한번만 돌려주세요.");                
            }
 
            public void Dispense()
            {
                gumballMachine.ReleaseBall();
                if (gumballMachine.GetCount() > 0)
                {
                    gumballMachine.SetState(gumballMachine.GetNoQuarterState());
                }
                else
                {
                    Console.WriteLine("구슬이 다 떨어졌습니다.");
                    gumballMachine.SetState(gumballMachine.GetSoldOutState());
                }
            }
 
            public override string ToString()
            {
                return "구슬 나가는 중";
            }
        }
 
        public class SoldOutState : State
        {
            GumballMachine gumballMachine;
 
            public SoldOutState(GumballMachine gumballMachine)
            {
                this.gumballMachine = gumballMachine;
            }
 
            public void InsertQuarter()
            {
                Console.WriteLine("매진 되었습니다.");
            }
 
            public void EjectQuarter()
            {
                Console.WriteLine("매진 되었습니다.");
            }
 
            public void TurnCrank()
            {
                Console.WriteLine("매진 되었습니다.");
            }
 
            public void Dispense()
            {
                Console.WriteLine("구슬이 나갈 수 없습니다.");
            }
 
            public override string ToString()
            {
                return "매진";
            }
        }
 
        public class WinnerState : State
        {
            GumballMachine gumballMachine;
 
            public WinnerState(GumballMachine gumballMachine)
            {
                this.gumballMachine = gumballMachine;
            }
 
            public void InsertQuarter()
            {
                Console.WriteLine("잠깐 기다려 주세요. 구슬이 나가는 중입니다.");
            }
 
            public void EjectQuarter()
            {
                Console.WriteLine("이미 구슬을 뽑으셨습니다.");
            }
 
            public void TurnCrank()
            {
                Console.WriteLine("손잡이는 한번만 돌려주세요.");
            }
 
            public void Dispense()
            {
                Console.WriteLine("축하합니다! 구슬을 하나 더 받을 수 있습니다.");
                gumballMachine.ReleaseBall();
                if (gumballMachine.GetCount() == 0)
                {
                    gumballMachine.SetState(gumballMachine.GetSoldOutState());
                }
                else
                {
                    gumballMachine.ReleaseBall();
                    if (gumballMachine.GetCount() > 0)
                    {
                        gumballMachine.SetState(gumballMachine.GetNoQuarterState());
                    }
                    else
                    {
                        Console.WriteLine("더 이상 구슬이 없습니다.");
                        gumballMachine.SetState(gumballMachine.GetSoldOutState());
                    }
                }
            }
 
            public override string ToString()
            {
                return "Winner이므로 구슬 하나 추가 중";
            }
        }
 
        // 뽑기 기계 클래스
        public class GumballMachine
        {
            State soldOutState;
            State noQuarterState;
            State hasQuarterState;
            State soldState;
            State winnerState;
 
            State state;
            int count = 0;
 
            public GumballMachine(int numberGumballs)
            {
                soldOutState = new SoldOutState(this);
                noQuarterState = new NoQuarterState(this);
                hasQuarterState = new HasQuarterState(this);
                soldState = new SoldState(this);
                winnerState = new WinnerState(this);
                state = soldOutState;
                this.count = numberGumballs;
                if (numberGumballs > 0)
                {
                    state = noQuarterState;
                }
            }
 
            public void InsertQuarter()
            {
                state.InsertQuarter();
            }
 
            public void EjectQuarter()
            {
                state.EjectQuarter();
            }
 
            public void TurnCrank()
            {
                state.TurnCrank();
                state.Dispense();
            }
 
            public void SetState(State state)
            {
                this.state = state;
            }
            
            public void ReleaseBall()
            {
                Console.WriteLine("구슬이 나오는 중입니다.");
                if(count != 0)
                {
                    count -= 1;
                }
            }
 
            public int GetCount()
            {
                return count;
            }
 
            public State GetState()
            {
                return state;
            }
 
            
 
            public State GetSoldOutState()
            {
                return soldOutState;
            }
 
            public State GetNoQuarterState()
            {
                return noQuarterState;
            }
 
            public State GetHasQuarterState()
            {
                return hasQuarterState;
            }
 
            public State GetSoldState()
            {
                return soldState;
            }
 
            public State GetWinnerState()
            {
                return winnerState;
            }
            
            public override string ToString()
            {
                StringBuilder result = new StringBuilder();
                result.Append("\nMighty Gumball, Inc.");
                result.Append("\nC#으로 돌아가는 2017년식 뽑기 기계\n");
                result.Append("남은 개수: " + count + " 개\n");
 
                result.Append("기계 상태: " + state);
                result.Append("\n");
                return result.ToString();
            }
            
        }
        static void Main(string[] args)
        {
            GumballMachine gumballMachine = new GumballMachine(5);
 
            Console.WriteLine(gumballMachine.ToString());
 
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
 
            Console.WriteLine(gumballMachine.ToString());
 
            gumballMachine.InsertQuarter();           
            gumballMachine.TurnCrank();
            gumballMachine.InsertQuarter();
            gumballMachine.TurnCrank();
 
            Console.WriteLine(gumballMachine.ToString());
        }
    }
}
cs

댓글 없음:

댓글 쓰기