-
[Design Pattern] 스테이트 패턴 (State Pattern)공부/디자인 패턴 2021. 7. 18. 17:14
스테이트 패턴은 상태를 관리하는데 도움을 주는 디자인 패턴입니다. 어떤 행동을 수행할 때, 상태에 맞는 행동을 수행하도록 처리하도록 합니다. 이러한 시스템을 클래스로 분리하고 각 클래스에서 행동에 맞는 수행동작을 구현합니다. 자바의 경우, 캡슐화를 위해 인터페이스를 생성해 시스템의 각 상태를 나타내는 클래스로 구현합니다. 예를 들어, 자판기에서 동전 있음, 동전 없음, 상품 품절, 상품 반환 과 같은 상태를 조건문으로 분기하여 처리하는 것이 아닌 각 상태를 클래스에 캡슐화하여 구현하는 방식입니다.
클래스 다이어그램
State: 시스템의 모든 상태를 제공하는 인터페이스.
Context: state를 이용하여 역할을 수행하는 클래스. 현재 시스템의 상태를 나타내는 상태 변수(state)와 실제 시스템의 상태를 구성하는 여러 변수가 존재
ConcreteStateA, ConcreteStateB: 요청한 작업을 상태에 맞게 구현. 다음 상태를 결정해 상태 변경을 Context 객체에 요청할 수도 있음
예시
먼저 위 자판기의 예시를 스테이트 패턴을 사용하지 않고 구현하면 아래와 같이 표현할 수 있습니다.
public class Machine { final static int SOLD_OUT = 0; final static int NO_COIN = 1; final static int COIN = 2; public int state = NO_COIN; public void insertCoin() { if (state == NO_COIN) { } else if (state == COIN) { } } public void returnCoin() { if (state == NO_COIN) { } else if (state == COIN) { } } public void pushItem() { if (state == NO_COIN) { } else if (state == COIN) { } else if (state == SOLD_OUT) { } } }
이렇게 각 행동마다 처리해야되는 동작이 다르기 때문에 조건문이 계속해서 들어가게 됩니다.
이를 스테이트 패턴을 적용하면 아래와 같이 표현할 수 있습니다.
public interface State { void insertCoin(MachineContext machineContext); void returnCoin(MachineContext machineContext); void pushItem(); String getState(); } public class MachineContext { private State state; public MachineContext() { state = new NoCoinState(); } public void setState(State state) { this.state = state; } public String getState() { return this.state.getState(); } public void insertCoin() { this.state.insertCoin(this); } public void returnCoin() { this.state.returnCoin(this); } } public class CoinState implements State { @Override public void insertCoin(MachineContext machineContext) { machineContext.setState(this); System.out.println("코인 입력됨"); } @Override public void returnCoin(MachineContext machineContext) { machineContext.setState(new NoCoinState()); System.out.println("코인 반환"); } @Override public void pushItem() { } public String getState() { return "코인있음"; } } public class NoCoinState implements State { @Override public void insertCoin(MachineContext machineContext) { machineContext.setState(new CoinState()); System.out.println("코인 입력됨"); } @Override public void returnCoin(MachineContext machineContext) { System.out.println("코인이 없어요"); } @Override public void pushItem() { System.out.println(""); } public String getState() { return "코인없음"; } } MachineContext machineContext = new MachineContext(); machineContext.insertCoin(); System.out.println(machineContext.getState()); machineContext.returnCoin(); machineContext.returnCoin(); System.out.println(machineContext.getState()); // 코인 입력됨 // 코인있음 // 코인 반환 // 코인이 없어요 // 코인없음
파이썬
파이썬에서는 이커머스에서 상품 판매 상태를 스테이트 패턴을 적용하여 간단하게 구현했습니다.
from abc import abstractmethod, ABCMeta from enum import Enum from enum import auto class StateType(str, Enum): def _generate_next_value_(name, start, count, last_values): return name def __str__(self): return self.name def __repr__(self): return self.name FOR_SALE = auto() SOLD_OUT = auto() class State(metaclass=ABCMeta): @abstractmethod def get_state(self): pass @abstractmethod def sale(self, context): pass @abstractmethod def cancel(self, context): pass @abstractmethod def take_back(self, context): pass @abstractmethod def exchange(self, context): pass class StatusContext: def __init__(self, item_name, init_qty, sale_qty, state): self.__for_sale = ForSale() self.__sold_out = SoldOut() self.sale_qty = sale_qty self.init_qty = init_qty self._state = self.__sold_out if state == StateType.SOLD_OUT else self.__for_sale def sale(self): self._state.sale(self) def cancel(self): self._state.cancel(self) def increase_init_qty(self): self.init_qty += self.sale_qty def decrease_init_qty(self): self.init_qty -= self.sale_qty def set_state_sold_out(self): self._state = self.__sold_out print('품절로 변경') def set_state_for_sale(self): self._state = self.__for_sale @property def state(self): return self._state.get_state() class ForSale(State): def get_state(self): return StateType.FOR_SALE def sale(self, context): left_qty = context.init_qty - context.sale_qty if left_qty < 0: raise ValueError('남은 수량보다 판매 수량이 더 많음') if left_qty == 0: context.set_state_sold_out() context.decrease_init_qty() else: context.decrease_init_qty() print('물건 판매됨') def cancel(self, context): context.set_state_for_sale() context.increase_init_qty() def take_back(self, quantity): pass def exchange(self, quantity): pass class SoldOut(State): def get_state(self): return StateType.SOLD_OUT def sale(self, context): raise ValueError('품절인 상품은 구매할 수 없음') def cancel(self, context): context.set_state_for_sale() context.increase_init_qty() def take_back(self, context): pass def exchange(self, context): pass item_info = { 'item_name': '상품1', 'init_qty': 10, 'sale_qty': 2, 'state': StateType.FOR_SALE } status_context = StatusContext(**item_info) status_context.sale() print(f'남은수량: {status_context.init_qty} 판매상태: {status_context.state}') # 물건 판매됨 # 남은수량: 8 판매상태: FOR_SALE