따봉도치야 고마워

Head First Design Patterns : (10)스테이트 패턴 본문

프로그래밍/공부

Head First Design Patterns : (10)스테이트 패턴

따봉도치 2020. 9. 30. 18:42

스테이트 패턴

  • 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있음
  • 마치 객체의 클래스가 바뀌는 것과 같은 결과

 

Q. 스테이트 패턴과 스트래티지 패턴의 다이어그램이 같은 것 같은데?

  • 다이어그램은 같지만 용도의 차이
스테이트 패턴 스트래티지 패턴
- 상황에 따라 Context에서 여러 상태 객체 중 하나에 행동 위임
- 내부 상태가 변경됨에 따라 컨텍스트의 행동도 자연스럽게 바뀜.
- 즉 클라이언트는 상태 객체에 대해 몰라도 됨
- 수많은 조건문 대신에 사용
- 일반적으로 클라이언트에서 컨텍스트한테 전략 객체를 결정해줌
- 실행 시 전략 객체를 변경할 수 있는 유연성 제공을 위해 사용
- 상속보다 구성을 이용한 유연성 극대화

 

Q. 반드시 구상 상태 클래스에서 다음 상태가 변경되어야 하는 건지?

  • 아님. 상태 전환이 고정되어 있으면 Context 자체에서 변환해도 됨, 동적으로 결정되는 경우 상태 클래스에서 처리 권장
  • 상태 전환 코드를 상태 클래스에 넣으면 상태 클래스 간 의존성이 생긴다는 단점이 있음
  • 상태 전환 코드를 어느 쪽에 넣을지에 따라 어떤 클래스가 변경에 대해 닫혀있게 되는지도 결정됨.

 

Q. 여러 Context에서 상태 객체를 공유할 수 있는지?

  • 각 상태 객체 내에서 자체 상태를 보관하지만 않으면 가능함
  • 상태를 공유할 땐 일반적으로 정적 인스턴스 변수에 할당하는 방법을 사용

 

Q. 스테이트 패턴을 사용하면 클래스의 개수가 너무 많이 늘어나지 않는지?

  • 맞지만 유연성 향상을 위한 비용이라고 생각
  • 실제 클래스 개수보다 클라이언트에게 노출되는 클래스 개수가 중요

 


왕뽑기 예제

  • 뽑기 기계의 4가지 상태 (동전 없음, 동전 있음, 알맹이 판매, 알맹이 매진)에 맞춰 적절한 행동을 하는 코드 작성
  • if(state == SOLD_OUT) 이런 식의 조건문으로 일일이 검사해야 함
  • 만약 새로운 상태가 추가된다면? 유지보수가 점점 어려워짐
    •  바뀌는 부분이 캡슐화되지 않음, OCP 원칙을 지키지 못함 등등 여러 문제가 있음
    • 상태 객체(State Object)를 만들어 작업을 넘기는 건 어떨까

 

1) State 인터페이스 및 클래스 정의

  • 뽑기 기계와 관련된 모든 행동이 정의된 State 인터페이스와 각 상태 클래스

  • 각 상태 클래스는 상태에 맞게 구현
  NO_QUARTER HAS_QUARTER SOLD SOLD_OUT
insertQuarter()  HAS_QUARTER로
상태 변환
동전이 이미 넣어져있다는 메시지 출력 판매중이니 기다려달라는 메시지 출력 매진
메시지 출력
ejectQuarter() 동전을 넣어달라는
메시지 출력
NO_QUARTER로
상태 변환
판매중이라 반환 불가
메시지 출력
매진
메시지 출력
turnCrank() 동전을 넣어달라는
메시지 출력
SOLD로
상태 변환
한 번만 돌려달라는
메시지 출력
매진
메시지 출력
dispense() 동전을 넣어달라는
메시지 출력
알맹이가 나가지 않았다는 메시지 출력 알맹이 하나 내보냄
남은 개수 > 0이면NO_QUARTER
0이면 SOLD_OUT
매진
메시지 출력

 

2) 기존 조건문 코드를 전부 삭제하고, 상태 클래스에 작업 위임

//기존 코드
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 void insertQuarter(){
    	if(state == HAS_QUARTER){
        	..
        }else if(state == NO_QUARTER){
        	..
        }else if(state == SOLD_OUT){
        	..
        }else if(state == SOLD){
        	..
        }
    }
    
    ..
}



================================>

//변경된 코드
public class GumballMachine{
    //상수 대신 상태 클래스를 사용
    State soldOutState; 
    State noQuarterState;
    State hasQuarterState
    State soldState;
    
    State state = soldState;
    int count = 0;
    
    public GumballMachine(int numberGumballs){
    	soldOutState = new SoldOutState(this); 
    	noQuarterState = new NoQuarterState(this);
    	hasQuarterState = new HasQuaterState(this);
    	soldState = new SoldState(this);
        
        this.count = numberGumballs;
        if(numberGumballs > 0)
        	state = noQuarterState;
    }
    
    public void insertQuarter(){
    	//조건문이 전부 사라지고 현재 상태 객체에 위임
        state.insertQuarter();
    }
    
    public void ejectQuarter(){
        state.ejectQuarter();
    }
    
    public void turnCrank(){
        state.turnCrank();
        state.dispense();
    }
    
    ..
    
    //해당 메소드를 사용해 다른 객체에서 상태 변경 가능
   	void setState(State state){
    	this.state = state;
    }
    
    
    //..각 State 객체를 위한 getter 메소드 등등..
    
}

 

개선점

  • 각 상태의 행동을 별개의 클래스로 국지화 시킴
  • 관리하기 힘든 if선언문들을 삭제
  • 각 상태를 변경에 대해선 닫혀있도록 하면서, 확장에 대해(기능 추가 등) 열려있도록 함 = OCP
  • 훨씬 더 이해하기 좋은 베이스와 구조

 

+ 뽑기 기계에 당첨 기능 추가

1) WinnerState 클래스 생성

public class WinnerState implements State{
    ..
    
    public void dispense(){
    	System.out.println("당첨을 축하드립니다! 알맹이를 하나 더 받으실 수 있어요.");
    	gumballMachine.releaseBall(); //알맹이를 하나 내보내는 메소드
        
        if(gumballMachine.getCount() == 0){
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }else{
            gumballMachine.releaseBall(); //하나 더 내보냄
            if(gumballMachine.getCount() == 0){
                gumballMachine.setState(gumballMachine.getNoQuarterState());
            }
            else{
                System.out.println("더 이상 알맹이가 없습니다");
                gumballMachine.setState(gumballMachine.getSoldOutState());
            }
        }
    }
}

 

2) HasQuarterState::turnCrank() 수정 - 당첨 처리

public class HasQuarterState implements State{
    //난수 생성기 
    Random randomWinner = new Random(System.currentTimeMillis());
    
    ..
    
    public void turnCrank(){
        System.out.println("손잡이를 돌리셨습니다");
        int winner = randomWinner.nextInt(10);
        
        //당첨 && 알맹이 개수 2개 이상인 경우
        if(winner == 0 && gumballMachine.getCount() > 1){
            gumballMachine.setState(gumballMachine.getWinnerState());
        }else{
            gumballMachine.setState(gumballMachine.getSoldState());
        }
    }
}

 

Q. WinnerState가 꼭 있어야 하는지? 그냥 SoldState에서 처리하면 안 되는지?

  • 문제는 없지만 한 상태 클래스에서 두 가지 상태를 처리한다는 단점이 있음
  • 코드 중복은 줄일 수 있지만 상태 클래스가 조금 불분명해짐 - 단일 역할 원칙도 위배

Q. SoldState와 WinnerState 간의 중복 코드 처리

  • State를 추상클래스로 만들고 기본 기능을 추가하면 될 것 같음
  • 또한 기본 구현에 예외처리 선언을 해두면 특정 상태에서 부적절한 메소드를 호출했을 때 별도 구현 없이 처리 될 것(상속)

Q. dispense() 메소드는 항상 호출됨. 부적절한 상황에서 조차

  • 위의 방법을 사용할 수 도 있고, turnCrank()에서 부울 값을 리턴해서 처리해도 됨

연필을 깎으며

page 434

- A B C D E F

 

NOTE

- GumballMachine 인스턴스가 여러 개일 때, 변경점에 대해 생각해보기

- 상태 전환이 상태 클래스 내에만 있는 것에 대한 장단점 생각해보기

Comments