본문 바로가기

OOP

헤드퍼스트 디자인패턴 - 상태 패턴(State Pattern)

상태패턴(State Pattern)이란 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀  수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다. 
 
여기서는 뽑기 기계에 게임기능을 추가하고   (10%의 확률로 알맹이를 하나 더받는 이벤트) 이라는  스티커를 붙여서 광고할 계획이다.  하지만 코드 추가를 하면 점점 복잡해진다 그래서  공통 상태의 별도의  하나의 상태 State라는 인터페이스로 만들고 그것을 구현하는 식으로.  즉 캡슐화로 만들 것이다.
 
 
여기서 캡슐화란 ? 
객체지향프로그래밍에서의 중요한 특징 중 하나로, 연관된 데이터와 함수를 논리적으로 묶어놓은 것이며, 데이터를 보호하기 위해 다른 객체의 접근을 제한하는 접근 제한 수식자의 기능을 제공한다.  (두산백과)

클래스 캡슐화

상태State들을 클래스로 만들었다.
SoldState(알맹이 판매)
SoldOutState (알매이 매진)
NoQuarterState(동전 없음)
HasQuarterState(동전 있음) 
 
 
각클래스 안에는 일어날수 있는 행동들 (메서드)가 있다 행동들을 예측해 if else 조건문으로 만들었다.
insertQuarter() - 동전투입
ejectQuarter() - 동전반환
turnCrank() - 손잡이 돌림
dispense() - 알맹이 내보냄
 
 
뽑기 기계의 모든 상태를 대상으로 상태 State클래스를 구현해야 한다. 기계가 어떤 상태에 있다면 그 상태에 해당하는 상태 클래스가 모든작업을 책임지게 구성.
 
무수히 많은 행동들을  if else조건문으로 예측해서 만들면 너무 많아져서 보기도 안좋고 관리가 어려워  조건문을 없애고 다시 리팩토링을 하였다. 각 상태를 변경에는 닫혀 있게 했고, GumballMachine클래스는 새로운 상태 클래스를 추가하는 확장에는 열려있도록 고치는 것 (디자인패턴 OCP원칙.)
 

public class GumballMachine {
	 State soldOutState ;
	 State noQuarterState ;
	 State hasQuarterState ;
	 State soldState ;
	 State winnerState;
	
	 State state = soldOutState;
	 int count = 0;
	
	public GumballMachine(int numberGumballs) {
		soldOutState = new SoldOutState(this);
		noQuarterState = new NoQuarterState(this);
		hasQuarterState = new HasQuarterState(this);
		soldState = new SoldState(this);
		winnerState = new WinnerState(this);
		
		this.count = numberGumballs;
		if(numberGumballs > 0) {
			state = noQuarterState;
		} 
		
	}
	
	public void insertQuarter() {
		state.insertQuarter();
	}
	
	public void ejectQuarter() {
		state.ejectQuarter();
	}
	
	public void turnCrank() { // dispense 메소드를 구현하지 않아도 됨 
		state.turnCrank();   // 내부에서만 필요한 행동이어서 사용자가 직접 기계에 알맹이를 내놓으
		state.dispense();   // 라고 요청을 할 수 없음 turnCrank() 메소드에서 dispense() 메소드를 호출 
		
		
	}
	
	void setState(State state) {
		this.state = state; // 뽑기 기계의 상태를 다른 상태로 전환 getter setter를 이용해 상태전환
	}
	
	void releaseBall() {  // 알맹이를 내본내고 개수를 count-1를 한다 
		System.out.println("알맹이를 내보내고 있습니다.");
		if(count > 0) {
			count = count - 1;
		}
	}
	
	int getCount() {
		return count;
	}
	
	void refill(int count) {
		this.count  += count;
		System.out.println("다시 채워졌습니다.: " + this.count);
		state.refill();
	}
	
	
	// 외부에서도 객체 필드를 읽을 수 있게 getter를 설정한다!
    
	public State getState() {
		return state;
	}
	
	public State getSoldOutState() {
		return soldOutState;
	}
	
	public State getNoQuarterState() {
		return noQuarterState;
	}
	
	public State getHasQuarterState() {
		return hasQuarterState;
	}
	
	public State getSoldState() {
		return soldState;
	}
	
	public State getWinnerState() {
		return winnerState;
	}
	
	public String toString() {
		StringBuffer result = new StringBuffer();
		result.append("\n주식회사 왕뽑기");
		result.append("\n자바로 돌아가는 최신형 뽑기 기계");
		result.append("남은개수: " + count + "gumball");
		if(count != 1) {
			result.append("개");
		}
		result.append("\n기계");
		result.append("기계상태 " + state + "\n" );
		return result.toString();
		}
		
	}

 
 
 
 
 
뽑기 기계 테스트할 코드이다.  insertQuater(동전넣기)와 turnCrank(손잡이 돌리기) 동전을 넣어서 손잡이를 돌리면 당첨되는 것을 구현할것이다.
 

public class GumballMachineTestDrive {

	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.turnCrank();       // 손잡이 돌리기 
	gumballMachine.insertQuarter();   // 동전 넣기
	gumballMachine.turnCrank();       // 손잡이 돌리기 
	
	System.out.println(gumballMachine);
	
	
	
	
	}

}

 
 
 
 
동전 넣기 전에 NoQuarter상태로 초기 설정을 하고 테스트 코드에 넣었던 insertQuarter메소드를 호출하여 동전을 넣은 후getter setter를 이용하여  HasQuarter 상태로 클래스를 변경한다 굳이 왜 상태 전환코드를 일일이 넣는이유는 한꺼번에 상태 State클래스에 넣게 되면 의존성이 생겨서 getter, setter를 이용한다. 
 

//리팩토링하기 전의 코드 
public void ejectQuarter(){
	if(state == HAS_QUARTER) {
    	System.out.println("동전이 반환됩니다.");
    	state = NO_QUARTER;
    } else if(state ==NO_QUARTER)  {
    	System.out.println("동전을 넣어주세요.");
    } ...생략
}




//리팩토링 후의 코드 - if else조건문 삭제
//상태State 인터페이스 NoQuarterState클래스로 구현
public class NoQuarterState implements State { 
	...생략 
    
    // if else를 없애고 행동에 맞는 메시지를 넣는다 지금은 동전투입 메서드에 메시지이다.
	public void insertQuarter() {
		System.out.println("동전을 넣으셨습니다."); 
        

// 동전을 투입과 동시에 getter,setter를 이용하여 NoQuarterState 클래스에서 HasQuarterState클래스로
// 변경한다.       
        gumballMachine.setState(gumballMachine.getHasQuarterState());
	} ...생략
 }

 
 
 
NoquarterState클래스에서 -> HasQuarter  변경된  클래스로  테스트코드에 있는 turnCrank() 메소드를 호출하여 조건문에 따라 상태변화를 시켜준다. 이때 만약 난수 발생기에서 나온 알맹이 개수가 2개이면 WinnerState 클래스로 전환해 알맹이를 하나더 받는 당첨메시지 표시하고 그렇지 않으면 soldState를 호출해 알맹이 한개를 내보낸다.
 

public class HasQuarterState implements State {
	// 10% 확률로 당첨 여부를 결정하는 난수 발생기를 추가한다..
	Random randomWinner = new Random(System.currentTimeMillis());
	GumballMachine gumballMachine;
	
	public HasQuarterState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	@Override
	public void insertQuarter() {
		// TODO Auto-generated method stub
		System.out.println("동전은 한 개만 넣어 주세요.");
		

	}

	@Override
	public void ejectQuarter() {
		// TODO Auto-generated method stub
		System.out.println("동전이 반환됩니다.");
		gumballMachine.setState(gumballMachine.getNoQuarterState());
	}

	@Override
	public void turnCrank() {
		// TODO Auto-generated method stub
		System.out.println("손잡이를 돌리셨습니다.");
		int winner = randomWinner.nextInt(10);
		if ((winner == 0) && (gumballMachine.getCount() > 1)) {
			// 남아 있는 알맹이가 2개 이상이면 상태를 WinnerState클래스로 전환 
			gumballMachine.setState(gumballMachine.getWinnerState());
		} else {
			gumballMachine.setState(gumballMachine.getSoldState());
		}
		

	}

	@Override
	public void dispense() {
		// TODO Auto-generated method stub
		System.out.println("알맹이를 내보낼 수 없습니다.");

	}

	public void refill() { } 
	
	

}

 
 
 
만약 당첨 되었다면 알맹이 개수2개 를 판별하여 축하메시지를 보내준다.
 

public class WinnerState implements State {
	
	GumballMachine gumballMachine;
	
	public WinnerState(GumballMachine gumballMachine
	) {
		this.gumballMachine = gumballMachine;
	}


	public void insertQuarter() {
		// TODO Auto-generated method stub
		System.out.println("동전을 넣으셨습니다.");
	}

	
	public void ejectQuarter() {
		// TODO Auto-generated method stub
		System.out.println("기달려주세요 이미 알맹이를 내보내고 있습니다.");

	}

	@Override
	public void turnCrank() {
		// TODO Auto-generated method stub
		System.out.println("손잡이를 돌리셨습니다");

	}


	public void dispense() {
		
		// 알맹이 개수를 확인하여서 당첨여부를 표시한다.
		gumballMachine.releaseBall(); //count -1;
		if(gumballMachine.getCount() == 0) {
			gumballMachine.setState(gumballMachine.getSoldOutState());
			// 0개면 매진 !
		} else {
			gumballMachine.releaseBall(); //알맹이가 하나 더 있으면 내보낸다.
			System.out.println("축하드립니다! 알맹이를 하나 더 받으실 수 있습니다!");
			if(gumballMachine.getCount() > 0) {
				gumballMachine.setState(gumballMachine.getNoQuarterState());
			} else {
				System.out.println("더 이상 알맹이가 없습니다...");
				gumballMachine.setState(gumballMachine.getSoldOutState());
			}
		}

	}


	public void refill() {
		// TODO Auto-generated method stub

	}
	


}

 
 
 
 
당첨이 안되었다면 알맹이는 1개가 뽑힌다. 그리고 dispense메소드에 있는 if else 조건문을 통해  알맹이 개수를 판별하여 다시 뽑는 상태로 변경하여 처음부터 시작한다. 알맹이가 다 떨어 졌다면 개수가 0개이면  상태를 매진으로 표시 
 

public class SoldState implements State {
	//당첨이 안되었다면 알맹이는 1개 뽑힌다.
GumballMachine gumballMachine;
	
	public SoldState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	@Override
	public void insertQuarter() {
		// TODO Auto-generated method stub
		System.out.println("알맹이를 내보내고 있습니다.");

	}

	@Override
	public void ejectQuarter() {
		// TODO Auto-generated method stub
		System.out.println("이미 알맹이를 뽑으셨습니다.");

	}

	@Override
	public void turnCrank() {
		// TODO Auto-generated method stub
		System.out.println("손잡이는 한 번만 돌려 주세요.");

	}

	@Override
	public void dispense() {
		// TODO Auto-generated method stub
		gumballMachine.releaseBall();
		if(gumballMachine.getCount() > 0) {
			gumballMachine.setState(gumballMachine.getNoQuarterState());
		} else {
			System.out.println("매진입니다.");
			gumballMachine.setState(gumballMachine.getSoldOutState());
		}

	}

	@Override
	public void refill() {
		// TODO Auto-generated method stub

	}
	


}

 
 
결과화면
 

알맹이가 내보내지면 뽑기기계남은 개수는 차감된다.

 

'OOP' 카테고리의 다른 글

다형성  (0) 2024.02.01
PHP 객체의 속성에 객체 저장  (0) 2024.01.30
PHP 게터 세터  (0) 2024.01.30
PHP 클래스, 객체  (0) 2024.01.30
헤드퍼스트 디자인패턴 - 데코레이터 패턴(Decorator Pattern)  (0) 2023.11.18