ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 행위패턴 > 상태(State) *
    기타/디자인패턴 2021. 2. 12. 15:41

     

    동일한 동작을 객체 상태에 따라 다르게 처리해야 할 때 사용.
    일련의 규칙에 따라 객체의 상태(State)를 변화시켜, 객체가 할 수 있는 행위를 바꾸는 패턴이다.

    이렇게 하나의 객체에 여러 가지 상태(예, 정지, 상승, 하강)가 존재할 때 패턴을 사용하지 않고 프로그래밍을 하면 if 문 또는 switch 문을 사용하여 처리한다. 
    그런데 신규 상태(예, 문 열림, 문 닫힘)가 발생하면 프로그램을 다시 수정해야 한다.

    또는 상속을 통해 만든 클래스에 새로운 기능을 추가할 때 일부 하위 클래스에서는 신규 기능이 필요하지만, 

    일부 클래스와는 무관할 때는? 

     

     

    뽑기 기계를 예시로,  동전없음, 동전있음, 알맹이 매진, 알맹이 판매 라는 네개의 상태를 가지는 뽑기 기계가 있다고 하자. 

    public class GambleTest {
        public static void main(String[] args) {
            GambleMachine gm = new GambleMachine(5); //5개 알맹이가 있는 기계
    
            System.out.println(gm);
    
            gm.insertQuater(); // 동전을 넣고 
            gm.turnCrank();   //손잡이를 돌린다.
    
            System.out.println(gm);
    
            gm.insertQuater(); 
            gm.ejectQuater(); //동전 반환 요청
            gm.turnCrank();
    
            System.out.println(gm);
    
            gm.insertQuater();
            gm.turnCrank();
            gm.insertQuater();
            gm.turnCrank();
            gm.ejectQuater();
        }
    }

     

    아래와 같이 기계 클래스 내 모든 상태를 가지고 있을 때, 요구사항이 추가된다면, 상태 추가뿐만 아니라

    모든 관련 메서드에 코드를 수정해야한다.

    public class GambleMachine { // Context
        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 GambleMachine(int count){
            this.count = count;
            if(count>0){
                state= NO_QUARTER;
            }   
        }
    
        public void insertQuater(){
            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("wait! 알맹이 나가는 중~");
            }
        }
        //사용자가 동전을 반환받고자 하는 경우 
        public void ejectQuater(){
            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;
                }
            }
        }
    }

     

     

    이렇게 네 가지 상태마다 클래스를 만들면 클래스 수는 많아지지만 나중에 추가적인 상태가 생겨도 

    클래스만 추가해주면 된다. 

    GambleMachine 에서 호출할 상태의 행동은 실행시점에 동적으로 결정된다. 

    public class GambleMachine { // Context
        State state ;
        State soldState;
        State hasQuarterState;
        State soldOutState;
        State noQuarterState;
        int count=0;
    
        public GambleMachine(int count){
            this.count = count;
            soldState = new SoldState(this);
            hasQuarterState = new HasQuarter(this);
            soldOutState = new SoldOutState(this);
            noQuarterState = new NoQuarterState(this);
    
            state = soldOutState;
            if(count>0){
                state= noQuarterState;
            }   
            
        }
    
        public void insertQuater(){
            state.insertQuater();
        }
        //사용자가 동전을 반환받고자 하는 경우 
        public void ejectQuater(){
            state.ejectQuater();
        }
    
        //손잡이를 돌리는 경우
        public void turnCrank(){
            state.turnCrank();
            state.dispense();
        }
    
        public void realeaseBall(){
            if(count>0){
                count = count-1;
            }       
        }
    
        public int getCount() {
            return count;
        }
    
        public void setState(State state) {
            this.state = state;
        }
    
        public State getSoldOutState() {
            return soldOutState; //new SoldOutState(this);
        }
    
        public State getNoQuarterState() {
            return noQuarterState;//new NoQuarterState(this);
        }
    
        public State getHasQuarterState() {
            return hasQuarterState; // HasQuarter(this);
        }
    
    	public State getSoldState() {
    		return soldState; //new SoldState(this);
    	}
    
        @Override
        public String toString() {
            String str = "남은공= " + count + "현재 state= " + state.getClass().getName();
            return str;
        }
    	
    }
    public class NoQuarterState implements State{
        GambleMachine gambleMachine;
        public NoQuarterState(GambleMachine gambleMachine){
            this.gambleMachine = gambleMachine;
        }
    
        @Override
        public void insertQuater() {
             gambleMachine.setState(gambleMachine.getHasQuarterState());
             System.out.println("동전 투입");
        }
    
        @Override
        public void ejectQuater() {
            System.out.println("동전을 먼저 넣어야함");
        }
    
        @Override
        public void turnCrank() {
            System.out.println("동전을 넣어주세요");
        }
    
        @Override
        public void dispense() {
            System.out.println("동전을 넣어주세요");
        }
    
    }
    public class SoldState implements State{
        
        GambleMachine gambleMachine;
        public SoldState(GambleMachine gambleMachine){
            this.gambleMachine = gambleMachine;
        }
    
        @Override
        public void insertQuater() {
            System.out.println("wait! 알맹이 나가는 중~");
        }
    
        @Override
        public void ejectQuater() {
            System.out.println("이미 뽑음");
        }
    
        @Override
        public void turnCrank() {
            System.out.println("손잡이는 한 번만 돌리세요");
        }
    
        @Override
        public void dispense() {
            System.out.println("알맹이 나가는 중..");
            
            gambleMachine.realeaseBall();       
                if(gambleMachine.getCount()>0){                
                    gambleMachine.setState(gambleMachine.getNoQuarterState());
                } else {
                    System.out.println("알맹이 이제 없음");
                    gambleMachine.setState(gambleMachine.getSoldOutState());
                }
        }
        
    }

     

    State 패턴으로 얻은 것 

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

    관리하기 힘든 if 선언문 삭제 

    각 상태를 변경에 닫혀있고 GambleMachine 자체에 새로운 상태 클래스를 추가하는 확장에 열린 구조 OCP

    이해하기 좋은 코드 베이스와 구조 

     

    스트래티지 Strategy 와 스테이트 State 패턴은 동일한 구조를 사용하는 것으로 보이지만 용도에 차이가 있다.

    state 패턴에서는 상태 객체에 일련의 행동이 캡슐화된다. 상황에 따라 Context 객체(GambleMachine)에서 여러 상태 객체 중 한 객체에게 행동을 맡기게 된다. 그 객체의 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌고 그 결과로 context 객체의 행동도 자연스레 바뀌게 된다. client 는 상태 객체에 대해 거의 아무것도 몰라도 된다.

     

    strategy 패턴에서는 일반적으로 client 에서 context 객체에서 어떤 전략 객체를 사용할지 지정해 준다.

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

    서브 클래스를 만드는 방법을 대신해 유연성을 극대화하기 위한 용도로 쓰인다. 상속을 이용해 클래스 행동을 정의하다 보면 한계가 있기 때문에 strategy 패턴으로 구성을 이용해 객체를 유연하게 바꿀 수 있다. 

     

    state 패턴은 수많은 조건문을 집어넣는 대신 사용할 수 있는 패턴이라고 볼 수 있다.

    행동을 상태 객체 내 캡슐화시키면 컨텍스트(GambleMachine) 내 상태 객체를 바꾸는 것만으로 컨텍스트 객체의 행동을 바꿀 수 있기 때문이다.

     

    GambleMachine 의 경우 구체 상태 클래스 내에서 setState 를 통해 다음 상태를 결정했지만, 

    GambleMachine (context) 에서 상태 변경을 할 수도 있다. 하지만 상태전환이 동적으로 결정되는 경우에는 상태 클래스 내에서 처리하는 것이 좋다. 

    상태 전환 코드를 상태 클래스내에 넣으면 상태 클래스간 의존성이 생긴다는 단점이 있다.

    이 예제에서는 getState 메서드를 통해 의존성을 줄이기 위한 노력을 했다.

    상태 전환 흐름을 결정하는 코드를 어느쪽에 넣는지에 따라 시스템이 확장될 때, 어떤 클래스가 변경에 대해 닫혀있을지 결정된다.

     

    정상성 점검

    - dispense 메서드는 동전을 넣지 않은 상태에서도 항상 호출되는데,

    만약 turncrank 에서 부울값을 리턴받거나 예외처리를 도입하는 것을 어떤지?

     

    - 상태 전환에 대한 정보가 모두 상태 클래스에 있는지 , 이로 인해 생길 수 있는 문제에는 무엇이 있을지 ? 

     

    - GambleMachine 의 인스턴스를 여러개 만든다면 상태 인스턴스를 정적 인스턴스 변수로 만드는 것이 나을 것이다.

    그럼 GambleMachine 과 상태 클래스는 각각 어떻게 고쳐야 하는가?

     

     

    상속의 단점

    • 상위 클래스 기능에 버그가 생기거나 기능의 추가/변경 등으로 변화가 생겼을 때 상위 클래스를 상속 받는 하위 클래스가 정상적으로 작동할 수 있을지에 대한 예측이 힘듬
      • 하위 클래스는 상위 클래스의 부분 집합이기 때문에
    • 상속 구조가 복잡해질 수록 그 영향에 대한 예측이 힘들어짐
    • 상위 클래스에서 의미 있었던 기능이 하위 클래스에서는 의미 없는 기능일 수 있음
    • 하위 클래스는 반드시 상위 클래스로부터 물려 받은 기능들을 제공해야 함 + 하위 클래스에서 기능들이 추가됨
      • 기능 확장에 따라 상위 클래스에서 파생된 클래스들이 많아지고, 그 규모가 커짐에 따라 일관성 있게 작성하지 않은 클래스들에 대한 이해도는 점차 복잡해지고 사용에 어려움이 생길 수 있음

    디자인 원칙 

    1. 문제를 명확하게 파악하기. 

    - 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.

    바뀌는 부분만 따로 뽑아서 캡슐화시키면 나중에 바뀌지 않는 부분에 영향을 미치지 않고 고치거나 확장가능.

    2. 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.

    3. 상속보다는 구성을 활용한다.  

    "A는 B이다" 보다 "A에는 B가 있다"가 나을 수 있다. 

     

    관리하기 용이한 객체지향 시스템을 만드는 비결은 '나중에 어떻게 바뀔 것인지' 에 대해 생각해보는 것.

     

     

    출처 : 

    Head First Design Patterns

     

     

    반응형

    '기타 > 디자인패턴' 카테고리의 다른 글

    싱글톤 패턴 *  (0) 2021.09.14
    행위패턴 > 템플릿 메서드 *  (0) 2021.02.12
    행위패턴 > 옵저버 *  (0) 2021.02.12
    행위패턴 > 이터레이터 *  (0) 2021.02.11
    행위패턴 > 커맨드 *  (0) 2021.02.11
Designed by Tistory.