본문 바로가기

Design Pattern

[Design Pattern] 4. State Pattern(상태 패턴) C#

728x90

체가 처한 현재 상황에 따라 스스로 취할 행동을 변경하는 패턴으로 상태 패턴이라 합니다.

 

이 또한 행동 패턴(Behavioral Design Patterns)중 하나입니다.

 

장점

  • 단일 책임 원칙을 만족하며 특정 상태에 사용되는 코드를 별도의 클래스로 분리할 수 있습니다.
  • 개방/폐쇄 원칙을 만족하며 기존 코드를 변경하지 않고 새 클래스를 추가할 수 있습니다.
  • 조건문을 줄여 코드가 단순해지고 가독성을 높일 수 있습니다.

단점

  • 처리하는 상태가 별로 없거나 상태의 변화가 적은 경우에 적용하는 것은 과유불급입니다.

 

이번 패턴도 역시 C#으로 Console 프로젝트를 하나 생성해서 할 것입니다.

 

이해를 위한 것이니 간단하게 만들겠습니다.

목표는 영수증 출력 기계(Receipt Printer)에 결제를 하고 영수증을 출력하는 처리입니다.

 

따라서 상태는 결제를 한 상태(Payment State)와 결제 하기 전 상태(Non Payment State)가 있습니다.

 

프로젝트의 tree구조는 아래와 같습니다.

State (Project)

|- Program.cs (Main)

|- ReceiptPrinter.cs

|- State.cs

|- PaymentState.cs

|- NonPaymentState.cs

 

그럼 구현해 보겠습니다.

 

ReceiptPrinter.cs

namespace State
{
    public class ReceiptPrinter
    {
        private State state;

        public ReceiptPrinter(State state)
        {
            this.SetState(state);
        }

        public void SetState(State state)
        {
            this.state = state;
            this.state.SetReceiptPrinter(this);
        }

        public void PrintReceipt()
        {
            this.state.PrintReceipt();
        }

        public void PayFor()
        {
            this.state.PayFor();
        }
    }
}

State를 가지고 State변경과 State의 메소드를 호출하는 메소드를 구현해 줍니다.

 

State.cs

namespace State
{
    public abstract class State
    {
        protected ReceiptPrinter receiptPrinter;

        public void SetReceiptPrinter(ReceiptPrinter receiptPrinter)
        {
            this.receiptPrinter = receiptPrinter;
        }

        public abstract void PrintReceipt();

        public abstract void PayFor();
    }
}

abstract로 구현하여 자식 클래스들에게 필요한 행동을 구현하도록 유도합니다.

필요에 따라 리턴값을 변경하여 사용하면 됩니다.

 

PaymentState.cs

using System;

namespace State
{
    public class PaymentState : State
    {
        public override void PayFor()
        {
            Console.WriteLine("You have already paid");
        }

        public override void PrintReceipt()
        {
            Console.WriteLine("Receipt Printing..."); 
            this.receiptPrinter.SetState(new NonPaymentState());
        }
    }
}

 Console.WriteLine을 통해 상황을 출력하도록 구현했습니다.

결제를 시도하면 이미 결제했다고 출력합니다.

State라는 부모 클래스에서 receiptPrinter를 선언해 놨기에 사용할 수 있으며 PrintReceipt를 한 뒤에 결제 되지 않은 상태로 변경해줍니다.

 

NonPaymentState.cs

using System;

namespace State
{
    public class NonPaymentState : State
    {
        public override void PayFor()
        {
            Console.WriteLine("Paying...");
            this.receiptPrinter.SetState(new PaymentState());
        }

        public override void PrintReceipt()
        {
            Console.WriteLine("You haven't paid yet");
        }
    }
}

결제를 하지 않은 상태의 클래스이기에 결제(PayFor)를 한 뒤 결제 상태(PaymentState)로 변경해 줍니다.

PrintReceipt를 시도하면 결제 하지 않았다고 출력합니다.

 

Program.cs

using System;

namespace State
{
    class Program
    {
        static void Main(string[] args)
        {
            ReceiptPrinter receiptPrinter = new ReceiptPrinter(new NonPaymentState());

            Console.Write("try payment: ");
            receiptPrinter.PayFor();
            Console.Write("end\nreceipt request: ");
            receiptPrinter.PrintReceipt();
            Console.WriteLine("end\n");

            Console.Write("receipt request before payment: ");
            receiptPrinter.PrintReceipt();
            receiptPrinter.PayFor();
            Console.Write("try double payment: ");
            receiptPrinter.PayFor();
            Console.Write("receipt request: ");
            receiptPrinter.PrintReceipt();
        }
    }
}

Console.Write가 많아 복잡해 보이지만 실행 결과와 같이 보면 이해하기 수월합니다.

실행 결과

ReceiptPrint를 생성하고 결제를 시도했습니다. "Paying..."이라고 출력됐으며 Main에서 end를 출력시켜 결제를 확인했습니다.

그 후 영수증을 요청하고 원하는 동작을 했음을 확인할 수 있습니다.

 

이후에 이전에 영수증을 요청하여 결제가 안된 상태로 변경됐음에도 영수증 출력을 시도해서  아직 결제가 안됐다고 출력됨을 확인 할 수 있고 두 번 결제를 시도 했을 땐 이미 결제 했다고 뜨는 것을 확인 하실 수 있습니다.

 

이후에 영수증을 요청해도 정상적으로 출력됨을 확인 하실 수 있습니다.

 

이런 경우에 return값을 bool로 해서 메인에서 처리해도 되고 해당 State내부에서 다른 처리를 하는 것이 편할 것 같습니다.

 

여기서는 결제에 관련된 두 개의 상태만 봤으나 프로젝트를 하다보면 좀 더 다양한 상태를 처리해야 할 때를 만날 수 있을 것입니다.

그런 때에 유용하게 사용하시면 됩니다.