把書讀薄 | 《設計模式之美》設計模式與範式(行爲型-狀態模式)

這是我參與8月更文挑戰的第4天,活動詳情查看: 8月更文挑戰html

0x0、引言

😀 週一搬磚,元氣滿滿,繼續啃設計模式之美,本文對應設計模式與範式:行爲型(64),狀態模式 (State Pattern),描述了對象 狀態變化 及如何在每種狀態下表現出不一樣的 行爲~java

Tips:二手知識加工不免有所紕漏,感興趣有時間的可自行查閱原文,謝謝。算法


0x一、定義

原始定義設計模式

容許一個對象在其內部狀態改變時改變它的行爲,對象看起來彷佛修改了本身的類同樣。數組

簡單點說markdown

讓一個對象經過一系列狀態的變化來控制行爲的變化。app

狀態模式策略模式 極其類似,可經過內在差異進行區分:ide

  • 策略模式將具體策略類暴露出去,調用者需瞭解每種策略的不一樣之處以便正確使用,封裝的是不一樣算法,算法間沒有交互,以達到算法能夠自由切換的目的。
  • 狀態模式狀態的改變是由其內部條件來改變的,與外界無關,封裝的是不一樣狀態,以達到狀態切換行爲隨之切換的目的。

0x二、寫個簡單例子

有home鍵的Android機爲例,按下home鍵,處於不一樣狀態有不一樣的行爲:函數

  • 關機狀態 → 沒有反應;
  • 開機後首次啓動 → 密碼解鎖;
  • 非首次啓動 → 密碼解鎖或指紋解鎖;
  • 啓動後 → 返回主界面

不使用狀態模式實現一波:oop

public class StateTest {
    private static int state = 0;
    private final static int CLOSE = 0; // 關機狀態
    private final static int FIRST_BOOT = 1;   // 首次啓動
    private final static int NOT_FIRST_BOOT = 2;    // 非首次啓動
    private final static int AFTER_BOOT = 3;    // 啓動後

    private static void clickHome() {
        if(state == CLOSE) {
            System.out.println("處於關機狀態,沒有反應");
        } else if(state == FIRST_BOOT) {
            System.out.println("首次啓動。能夠進行密碼解鎖");
        } else if(state == NOT_FIRST_BOOT) {
            System.out.println("非首次啓動,能夠進行密碼或指紋解鎖");
        } else if(state == AFTER_BOOT) {
            System.out.println("啓動狀態,返回主界面");
        }
    }

    public static void main(String[] args) {
        state = CLOSE;
        clickHome();
        state = FIRST_BOOT;
        clickHome();
        state = NOT_FIRST_BOOT;
        clickHome();
        state = AFTER_BOOT;
        clickHome();
    }
}
複製代碼

代碼運行結果以下

若是須要增長一種狀態,如處於fastboot模式,狀態定義要寫一個,而後if-else加一個判斷;還有,不止處理Home鍵,還有音量鍵、電源鍵,又得定義幾個函數,而後複製一波這個if-else,試試用狀態模式實現一波。

// 抽象狀態
public abstract class State {
    protected StateContext context;

    public void setContext(StateContext context) { this.context = context; }

    abstract void onHomeClick();
    abstract void onPowerClick();
    abstract void onVolumeAscClick();
    abstract void onVolumeDescClick();
}


// 具體狀態 → 關機狀態
public class CloseState extends State {
    @Override public void onHomeClick() { System.out.println("處於關機狀態,按Home鍵沒有反應"); }

    @Override void onPowerClick() {
        System.out.println("手機開機");
        context.setState(FirstBootState.class);
        context.setScreenOn(true);
        context.getState().onHomeClick();
    }

    @Override void onVolumeAscClick() { System.out.println("處於關機狀態,按音量+沒反應"); }

    @Override void onVolumeDescClick() {  System.out.println("處於關機狀態,按音量-沒反應"); }
}

// 具體狀態 → 第一次啓動狀態
public class FirstBootState extends State {
    @Override public void onHomeClick() {
        System.out.println("首次啓動,能夠進行密碼解鎖");
        System.out.println("解鎖完畢,進入主界面");
        context.setState(AfterBootState.class);
        context.setScreenOn(true);
    }

    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
        } else {
            System.out.println("亮屏,等待密碼解鎖");
        }
        context.setScreenOn(!context.isScreenOn());
    }

    @Override void onVolumeAscClick() { System.out.println("音量+"); }

    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}

// 具體狀態 → 非第一次啓動狀態
public class NotFirstBootState extends State {
    @Override public void onHomeClick() {
        System.out.println("非首次啓動,能夠經過密碼或指紋解鎖");
        System.out.println("解鎖完畢,進入主界面");
        context.setScreenOn(true);
        context.setState(AfterBootState.class);
    }

    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
        } else {
            System.out.println("亮屏,等待密碼或指紋解鎖");
            context.setState(NotFirstBootState.class);
        }
        context.setScreenOn(!context.isScreenOn());
    }

    @Override void onVolumeAscClick() { System.out.println("音量+"); }

    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}

// 具體狀態 → 啓動後
public class AfterBootState extends State {
    @Override void onHomeClick() { System.out.println("返回主界面"); }

    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
            context.setState(NotFirstBootState.class);
        } else {
            System.out.println("亮屏,等待密碼或指紋解鎖");
            context.getState().onHomeClick();
        }
        context.setScreenOn(!context.isScreenOn());
    }

    @Override void onVolumeAscClick() { System.out.println("音量+"); }

    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}

// 上下文信息類
public class StateContext {
    private boolean isScreenOn = false;   // 屏幕是否亮着
    public final static Map<Class, State> stateMap = new HashMap<>();
    private State state;    // 手機當前狀態

    static {
        stateMap.put(CloseState.class, new CloseState());
        stateMap.put(FirstBootState.class, new FirstBootState());
        stateMap.put(NotFirstBootState.class, new NotFirstBootState());
        stateMap.put(AfterBootState.class, new AfterBootState());
    }

    public void setState(Class stateClass) {
        this.state = stateMap.get(stateClass);
        this.state.setContext(this);
    }

    public State getState() { return state; }

    public boolean isScreenOn() { return isScreenOn; }

    public void setScreenOn(boolean screenOn) {
        isScreenOn = screenOn;
        System.out.println("===> 屏幕處於:" + (isScreenOn ? "亮屏狀態": "熄屏狀態"));
    }
}

// 測試用例
public class StateTest {
    public static void main(String[] args) {
        StateContext context = new StateContext();
        context.setState(CloseState.class);
        // 處於關機狀態點擊音量- 和 home鍵
        context.getState().onVolumeDescClick();
        context.getState().onHomeClick();
        // 處於關機狀態點擊電源鍵
        context.getState().onPowerClick();
        context.getState().onPowerClick();
        context.getState().onHomeClick();
        context.getState().onVolumeAscClick();
    }
}
複製代碼

代碼運行結果以下

經過狀態模式,咱們把事件觸發的 狀態轉移和動做執行,拆分到不一樣的狀態類中,避免了分支判斷結構

順帶帶出UML類圖、組成角色、使用場景及優缺點~

  • Context (上下文信息類) → 存儲當前狀態類,並負責具體狀態的切換;
  • State (抽象狀態類) → 定義聲明狀態更新的操做方法,能夠是接口或抽象類;
  • ConcreteState (具體狀態類) → 實現抽象狀態類中定義的方法,根據具體場景指定對應狀態改變後的代碼邏輯;

使用場景

  • 某個操做含有龐大的分支判斷結構,且分支決定於對象的狀態時;
  • 對象行爲取決於狀態,且必須在運行時根據狀態改變其行爲時;

優勢

  • 符合單一職責原則:將與特定狀態相關的代碼組織到單獨的類中;
  • 更好的擴展性:擴展新的狀態只需增長實現類,在須要維護的地方設置下新狀態便可;
  • 提早定好可能的狀態,下降代碼實現複雜度,避免寫大量的if-else條件語句;

缺點

  • 類增長,每一個狀態對應一個具體狀態類;
  • 不知足開閉原則,狀態模式雖然下降了狀態之間的耦合,可是新增或修改狀態都會涉及前/後一個狀態的修改;
  • 邏輯零散,沒法在一個地方就看出整個狀態機的轉換邏輯;

0x三、補充:有限狀態機的概念

英文翻譯 Finite State Machine,縮寫FSM,簡稱狀態機,它有三個組成部分:狀態(State)事件(Event)動做(Action)。其中的事件又稱爲 轉移條件,事件觸發狀態的轉移和動做的執行(非必須)。

也能夠理解爲一種數學模型,該模型中有幾個狀態(有限的),在不一樣場景下,不一樣的狀態間發生轉移,在狀態轉移過程當中可能伴隨着不一樣的事件發生。

狀態機有三種常見的實現方式:

  • 分支邏輯法 → 缺點是改變業務邏輯,改起來容易出錯,代碼也不易看懂。適合簡單狀態機;
  • 查表法 → 適用於狀態不少、狀態轉移比較複雜的狀態機,用二維數組表示狀態轉移圖,可極大提升代碼的可讀性與可維護性;
  • 狀態模式 → 適用於狀態並很少、狀態轉移較簡單,事件觸發動做包含的業務邏輯可能較複雜的狀態機。

0x四、加餐:Android源碼中是如何使用16進制進行狀態管理的?

在Android系統源碼中涉及到 多狀態 管理老是經過十六進制數字來表示,如ViewGroup中:

static final int FLAG_CLIP_CHILDREN = 0x1;
private static final int FLAG_CLIP_TO_PADDING = 0x2;
static final int FLAG_INVALIDATE_REQUIRED  = 0x4;
private static final int FLAG_RUN_ANIMATION = 0x8;
static final int FLAG_ANIMATION_DONE = 0x10;
private static final int FLAG_PADDING_NOT_NULL = 0x20;
private static final int FLAG_ANIMATION_CACHE = 0x40;
static final int FLAG_OPTIMIZE_INVALIDATE = 0x80;
static final int FLAG_CLEAR_TRANSFORMATION = 0x100;
private static final int FLAG_NOTIFY_ANIMATION_LISTENER = 0x200;
複製代碼

這是爲何呢?先複習下幾種二進制運算:

  • 按位與(&)對應位都爲1才爲1,不然爲0,如0x1 & 0x2 → 0001 & 0010 → 0000;
  • 按位或(|)對應位有一個爲1即爲1,如0x1 | 0x2 → 0001 | 0010 → 0011
  • 取反(~)按位取反,如~0x1 → 0001 → 1110

接着以上面手機狀態爲例,寫個狀態管理的例子:

private static int state = 0;
private final static int CLOSE = 0x1; // 關機狀態
private final static int FIRST_BOOT = 0x2;   // 首次啓動
private final static int NOT_FIRST_BOOT = 0x4;    // 非首次啓動
private final static int AFTER_BOOT = 0x8;    // 啓動後
複製代碼

狀態增長 → 或運算

state | CLOSE → (0000 | 0001) → 0001 → 此時狀態:CLOSE
state | FIRST_BOOT → (0001 | 0010) → 0011 → 此時狀態:CLOSE + FIRST_BOOT
state | NOT_FIRST_BOOT → (0011 | 0100) → 0111 → 此時狀態:CLOSE + FIRST_BOOT + NOT_FIRST_BOOT
複製代碼

狀態移除 → 對應的位數從1改成0,先取反,再與運算

state &= ~NOT_FIRST_BOOT → (0111 & 1011) → 0011 → 此時狀態:CLOSE + FIRST_BOOT
state &= ~CLOSE → (0011 & 1110) → 0010 → 此時狀態:FIRST_BOOT
複製代碼

狀態判斷 → 與運算判斷結果是否爲0

// 假設此時狀態爲:CLOSE + FIRST_BOOT + NOT_FIRST_BOOT
state & FIRST_BOOT → 0111 & 0010 = 00100010 → 結果不爲0,包含此狀態;

// 假設此時狀態爲:CLOSE + FIRST_BOOT
state & NOT_FIRST_BOOT → 0011 & 0100 → 結果爲0,不包含此狀態;
複製代碼

疑惑:用來標識狀態的十六進制並非連續的,如跳過了0x3:

若是把上面的NOT_FIRST_BOOT從0x4改成0x3,而CLOSE + FIRST_BOOT 結果爲0011,同爲0x3,此時進行狀態判斷結果不爲0,難道說增長了NOT_FIRST_BOOT狀態嗎?因此這裏的取值是有固定規則的,即 左移一位

private final static int CLOSE = 1 << 0; 
private final static int FIRST_BOOT = 1 << 1;   
private final static int NOT_FIRST_BOOT = 1 << 2;    
private final static int AFTER_BOOT = 1 << 3;
複製代碼

選擇十六進制的緣由而不用其餘進制的緣由(如十進制):

計算機中,一個字節有八位,最大值爲1111111,對應十進制255,十六進制FF,半個字節用十六進制 經過一個字母 就能表示,而轉換成十進制則是一個無規律的數字,相比起十進制,十六進制轉二進制 更直觀一些。


參考文獻

相關文章
相關標籤/搜索