본문 바로가기

JAVA/Design Patterns

10.스테이트 패턴

반응형

스테이트 패턴


정의

- 객체의 내부상태가 바뀜에 따라서 객체의 행동을 바꿀수 있다.

객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.


설명

->상태를 별도로 클래스로 캡슐화한 다음 현재 상태를 나타내는 객체에 행동을 위임하기 때문에, 내부 상태가 바뀜에 따라서 행동이 달라지게 된다.


클래스가 바뀌는 것 같은 결과를 얻는다는 것?

클라이언트 입장에서 사용하는 객체의 행동이 완전히 달라질수 있다면 마치 그 객체가 다른 클래스로부터 만들어진 객체러럼 느껴지게된다. 다른 클래스로 변신하는게 아니고 구성을 통해서 여러 상태 객체를 바꿔가면서 사용하기 때문에 이런 결과를 얻을 수 있다.





예) 뽑기기계


package patterns.state;


public class GumballMachine {

    final static int SOLD_OUT = 0;

    final static int NO_QUARTER = 1;

    final static int HAS_QUARTER = 2;

    final static int SOLD = 3;


    int state = SOLD_OUT;

    int count = 0;


    public GumballMachine(int count) {

        this.count = count;

        if (count > 0) {

            state = NO_QUARTER;

        }

    }


    public void insertQuarter() {

        if (state == HAS_QUARTER) {

            System.out.println("동전은 한개만 넣어주세요.");

        } else if (state == NO_QUARTER) {

            state = HAS_QUARTER;

            System.out.println("동전을 넣으셨습니다.");

        } else if (state == SOLD_OUT) {

            System.out.println("매진되었습니다. 다음기회에 이용해주세요.");

        } else if (state == SOLD) {

            System.out.println("잠깐만 기다려주세요. 알맹이가 나가고 있습니다.");

        }

    }


    public void ejectQuarter() {

        if (state == HAS_QUARTER) {

            System.out.println("동진이 반환합니다");

            state = NO_QUARTER;

        } else if (state == NO_QUARTER) {

            System.out.println("동전을 넣어주세요.");

        } else if (state == SOLD_OUT) {

            System.out.println("동전을 넣지 않았습니다. 동전이 반환되지 않습니다.");

        } else if (state == SOLD) {

            System.out.println("이미 알맹이를 뽑으셨습니다.");

        }

    }


    public void turnCrank() {

        if (state == HAS_QUARTER) {

            System.out.println("손잡이를 돌렸습니다.");

            state = SOLD;

            dispense();

        } else if (state == NO_QUARTER) {

            System.out.println("동전을 넣어주세요.");

        } else if (state == SOLD_OUT) {

            System.out.println("매진되었습니다.");

        } else if (state == SOLD) {

            System.out.println("손잡이는 한번만 돌려주세요");

        }

    }


    public void dispense() {

        if (state == HAS_QUARTER) {

            System.out.println("알맹이가 나갈수 없습니다.");

        } else if (state == NO_QUARTER) {

            System.out.println("동전을 넣어주세요.");

        } else if (state == SOLD_OUT) {

            System.out.println("매진되었습니다.");

        } else if (state == SOLD) {

            System.out.println("알맹이가 나가고 있습니다.");

            count = count - 1;

            if ( count == 0) {

                System.out.println("더이상 알맹이가 없습니다.");

                state = SOLD_OUT;

            } else {

                state = NO_QUARTER;

            }

        }

    }


    public void refill(int numGumBalls) {

        this.count = numGumBalls;

        state = NO_QUARTER;

    }


    @Override

    public String toString() {

        StringBuffer sb = new StringBuffer();

        sb.append("\n뽑기");

        sb.append("\n자바머신\n");

        sb.append("남은 개수 :" + count + "개\n");

        if (state == HAS_QUARTER) {

            sb.append("뽑기 대기중");

        } else if (state == NO_QUARTER) {

            sb.append("동진 투입 대기중");

        } else if (state == SOLD_OUT) {

            sb.append("매진");

        } else if (state == SOLD) {

            sb.append("판매중");

        }

        sb.append("\n");

        return sb.toString();

    }

}



package patterns.state;


public class GumballMachineTestDriven {

    public static void main(String[] args) {

        GumballMachine gumballMachine = new GumballMachine(5);


        System.out.println(gumballMachine);


        gumballMachine.insertQuarter();

        gumballMachine.turnCrank();


        System.out.println(gumballMachine);


        gumballMachine.insertQuarter();

        gumballMachine.ejectQuarter();

        gumballMachine.turnCrank();


        System.out.println(gumballMachine);


        gumballMachine.insertQuarter();

        gumballMachine.turnCrank();

        gumballMachine.insertQuarter();

        gumballMachine.turnCrank();

        gumballMachine.ejectQuarter();


        System.out.println(gumballMachine);


        gumballMachine.insertQuarter();

        gumballMachine.insertQuarter();

        gumballMachine.turnCrank();

        gumballMachine.insertQuarter();

        gumballMachine.turnCrank();

        gumballMachine.insertQuarter();

        gumballMachine.turnCrank();


        System.out.println(gumballMachine);

    }

}


알맹이를 하나 더 주는 상태가 추가될경우


OCP를 지키지 않고 있다.

상태전환이 복잡한 조건문 속에 숨어있기 때문에 분명하게 드러나지 않는다.

바뀌는 부분을 캡슐화하지 않았다.

다른 기능을 더 추가하는 과정에서 기존 코드에 없던 새로운 버그가 생길 가능성이 높다.


1. 행동에 대한 메서드가 들어있는 State 인터페이슬 정의

2. 모든상태에 대해서 상태 클래스를 구현. 그 상태에 해당하는 상태클래스가 모든 작업을 책임

3. 조건문 코드를 없애고 상태 클래스에 모든 작업을 위임


package patterns.state;


public interface State {

    void insertQuarter();

    void ejectQuarter();

    void turnCrank();

    void dispense();

}


package patterns.state;


public class HasQuarterState implements State {

    GumballMachineState gumballMachineState;


    public HasQuarterState(GumballMachineState gumballMachineState) {

        this.gumballMachineState = gumballMachineState;

    }


    @Override

    public void insertQuarter() {

        System.out.println("동전은 한개만 넣어주세요.");

    }


    @Override

    public void ejectQuarter() {

        System.out.println("동전이 반환합니다");

        gumballMachineState.setState(gumballMachineState.getNoQuarterState());

    }


    @Override

    public void turnCrank() {

        System.out.println("손잡이를 돌렸습니다.");

        gumballMachineState.setState(gumballMachineState.getSoldState());

        dispense();

    }


    @Override

    public void dispense() {

        System.out.println("알맹이가 나갈수 없습니다.");

    }

}


package patterns.state;


public class SoldState implements State {

    GumballMachineState gumballMachine;


    public SoldState(GumballMachineState gumballMachineState) {

        this.gumballMachine = gumballMachineState;

    }


    @Override

    public void insertQuarter() {

        System.out.println("잠깐만 기다려주세요. 알맹이가 나가고 있습니다.");

    }


    @Override

    public void ejectQuarter() {

        System.out.println("이미 알맹이를 뽑으셨습니다.");

    }


    @Override

    public void turnCrank() {

        System.out.println("손잡이는 한번만 돌려주세요");

    }


    @Override

    public void dispense() {

        gumballMachine.releaseBall();

        if (gumballMachine.getCount() > 0) {

            gumballMachine.setState(gumballMachine.getNoQuarterState());

        } else {

            System.out.println("매진되었습니다.");

            gumballMachine.setState(gumballMachine.getSoldOutState());

        }

    }

}


package patterns.state;


public class NoQuarterState implements State {

    GumballMachineState gumballMachineState;


    public NoQuarterState(GumballMachineState gumballMachineState) {

        this.gumballMachineState = gumballMachineState;

    }


    @Override

    public void insertQuarter() {

        System.out.println("동전을 넣으셨습니다.");

        gumballMachineState.setState(gumballMachineState.getHasQuarterState());

    }


    @Override

    public void ejectQuarter() {

        System.out.println("동전을 넣어주세요.");

    }


    @Override

    public void turnCrank() {

        System.out.println("동전을 넣어주세요.");

    }


    @Override

    public void dispense() {

        System.out.println("동전을 넣어주세요.");

    }

}


package patterns.state;


public class SoldOutState implements State {


    GumballMachineState gumballMachineState;


    public SoldOutState(GumballMachineState gumballMachineState) {

        this.gumballMachineState = gumballMachineState;

    }



    @Override

    public void insertQuarter() {

        System.out.println("매진되었습니다. 다음기회에 이용해주세요.");

    }


    @Override

    public void ejectQuarter() {

        System.out.println("동전을 넣지 않았습니다. 동전이 반환되지 않습니다.");

    }


    @Override

    public void turnCrank() {

        System.out.println("매진되었습니다.");

    }


    @Override

    public void dispense() {

        System.out.println("매진되었습니다.");

    }

}


package patterns.state;


public class GumballMachineState {

    State soldOutState;

    State noQuarterState;

    State hasQuarterState;

    State soldState;


    State state = soldOutState;

    int count = 0;


    public GumballMachineState(int numberGumballs) {

        soldState = new SoldState(this);

        noQuarterState = new NoQuarterState(this);

        hasQuarterState = new HasQuarterState(this);

        soldOutState = new SoldOutState(this);

        this.count = numberGumballs;

        if (count > 0) {

            state = noQuarterState;

        }

    }


    public void insertQuarter() {

        state.insertQuarter();

    }


    public void ejectQuarter() {

        state.ejectQuarter();

    }


    public void turnCrank() {

        state.turnCrank();

        state.dispense();

    }


    public State getState() {

        return state;

    }


    void releaseBall() {

        System.out.println("나오고 있습니다.");

        if (count != 0) {

            count = count - 1;

        }

    }


    public State getSoldOutState() {

        return soldOutState;

    }

    public State getNoQuarterState() {

        return noQuarterState;

    }

    public State getHasQuarterState() {

        return hasQuarterState;

    }

    public State getSoldState() {

        return soldState;

    }

    public void setSoldState(State soldState) {

        this.soldState = soldState;

    }

    public void setState(State state) {

        this.state = state;

    }

    public int getCount() {

        return count;

    }

}


수정결과

- 각 상태의 행동을 별개의 클래스로 국지화 시킴.

- 관리하기 힘든 if 선언문을 제거

- 각 상태를 변경에 대해서는 닫혀 있도록 하면서도 GumballMachine 자체는 새로운 상태 클래스를 추가하는 확장에 대해서 열려 있도록 수정

- 더 이해하기 좋은 코드 베이스와 클래스 구조를 만듬


스테이트 패턴 VS 스트래티지 패턴


스테이트 패턴

상태 객체에 일련의 행동이 캡슐화

상황에 따라 Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 맡기게 된다.

그 객체의 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고, 그 결과로 컨텍스트 객체의 행동도 자연스럽게 바뀌게 된다.

클라이언트는 상태 객체에 대해서 아무것도 몰라된다.

스테이트 패턴은 컨텍스트 객체에 수많은 조건문을 집어넣는 대신에 사용할 수 있는 패턴이라고 생각하면된다.

행동을 상태 객체내에 캡슐화시키면 컨텍스트 내의 상태 객체를 바꾸는 것만으로도 컨텍스트 객체의 행동을 바꿀 수 있다.


스트래티지 패턴

일반적으로 클라이언트에서 컨텍스트 객체한테 어떤 전략 객체를 사용할지를 지정해준다.

주로 실행시에 전략 객체를 변경할 수 있는 유연성을 제공하기 위한 용도로 쓰인다.

보통 가장 적합한 전략 객체를 선택해서 사용하게 된다. 필요한 전략 객체들을 지정해서 사용한다.

일반적으로 스태래티지 패턴은 서브클래스를 만드는 방법을 대신하여 유연성을 극대화하기 위한 용도로 쓰임

상속을 이용해서 클래스의 행동을 정의하다보면 행동을 변경해야 할때 마음대로 변경하기 힘들다.

하지만 스트래티지 패턴을 사용하면 구성을 통해 행동을 정의하는 객체를 유현하게 바꿀수 있다.



핵심정리

- 스테이트 패턴을 이용하면 내부 상태를 바탕으로 여러가지 서로 다른 행동을 사용할 수 있다.

- 스테이트 패턴을 사용하면 프로시저형 상태 기계를 쓸 때와 달리 각 상태를 클래스를 이용하여 표현하게 된다.

- Context 객체에서 현재 상태에게 행동을 위임한다.

- 각 상태를 클래스로 캡슐화함으로써 나중에 변경시켜야 하는 내용을 국지화할 수 있다.

- 스테이트 패턴과 스트래티지 패턴의 클래스 다이어그램은 똑같지만 그 용도는 서로 다르다.

- 스트래티지 패턴에서는 일반적으로 행동 또는 알고리즘을 Context 클래스를 만들 때 설정한다.

- 스테이트 패턴을 이용하면 Context의 내부 상태가 바뀜에 따라 알아서 행동을 바꿀수 있도록 할 수 있다.

- 상태 전환은 State 클래스에 의해서 제어할 수도 있고, Context 클래스에 의해서 제어할 수도 있다.

- 스테이트 패턴을 이용하면 보통 디자인에 필요한 클래스의 개수가 늘어난다.

- State 클래스를 여러 Context 객체의 인스턴스에서 공유하도록 디자인할 수도 있다.


출처 - Head First Design Patterns 저자- 에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠


반응형

'JAVA > Design Patterns' 카테고리의 다른 글

12. 컴파운드 패턴  (0) 2019.01.02
11. 프록시 패턴  (0) 2019.01.02
9.컴포지트 패턴  (0) 2018.12.13
9.이터레이터 패턴  (0) 2018.12.13
8.템플릿 메서드 패턴  (0) 2018.12.10