본문으로 건너뛰기

디자인 패턴: 스테이트 패턴 (State Pattern)

무료2015-03-07#Design_Pattern#状态模式#State Pattern

스테이트 패턴은 어떤 상태하의 일련의 행동을 캡슐화하는 데 사용됩니다. 스테이트 패턴은 상태 전환 과정을 숨기며, 호출자는 모듈 내부의 상태 전환 세부 사항을 알지 못합니다. 스테이트 패턴은 프로그램 실행 시의 다형성을 구현합니다. 코드에서大量의 유사한 if-else 구조가 나타난다면, 스테이트 패턴을 사용하여 이러한 부조화한 조건 블록을 제거해야 할 수도 있습니다.

no_mkd

일.스테이트 패턴이란 무엇인가?

모든 액션을 스테이트 오브젝트에 캡슐화하고, 스테이트 홀더는 현재 스테이트 오브젝트에 행동을 위임합니다

즉, 스테이트 홀더 (예를 들어 자동차, 텔레비전, ATM 기는 여러 상태를 가집니다) 는 행동의 세부 사항을 알지 못하며, 스테이트 홀더는 자신이 현재 처해 있는 상태 (어떤 스테이트 오브젝트를 보유하고 있는지) 만 신경 쓰면 되고, 모든 일을 현재 스테이트 오브젝트에 맡기면 됩니다. 물론 스테이트 홀더에게는 상태 전환을 제어할 권리가 있으며, 선택하여 아무것도 하지 않을 수도 있습니다..)

이.예를 들다

ATM 기를 시뮬레이션한다고 가정하고, 다음과 같은 요구 사항이 있습니다:

  • 현금 인출, 카드 비밀번호 인증, 현금吐出, 서비스 종료
  • 카드 비밀번호 인증 실패 또는 잔액 부족 시, 카드를 반환하고 이번 서비스를 종료한다
  • 기계 내에 현금이 없는 경우, No Cash 를 표시하고 은행에 현금 부족 정보를 전송한다
  • 기계 고장의 경우, No Service 를 표시하고 유지보수 담당자에게 유지보수 요청을 전송한다

사용자의 현금 인출 프로세스는 다음과 같습니다:

이것은 사용자 조작의 흐름입니다. ATM 기에게는 다음과 같은 점에도 주의해야 합니다:

  • 사용자 입력 금액을 획득한 후, 사용자의 카드 내 잔액뿐만 아니라 ATM 기 내의 잔액도 검증해야 한다
  • 매번 성공적인 현금 인출 서비스 종료 후, ATM 기 내의 잔액을 체크해야 한다 (현금이 없으면 해당 처리를 수행한다)
  • 어떠한 링크에서도 처리할 수 없는 오류가 발생하면, 고장으로 처리한다

위의 현금 인출 프로세스를 생각해 보면, 다양한 불법 조작도 고려해야 합니다. 예를 들어:

  • 카드를 삽입하지 않고 직접 비밀번호를 입력한다
  • 기계 고장 시에도 계속 조작을 수행한다
  • 기계 내에 현금이 없을 때 현금 인출을 요구한다

세분화한 후, 세부 문제가 매우 번거로워진다는 것을 알게 됩니다. 기계는 그곳에 놓여 있고, 모든 사용자 인터페이스가 개방되어 있습니다. 사용자는 언제든지任意의 인터페이스를 사용할 수 있습니다. 이러한 불법 조작을 방지하기 위해, 일련의 판단을 추가하지 않을 수 없습니다. 예를 들어:

  • 비밀번호를 검증하기 전에 카드가 삽입되었는지, 그리고 기계가 고장나지 않았는지 체크해야 한다
  • 카드를 삽입하기 전에 기계가 고장나지 않았는지 체크해야 한다
  • 현금 인출 조작 전에 기계가 고장났는지, 현금이 없는지,是否。。。

코드에는 기계의 상태를 나타내기 위해大量의 멤버 변수를 설정해야 하며, 더 무서운 것은 로직 블록 내에大量의 if-else 가 존재하고, 게다가 각 조작 내의 if-else 블록에는 미세한 차이만 있다는 것입니다. 중복으로 보이지만 처리하기 어렵습니다 (각 판단을 독립된 메서드로 추출해도 코드 규모를 축소할 수 있지만, 처리 과정상으로는 여전히 중복입니다..)

더 나은 처리 방법은 스테이트 패턴을 사용하는 것으로, 상태 판단 부분의 중복을 완전히 제거하고 명확하고 정돈된 코드 구조를 제공할 수 있습니다. 아래에서 스테이트 패턴을 사용하여 예제의 요구 사항을 구현합니다

(1) 먼저 ATM 이 제공하는 모든 인터페이스를 찾는다

  1. 카드 삽입
  2. 비밀번호 제출
  3. 현금 인출 (현금 인출 버튼은 물리 키라고 가정)
  4. 조회 (위와 같다고 가정)
  5. 카드取り出し

(2) 다음으로 ATM 의 모든 상태 및 각 상태에 해당하는 동작을 찾는다

  • 준비 완료 (Ready), 사용 가능 인터페이스: 모두
  • 현금 부족 (NoCash), 사용 가능 인터페이스: 1, 2, 4, 5
  • 고장 (NoService), 사용 가능 인터페이스: 없음

(3) 코딩 구현

먼저 State 베이스 클래스를 정의하고, 클래스 내에 (1) 에서 리스팅한 모든 인터페이스를 캡슐화합니다:

package StatePattern;

/**

  • 定義 ATM 機狀態

  • @author ayqy / public interface ATMState { /*

    • 插卡 */ public abstract void insertCard();

    /**

    • 提交密码 */ public abstract void submitPwd();

    /**

    • 取款 */ public abstract void getCash();

    /**

    • 查询余额 */ public abstract void queryBalance();

    /**

    • 取卡 */ public abstract void ejectCard(); }

다음으로 3 개의 상태를逐一 구현합니다

ReadyState:

package StatePattern;

/**

  • 實現 ATM 就緒狀態

  • @author ayqy */ public class ReadyState implements ATMState{ private ATM atm;//상태 홀더의 참조를 보유하여 조작할 수 있도록 함

    public ReadyState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("카드 삽입 완료"); }

    @Override public void submitPwd() { System.out.println("비밀번호 제출 완료"); //비밀번호를 검증하고 해당 처리 수행 if("123".equals(atm.getPwd())){ System.out.println("비밀번호 인증 통과"); } else{ System.out.println("비밀번호 인증 실패"); //카드 반환 ejectCard(); } }

    @Override public void getCash() { if(atm.getTotalAmount() >= atm.getAmount() && atm.getBalance() >= atm.getAmount()){ //계좌 잔액 업데이트 atm.setBalance(atm.getBalance() - atm.getAmount()); //기계 내 현금 총액 업데이트 atm.setTotalAmount(atm.getTotalAmount() - atm.getAmount()); System.out.println("¥" + atm.getAmount() + "吐出"); System.out.println("현금 인출 완료"); //카드 반환 ejectCard(); //기계 내 잔钞 체크 if(atm.getTotalAmount() == 0){//현금이 없으면 NoService 상태로 전환 atm.setCurrState(atm.getNoCashState()); System.out.println("현금 부족 정보를 은행에 전송했습니다"); } } else{ System.out.println("현금 인출 실패, 잔액 부족"); //카드 반환 ejectCard(); } }

    @Override public void queryBalance() { System.out.println("잔액¥" + atm.getBalance()); System.out.println("잔액 조회 완료"); }

    @Override public void ejectCard() { System.out.println("카드取り出し 완료"); } }

스테이트 클래스 내에서상태 전환을 수행하는 부분에 주의하세요:

if(atm.getTotalAmount() == 0){//현금이 없으면 NoService 상태로 전환
	atm.setCurrState(atm.getNoCashState());
}

구체적인 스테이트 오브젝트를 직접 new 하는 것이 아니라, ATM 이 제공하는 set 인터페이스를 사용했습니다. 이렇게 하는 것은 가능한 한 결합을 풀기 위함입니다 (형제 오브젝트는 서로를 인식하지 않습니다). 더 많은 유연성을 얻기 위함입니다

NoCashState 구현:

package StatePattern;

/**

  • 實現 ATM 無鈔狀態

  • @author ayqy */ public class NoCashState implements ATMState{ private ATM atm;//상태 홀더의 참조를 보유하여 조작할 수 있도록 함

    public NoCashState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("카드 삽입 완료"); }

    @Override public void submitPwd() { System.out.println("비밀번호 제출 완료"); //비밀번호를 검증하고 해당 처리 수행 if("123".equals(atm.getPwd())){ System.out.println("비밀번호 인증 통과"); } else{ System.out.println("비밀번호 인증 실패"); //카드 반환 ejectCard(); } }

    @Override public void getCash() { System.out.println("현금 인출 실패, 기계 내 현금 없음"); }

    @Override public void queryBalance() { System.out.println("잔액¥" + atm.getBalance()); System.out.println("잔액 조회 완료"); }

    @Override public void ejectCard() { System.out.println("카드取り出し 완료"); } }

NoServiceState 구현:

package StatePattern;

/**

  • 實現 ATM 故障狀態

  • @author ayqy */ public class NoServiceState implements ATMState{ private ATM atm;//상태 홀더의 참조를 보유하여 조작할 수 있도록 함

    public NoServiceState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("카드 삽입 실패, 기계에 고장이 발생했습니다"); }

    @Override public void submitPwd() { System.out.println("비밀번호 제출 실패, 기계에 고장이 발생했습니다"); }

    @Override public void getCash() { System.out.println("현금 인출 실패, 기계에 고장이 발생했습니다"); }

    @Override public void queryBalance() { System.out.println("잔액 조회 실패, 기계에 고장이 발생했습니다"); }

    @Override public void ejectCard() { System.out.println("카드取り出し 실패, 기계에 고장이 발생했습니다"); } }

구체적인 스테이트를 구현했으므로, ATM 클래스를 구축할 수 있습니다. 다음과 같습니다:

package StatePattern;

/**

  • 實現 ATM 機

  • @author ayqy */ public class ATM { /모든 상태/ private ATMState readyState; private ATMState noCashState; private ATMState noServiceState;

    private ATMState currState;//현재 상태 private int totalAmount;//기계 내 현금 총액

    /테스트용 임시 변수/ private String pwd;//비밀번호 private int balance;//잔액 private int amount;//인출 금액

    public ATM(int totalAmount, int balance, int amount, String pwd) throws Exception{ //모든 상태 초기화 readyState = new ReadyState(this); noCashState = new NoCashState(this); noServiceState = new NoServiceState(this);

     if(totalAmount > 0){
     	currState = readyState;
     }
     else if(totalAmount == 0){
     	currState = noCashState;
     }
     else{
     	throw new Exception();
     }
     
     //테스트 데이터 초기화
     this.totalAmount = totalAmount;
     this.balance = balance;
     this.amount = amount;
     this.pwd = pwd;
    

    }

    /구체적인 동작을 스테이트 오브젝트에 위임/ /**

    • 카드 삽입 */ public void insertCard(){ currState.insertCard(); }

    /**

    • 비밀번호 제출 */ public void submitPwd(){ currState.submitPwd(); }

    /**

    • 현금 인출 */ public void getCash(){ currState.getCash(); }

    /**

    • 잔액 조회 */ public void queryBalance(){ currState.queryBalance(); }

    /**

    • 카드取り出し */ public void ejectCard(){ currState.ejectCard(); }

    public String toString(){ return "현금 총액¥" + totalAmount; }

    /여기에大量의 getter and setter 생략/ }

모두 완료했으므로,早速 테스트해 봅시다

삼.실행 예

먼저 Test 클래스를 구현합니다:

package StatePattern;

import java.util.Scanner;

/**

  • 實現測試類

  • @author ayqy / public class Test { public static void main(String[] args) { /테스트 데이터/ / 기계 내 총액 계좌 잔고 인출 금액 비밀번호 * 1000 500 200 123 * 1000 300 500 123 * 0 500 200 123 * */ try { test(1000, 500, 200, "123"); System.out.println("-------"); test(1000, 300, 500, "123"); System.out.println("-------"); test(0, 500, 200, "123"); } catch (Exception e) { System.out.println("기계 고장, 유지보수 요청을 유지보수처에 전송했습니다"); } }

    private static void test(int totalAmount, int balance, int amount, String pwd)throws Exception{ //ATM 생성 ATM atm; atm = new ATM(totalAmount, balance, amount, pwd); //초기 상태 출력 System.out.println(atm.toString()); atm.insertCard(); atm.submitPwd(); atm.getCash(); //종료 상태 출력 System.out.println(atm.toString()); } }

설계한 3 개의 유스케이스 (정상 현금 인출, 잔액 초과 인출, 기계 내 현금 없음) 실행 결과는 다음과 같습니다:

사.스테이트 패턴과 스트래티지 패턴

스트래티지 패턴을 기억하십니까? 이 두 가지가 매우 비슷하다고 생각하지 않으십니까?

맞습니다. 두 패턴의 클래스 도완全히 동일합니다. 설명하자면:

  • 스테이트 주체 (소유자) 는 스테이트 오브젝트를 보유하며, 실행 시 스테이트 오브젝트를 동적으로 지정하여 클래스의 동작을 변경할 수 있다
  • 스트래티지 주체는 알고리즘 패밀리 오브젝트를 보유하며, 실행 시 알고리즘 패밀리 내의 알고리즘 (스트래티지) 을 동적으로 선택하여 클래스의 동작을 변경할 수 있다

즉, 스테이트 패턴과 스트래티지 패턴은 모두 실행 시의 다형성을 지원하며, 그 구현 방법은 모두 컴포지션 + 델리게이션입니다. 하지만 이것이 두 패턴이 동일하다는 것을 의미하는 것은 아닙니다. 왜냐하면 그들의 목표가 다르기 때문입니다:

  • 스테이트 패턴은 알고리즘플로우 변경 가능을 구현한다 (즉 상태 전환. 다른 상태에는 다른 플로우가 있다)
  • 스트래티지 패턴은 알고리즘세부 사항 선택 가능을 구현한다 (즉 알고리즘 패밀리 내의 알고리즘을 선택. 하나의 알고리즘 패밀리는 여러 개의 선택 가능 알고리즘을 포함한다)

댓글

아직 댓글이 없습니다

댓글 작성