State Pattern

Muzi·2023년 6월 17일
0

디자인 패턴

목록 보기
12/14

Intro

'상태'를 클래스로 표현하는 패턴
: 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 한다

  • Draft - 문서 검토 상태
  • Moderation - 문서를 공개하나, 현재 사용자가 관리자인 경우에만 공개
  • Published - 출판되어 아무것도 하지않는 상태

일반적으로 객체의 상태에 따라 적절한 행동들을 선택하는 많은 조건문​(if 또는 switch)​으로 구현된다

class Document is
    field state: string
    // …
    method publish() is
        switch (state)
            "draft":
                state = "moderation"
                break
            "moderation":
                if (currentUser.role == "admin")
                    state = "published"
                break
            "published":
                // Do nothing.
                break
    // …

문제점

  • 상태가 추가되면 될수록 상태에 의존하는 메소드,클래스들이 많아질수록, 유지 관리하기가 매우 어렵다(상태 변경때마다 상태에 의존하는 모든 코드들을 변경해야하니까.)

State Pattern

설명

: 시간마다 경비 상태가 변화하는 금고경비 시스템

금고는 1개
금고는 경비센터와 연결되어 있다
금고에는 비상벨과 일반통화용 전화가 연결되어 있다.
금고에는 시계가 설치되어 있어 현재의 시간을 감시

주간은 09:00~16:59, 야간은 17:00~23:590:00~8:59

금고는 주간에만 사용 가능
주간에 금고를 사용하면 경비센터에 사용기록이 남는다
야간에 금고를 사용하면 경비센터에 비상사태로 통보가 간다

비상벨은 언제나 사용가능
비상벨을 사용하면 경비센터에 비상벨 통보가 된다

일반통화용의 전화는 언제나 사용가능(야간은 녹음만 가능)
주간에 전화를 사용하면 경비센터가 호출됩니다.
야간에 전화를 사용하면 경비센터의 자동응답기가 호출됩니다.

state pattern를 사용하지 않을때

경비시스템클래스 {
    금고사용시_호출_메소드() {
        if(주간){
            경비센터에_이용_기록
        } else if (야간) {
            경비센터에_비상사태_통보
        }
    }
    비상벨_사용시_호출_메소드() {
        경비센터에_비상벨_통보
    }
    일반_통화시_호출_메소드() {
        if (주간) {
            경비센터의_호출
        } else if (야간) {
            경비센터의_자동응답기_호출
        }
    }
}
  • 상태가 추가되면 될수록 상태에 의존하는 메소드,클래스들이 많아질수록, 유지 관리하기가 매우 어렵다(상태 변경때마다 상태에 의존하는 모든 코드들을 변경해야하니까.)
  • 상태가 고정적이고 추가 가능성이 적으면 문제없는 코드

사용한다면

이름설명
State금고의 상태를 나타내는 인터페이스
DayStateState를 구현하고 있는 클래스, 주간의 상태를 나타낸다.
NightStateState를 구현하고 있는 클래스. 야간의 상태를 나타낸다.
Context금고의 상태변환을 관리하고 경비센터와 연락을 취하는 인터페이스
SafeFrameContext를 구현하는 클래스, 버튼이나 화면표시 등의 사용자 인터페이스를 갖는다.
Main동작 테스트용 클래스

State.class

public interface State {
    public abstract void doClock(Context context, int hour); // 시간 설정
    public abstract void doUse(Context context); // 금고 사용
    public abstract void doAlarm(Context context); // 비상벨
    public abstract void doPhone(Context context); // 일반 통화
}
  • 각각의 사건(시간 설정, 금고 사용, 비상벨 눌렸을 때, 일반통화를 할 때)에 대응해서 호출되는 메소드를 명세

DayState.class

public class DayState implements State{
    private static DayState singletone = new DayState(); // 주간 상태 인스턴스 생성

    public DayState() {
    }

    public static State getInstance() {
        return singletone;
    }
    @Override
    public void doClock(Context context, int hour) {
        if (hour < 9 || 17 <= hour) { // 야간일때
            context.changeState(NightState.getInstance());
        }
    }

    @Override
    public void doUse(Context context) {
        context.recordLog("금고사용(주간)");
    }

    @Override
    public void doAlarm(Context context) {
        context.callSecurityCenter("비상벨(주간)");
    }

    @Override
    public void doPhone(Context context) {
        context.callSecurityCenter("일반통화(주간)");
    }
    public String toString() {
        return "[주간]";
    }
}
  • doUser,doAlarm, doPhone은 Context의 메소드를 호출할 뿐 상태를 조건으로 검사 x
  • state pattern의 경우 상태의 차이가 클래스의 차이로 표현되기 때문에 조건문으로 분기할 필요 x

NightState.class

public class NightState implements State{
    private static NightState singletone = new NightState();

    public NightState() {
    }
    public static State getInstance() {
        return singletone;
    }

    @Override
    public void doClock(Context context, int hour) {
        if (9 <= hour && hour < 17) { // 주간의 경우
            context.changeState(DayState.getInstance());
        }
    }

    @Override
    public void doUse(Context context) {
        context.recordLog("비상: 야간금고 사용!");
    }

    @Override
    public void doAlarm(Context context) {
        context.callSecurityCenter("비상벨(야간)");
    }

    @Override
    public void doPhone(Context context) {
        context.callSecurityCenter("야간통화 녹음");
    }
    public String toString() {
        return "[야간]";
    }
}

Context.class

public interface Context {
    public abstract void setClock(int hour);
    public abstract void changeState(State state);
    public abstract void callSecurityCenter(String msg);
    public abstract void recordLog(String msg);
}
  • 상태관리 및 경비센터의 호출을 수행함

SafeFrame.class

public class SafeFrame extends Frame implements ActionListener, Context {
    private final TextField textClock = new TextField(60);
    private final TextArea textScreen = new TextArea(10, 60);
    private final Button buttonUse = new Button("금고사용");
    private final Button buttonAlarm = new Button("비상벨");
    private final Button buttonPhone = new Button("일반통화");
    private final Button buttonExit = new Button("종료");

    private State state = DayState.getInstance();

    public SafeFrame(String title) throws HeadlessException {
        super(title);
        setBackground(Color.lightGray);
        setLayout(new BorderLayout());
        add(textClock, BorderLayout.NORTH);
        textClock.setEditable(false);
        add(textScreen, BorderLayout.CENTER);
        textScreen.setEditable(false);

        Panel panel = new Panel();
        panel.add(buttonUse);
        panel.add(buttonAlarm);
        panel.add(buttonPhone);
        panel.add(buttonExit);
        add(panel, BorderLayout.SOUTH);

        pack();
        show();

        buttonUse.addActionListener(this);
        buttonAlarm.addActionListener(this);
        buttonPhone.addActionListener(this);
        buttonExit.addActionListener(this);
    }

    //Event Listener Handler
    public void actionPerformed(ActionEvent event) {
        System.out.println(event.toString());
        if (event.getSource() == buttonUse) {
            state.doUse(this); // 금고 사용
        } else if (event.getSource() == buttonAlarm) {
            state.doAlarm(this);
        } else if (event.getSource() == buttonPhone) {
            state.doPhone(this);
        } else if (event.getSource() == buttonExit) {
            System.exit(0);
        } else {
            System.out.println("?");
        }
    }

    @Override
    public void setClock(int hour) {
        String clockstring = "현재 시간은";
        if (hour < 10) {
            clockstring += "0" + hour + ":00";
        } else {
            clockstring += hour + ":00";
        }
        System.out.println(clockstring);
        textClock.setText(clockstring);
        state.doClock(this,hour);
    }
    //상태전환
    @Override
    public void changeState(State state) {
        System.out.println(this.state + "에서" + state + "로 상태가 변화했습니다.");
        this.state = state;
    }
    //경비센터의 호출
    @Override
    public void callSecurityCenter(String msg) {
        textScreen.append("call! " + msg + "\n");
    }
    //경비센터의 기록
    @Override
    public void recordLog(String msg) {
        textScreen.append("record ... " + msg + "\n");
    }
}
  • 금고 사용 버튼을 눌렀을때 시간이나 상태를 검사하지 않고 바로 state.doUse(this)를 호출

Main.class

public class Main {
    public static void main(String[] args) {
        SafeFrame frame = new SafeFrame("State Sample");
        while (true) {
            for (int hour = 0; hour < 24; hour++) {
                frame.setClock(hour);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                }
            }
        }
    }
}

실행 흐름

싱글톤의 적용

  • 상태를 변경할 때 마다 새로 객체를 생성하게 되면 메모리 낭비 굳이 상태의 경우 새로 인스턴스화 할 필요 없음
profile
좋아하는걸 열심히

0개의 댓글