no_mkd
일.스테이트 패턴이란 무엇인가?
모든 액션을 스테이트 오브젝트에 캡슐화하고, 스테이트 홀더는 현재 스테이트 오브젝트에 행동을 위임합니다
즉, 스테이트 홀더 (예를 들어 자동차, 텔레비전, ATM 기는 여러 상태를 가집니다) 는 행동의 세부 사항을 알지 못하며, 스테이트 홀더는 자신이 현재 처해 있는 상태 (어떤 스테이트 오브젝트를 보유하고 있는지) 만 신경 쓰면 되고, 모든 일을 현재 스테이트 오브젝트에 맡기면 됩니다. 물론 스테이트 홀더에게는 상태 전환을 제어할 권리가 있으며, 선택하여 아무것도 하지 않을 수도 있습니다..)
이.예를 들다
ATM 기를 시뮬레이션한다고 가정하고, 다음과 같은 요구 사항이 있습니다:
- 현금 인출, 카드 비밀번호 인증, 현금吐出, 서비스 종료
- 카드 비밀번호 인증 실패 또는 잔액 부족 시, 카드를 반환하고 이번 서비스를 종료한다
- 기계 내에 현금이 없는 경우, No Cash 를 표시하고 은행에 현금 부족 정보를 전송한다
- 기계 고장의 경우, No Service 를 표시하고 유지보수 담당자에게 유지보수 요청을 전송한다
사용자의 현금 인출 프로세스는 다음과 같습니다:

이것은 사용자 조작의 흐름입니다. ATM 기에게는 다음과 같은 점에도 주의해야 합니다:
- 사용자 입력 금액을 획득한 후, 사용자의 카드 내 잔액뿐만 아니라 ATM 기 내의 잔액도 검증해야 한다
- 매번 성공적인 현금 인출 서비스 종료 후, ATM 기 내의 잔액을 체크해야 한다 (현금이 없으면 해당 처리를 수행한다)
- 어떠한 링크에서도 처리할 수 없는 오류가 발생하면, 고장으로 처리한다
위의 현금 인출 프로세스를 생각해 보면, 다양한 불법 조작도 고려해야 합니다. 예를 들어:
- 카드를 삽입하지 않고 직접 비밀번호를 입력한다
- 기계 고장 시에도 계속 조작을 수행한다
- 기계 내에 현금이 없을 때 현금 인출을 요구한다
세분화한 후, 세부 문제가 매우 번거로워진다는 것을 알게 됩니다. 기계는 그곳에 놓여 있고, 모든 사용자 인터페이스가 개방되어 있습니다. 사용자는 언제든지任意의 인터페이스를 사용할 수 있습니다. 이러한 불법 조작을 방지하기 위해, 일련의 판단을 추가하지 않을 수 없습니다. 예를 들어:
- 비밀번호를 검증하기 전에 카드가 삽입되었는지, 그리고 기계가 고장나지 않았는지 체크해야 한다
- 카드를 삽입하기 전에 기계가 고장나지 않았는지 체크해야 한다
- 현금 인출 조작 전에 기계가 고장났는지, 현금이 없는지,是否。。。
코드에는 기계의 상태를 나타내기 위해大量의 멤버 변수를 설정해야 하며, 더 무서운 것은 로직 블록 내에大量의 if-else 가 존재하고, 게다가 각 조작 내의 if-else 블록에는 미세한 차이만 있다는 것입니다. 중복으로 보이지만 처리하기 어렵습니다 (각 판단을 독립된 메서드로 추출해도 코드 규모를 축소할 수 있지만, 처리 과정상으로는 여전히 중복입니다..)
더 나은 처리 방법은 스테이트 패턴을 사용하는 것으로, 상태 판단 부분의 중복을 완전히 제거하고 명확하고 정돈된 코드 구조를 제공할 수 있습니다. 아래에서 스테이트 패턴을 사용하여 예제의 요구 사항을 구현합니다
(1) 먼저 ATM 이 제공하는 모든 인터페이스를 찾는다
- 카드 삽입
- 비밀번호 제출
- 현금 인출 (현금 인출 버튼은 물리 키라고 가정)
- 조회 (위와 같다고 가정)
- 카드取り出し
(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 개의 유스케이스 (정상 현금 인출, 잔액 초과 인출, 기계 내 현금 없음) 실행 결과는 다음과 같습니다:

사.스테이트 패턴과 스트래티지 패턴
스트래티지 패턴을 기억하십니까? 이 두 가지가 매우 비슷하다고 생각하지 않으십니까?
맞습니다. 두 패턴의 클래스 도완全히 동일합니다. 설명하자면:
- 스테이트 주체 (소유자) 는 스테이트 오브젝트를 보유하며, 실행 시 스테이트 오브젝트를 동적으로 지정하여 클래스의 동작을 변경할 수 있다
- 스트래티지 주체는 알고리즘 패밀리 오브젝트를 보유하며, 실행 시 알고리즘 패밀리 내의 알고리즘 (스트래티지) 을 동적으로 선택하여 클래스의 동작을 변경할 수 있다
즉, 스테이트 패턴과 스트래티지 패턴은 모두 실행 시의 다형성을 지원하며, 그 구현 방법은 모두 컴포지션 + 델리게이션입니다. 하지만 이것이 두 패턴이 동일하다는 것을 의미하는 것은 아닙니다. 왜냐하면 그들의 목표가 다르기 때문입니다:
- 스테이트 패턴은 알고리즘플로우 변경 가능을 구현한다 (즉 상태 전환. 다른 상태에는 다른 플로우가 있다)
- 스트래티지 패턴은 알고리즘세부 사항 선택 가능을 구현한다 (즉 알고리즘 패밀리 내의 알고리즘을 선택. 하나의 알고리즘 패밀리는 여러 개의 선택 가능 알고리즘을 포함한다)
아직 댓글이 없습니다