適用場景
- 如果代碼中出現大量與對象狀態有關的條件語句,並且這些語句會導致代碼的可維護性和靈活性變差。不能方便的增加和刪除狀態。(主要適用場景)
- 如果對象的行爲依賴於他的狀態,狀態的改變將導致行爲的變化。(這一點跟策略模式很像)
例如:搜狗輸入法的輸入狀態切換 , 自動售貨機的工作狀態切換
狀態模式和策略模式的UML類圖和實現方式都很接近,但是這兩個模式的差別就在於他們的“意圖”。
舉個例子,在狗血劇中,同樣是KillPeople,有時候就是爲了報仇,有時候就是爲了解除隊友的痛苦。
對於狀態模式而言,context的行爲隨時可以委託到那些狀態對象中的一個,當前狀態在狀態對象集合中游走改變,因此,context的行爲也會跟着改變,但是注意,context的客戶對於狀態對象瞭解不多,甚至根本是渾然不覺,這是區別於策略模式的主要因素之一。
還不懂?那我們再直白一點,狀態模式關注的是複雜的狀態判斷和狀態之間的切換,用戶並不會直接對狀態進行切換。而策略模式是由用戶設置不同的策略,而直接影響對象的行爲。
定義
(源於Design Pattern):當一個對象的內在狀態改變時允許改變其行爲,這個對象看起來像是改變了其類。
狀態模式主要解決的是當控制一個對象狀態的條件表達式過於複雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類中,可以把複雜的判斷邏輯簡化。
模式角色
- Context 上下文: 維護一個ConcreteState實例,這就是他當前的狀態。
- State 狀態接口: 每個狀態都要實現它,它定義了所有可能發生的狀態。
- ConcreteState具體狀態類: 實現State接口,個性化需要被實現的方法。
Demo
搜狗輸入法在按下shift的時候可以在中文和英文之間切換,而按下Lock鍵可以在大小寫之間切換,那麼我們就用狀態模式來實現這個場景。
- InputState 狀態接口
- 中文輸入法 狀態實現類
- 英文輸入法 狀態實現類
- 大寫輸入法 狀態實現類
- SouGou Context上下文
- Client 用戶
State
/**
*
* 輸入狀態的接口
* Created by YY on 2016/11/16.
*/
public interface InputState {
void downShift();
void downLock();
void print();
}
中文輸入法
/**
* 中文狀態
* Created by YY on 2016/11/16.
*/
public class ChinaInputState implements InputState{
SouGou mSouGou;
public ChinaInputState(SouGou sougou){
mSouGou = sougou;
}
@Override
public void downShift() {
mSouGou.setState(mSouGou.getLetterInputState());
}
@Override
public void downLock() {
mSouGou.setState(mSouGou.getCapitalInputState());
}
@Override
public void print() {
System.out.println("中文狀態");
}
}
英文輸入法(小寫)
/**
* 英文狀態
* Created by YY on 2016/11/16.
*/
public class LetterInputState implements InputState{
SouGou mSouGou;
public LetterInputState(SouGou sougou){
mSouGou = sougou;
}
@Override
public void downShift() {
mSouGou.setState(mSouGou.getChinaInputState());
}
@Override
public void downLock() {
mSouGou.setState(mSouGou.getCapitalInputState());
}
@Override
public void print() {
System.out.println("小寫英文狀態");
}
}
英文輸入法(大寫)
/**
* 大寫狀態
* Created by YY on 2016/11/16.
*/
public class CapitalInputState implements InputState{
SouGou mSouGou;
public CapitalInputState(SouGou sougou){
mSouGou = sougou;
}
@Override
public void downShift() {
// 不做任何操作
}
@Override
public void downLock() {
mSouGou.setState(mSouGou.getLetterInputState());
}
@Override
public void print() {
System.out.println("大寫英文狀態");
}
}
搜狗輸入法
/**
* 搜狗輸入法
* Created by YY on 2016/11/16.
*/
public class SouGou{
private InputState mState;
private ChinaInputState chinaInputState;
private LetterInputState letterInputState;
private CapitalInputState capitalInputState;
public SouGou(){
chinaInputState = new ChinaInputState(this);
letterInputState = new LetterInputState(this);
capitalInputState = new CapitalInputState(this);
// 默認爲中文輸入狀態
mState = chinaInputState;
}
public void downShift(){
mState.downShift();
}
public void downLock(){
mState.downLock();
}
public void print(){
mState.print();
}
public ChinaInputState getChinaInputState() {
return chinaInputState;
}
public void setChinaInputState(ChinaInputState chinaInputState) {
this.chinaInputState = chinaInputState;
}
public LetterInputState getLetterInputState() {
return letterInputState;
}
public void setLetterInputState(LetterInputState letterInputState) {
this.letterInputState = letterInputState;
}
public CapitalInputState getCapitalInputState() {
return capitalInputState;
}
public void setCapitalInputState(CapitalInputState capitalInputState) {
this.capitalInputState = capitalInputState;
}
public void setState(InputState state){
mState = state;
}
public InputState getState(){
return mState;
}
}
用戶
/**
* 模擬用戶按下按鍵時候的操作
* Created by L on 2016/11/17.
*/
public class Client {
public void main(){
SouGou souGou = new SouGou();
souGou.downShift();
souGou.print();
souGou.downShift();
souGou.print();
souGou.downShift();
souGou.print();
souGou.downLock();
souGou.print();
souGou.downLock();
souGou.print();
}
}
輸出
小寫英文狀態
中文狀態
小寫英文狀態
大寫英文狀態
小寫英文狀態
到這裏,我的20W廣告費已經到賬了,不……是我們已經完成整改搜狗輸入法的狀態切換了,他能夠輕鬆的再任何狀態下都做出正確的動作,可是別高興的太早。
搜狗的產品經理在某天晚上失戀了,然後他在極度鬱悶的情況下召開了會議,會議中他將需求改成了如下方案:
按Lock鍵切換到大寫字母,再次按下Lock鍵依舊要回到之前的狀態。同時在大寫狀態的時候按shift鍵也可以切換到小寫字母。
例如當前是中文狀態,那麼按下兩次Lock鍵依舊要回到中文狀態。
他以爲可以讓一羣人跟着他傷心,但他不知道我們已經用了狀態模式來寫這個模塊,只要稍加改動就能完成它的需求改動。
我們只需要在SouGou這個上下文中增加一個State變量,在除了大寫狀態之外的狀態實現類中,來記錄按下Lock鍵之前的狀態。下面我把改動後的代碼放上來,除了類名上面的段落註釋,代碼內的段落註釋就是我們改動的地方。
英文狀態(大寫)
/**
* 大寫狀態
* Created by YY on 2016/11/16.
*/
public class CapitalInputState implements InputState{
SouGou mSouGou;
public CapitalInputState(SouGou sougou){
mSouGou = sougou;
}
@Override
public void downShift() {
/**
* 之前是不做任何操作
*/
mSouGou.setState(mSouGou.getLetterInputState());
}
@Override
public void downLock() {
//mSouGou.setState(mSouGou.getLetterInputState());
/**
* 這裏註釋掉之前的代碼 改爲下面這行
*/
mSouGou.setState(mSouGou.getLastState());
}
@Override
public void print() {
System.out.println("大寫英文狀態");
}
}
英文狀態(小寫)
/**
* 英文狀態
* Created by YY on 2016/11/16.
*/
public class LetterInputState implements InputState{
SouGou mSouGou;
public LetterInputState(SouGou sougou){
mSouGou = sougou;
}
@Override
public void downShift() {
mSouGou.setState(mSouGou.getChinaInputState());
}
@Override
public void downLock() {
mSouGou.setState(mSouGou.getCapitalInputState());
/**
* 在每次按Lock鍵的時候記錄當前的狀態
*/
mSouGou.setLastState(this);
}
@Override
public void print() {
System.out.println("小寫英文狀態");
}
}
中文狀態
/**
* 中文狀態
* Created by YY on 2016/11/16.
*/
public class ChinaInputState implements InputState{
SouGou mSouGou;
public ChinaInputState(SouGou sougou){
mSouGou = sougou;
}
@Override
public void downShift() {
mSouGou.setState(mSouGou.getLetterInputState());
}
@Override
public void downLock() {
mSouGou.setState(mSouGou.getCapitalInputState());
/**
* 在每次按Lock鍵的時候記錄當前的狀態
*/
mSouGou.setLastState(this);
}
@Override
public void print() {
System.out.println("中文狀態");
}
}
搜狗輸入法
/**
* 搜狗輸入法
* Created by YY on 2016/11/16.
*/
public class SouGou{
private InputState mState;
private ChinaInputState chinaInputState;
private LetterInputState letterInputState;
private CapitalInputState capitalInputState;
/**
* 這個狀態用於存放之前的狀態
*/
InputState mLastState;
public SouGou(){
chinaInputState = new ChinaInputState(this);
letterInputState = new LetterInputState(this);
capitalInputState = new CapitalInputState(this);
mState = chinaInputState;
}
public void downShift(){
mState.downShift();
}
public void downLock(){
mState.downLock();
}
public void print(){
mState.print();
}
/**
* 爲了閱讀方便,我們把之前的get set方法隱藏於此
*/
/**
* 這裏提供了get set方法
*/
public InputState getLastState() {
return mLastState;
}
public void setLastState(InputState mLastState) {
this.mLastState = mLastState;
}
}
甚至比想象的還要簡單,接着我們來測試一下:
/**
* 模擬用戶按下按鍵時候的操作
* Created by L on 2016/11/17.
*/
public class Client {
public void main(){
SouGou souGou = new SouGou();
souGou.print();
souGou.downLock();
souGou.print();
souGou.downLock();
souGou.print();
souGou.downShift();
souGou.print();
souGou.downLock();
souGou.print();
souGou.downLock();
souGou.print();
souGou.downLock();
souGou.print();
souGou.downShift();
souGou.print();
}
}
我的按鍵順序是這樣的(默認起始狀態是中文):
- Lock 大寫
- Lock 中文
- Shift 英文
- Lock 大寫
- Lock 英文
- Lock 大寫
- Shift 英文
這是我們的期望結果,讓我們看看是不是這樣:
大寫英文狀態
中文狀態
小寫英文狀態
大寫英文狀態
小寫英文狀態
大寫英文狀態
小寫英文狀態
如果你安裝了搜狗輸入法,那麼可以試着按一下。
總結
狀態模式是爲了解決複雜的狀態結構,其優點是將複雜的狀態判斷條件拆分成了對象,各個狀態各掃門前雪,似的擴展更加方便。
但是如果你細心的話會發現,他並沒有很好的實現開閉原則,我們改變需求的同時幾乎要對每個文件的代碼都進行改動,並且如果狀態更多會產生更多的文件,這也是該模式的缺點。
但還是要記住,只要不濫用,每個模式都是好模式。