ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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

    댓글