狀態模式

1  場景問題

1.1  實如今線投票

考慮一個在線投票的應用,要實現控制同一個用戶只能投一票,若是一個用戶反覆投票,並且投票次數超過5次,則斷定爲惡意刷票,要取消該用戶投票的資格,固然同時也要取消他所投的票。若是一個用戶的投票次數超過8次,將進入黑名單,禁止再登陸和使用系統。html

該怎麼實現這樣的功能呢?java

1.2  不用模式的解決方案

       分析上面的功能,爲了控制用戶投票,須要記錄用戶所投票的記錄,同時還要記錄用戶投票的次數,爲了簡單,直接使用兩個Map來記錄。數據庫

       在投票的過程當中,又有四種狀況:編程

  • 一是用戶是正常投票設計模式

  • 二是用戶正常投票事後,有意或者無心的重複投票緩存

  • 三是用戶惡意投票多線程

  • 四是黑名單用戶併發

這幾種狀況下對應的處理是不同的。看看代碼吧,示例代碼以下:框架

/**
 * 投票管理
 */
public class VoteManager {
    /**
     * 記錄用戶投票的結果,Map<String,String>對應Map<用戶名稱,投票的選項>
     */
    private Map<String,String> mapVote =
new HashMap<String,String>();
    /**
     * 記錄用戶投票次數,Map<String,Integer>對應Map<用戶名稱,投票的次數>
     */
    private Map<String,Integer> mapVoteCount =
new HashMap<String,Integer>();
    /**
     * 投票
     * @param user 投票人,爲了簡單,就是用戶名稱
     * @param voteItem 投票的選項
     */
    public void vote(String user,String voteItem){
       //1:先爲該用戶增長投票的次數
       //先從記錄中取出已有的投票次數
       Integer oldVoteCount = mapVoteCount.get(user);
       if(oldVoteCount==null){
           oldVoteCount = 0;
       }
       oldVoteCount = oldVoteCount + 1;
       mapVoteCount.put(user, oldVoteCount);
      
       //2:判斷該用戶投票的類型,究竟是正常投票、重複投票、惡意投票
//仍是上黑名單,而後根據投票類型來進行相應的操做  
       if(oldVoteCount==1){
           //正常投票
           //記錄到投票記錄中
           mapVote.put(user, voteItem);
           System.out.println("恭喜你投票成功");
       }else if(oldVoteCount>1 && oldVoteCount<5){
           //重複投票
           //暫時不作處理
           System.out.println("請不要重複投票");
       }else if(oldVoteCount >= 5 && oldVoteCount<8){
           //惡意投票
           //取消用戶的投票資格,並取消投票記錄
           String s = mapVote.get(user);
           if(s!=null){
              mapVote.remove(user);
           }
           System.out.println("你有惡意刷票行爲,取消投票資格");
       }else if(oldVoteCount>=8){
           //黑名單
           //記入黑名單中,禁止登陸系統了
           System.out.println("進入黑名單,將禁止登陸和使用本系統");
       }
    }
}

寫個客戶端來測試看看,是否能知足功能要求,示例代碼以下:ide

public class Client {
    public static void main(String[] args) {
       VoteManager vm = new VoteManager();
       for(int i=0;i<8;i++){
           vm.vote("u1", "A");
       }
    }
}

運行結果以下:

恭喜你投票成功
請不要重複投票
請不要重複投票
請不要重複投票
你有惡意刷票行爲,取消投票資格
你有惡意刷票行爲,取消投票資格
你有惡意刷票行爲,取消投票資格
進入黑名單,將禁止登陸和使用本系統

1.3  有何問題

看起來很簡單,是否是?幸好這裏只是示意,不然,你想一想,在vote()方法中那麼多判斷,還有每一個判斷對應的功能處理都放在一塊兒,是否是有點太雜亂了,那簡直就是個大雜燴,若是把每一個功能都完整的實現出來,那vote()方法會很長的。

       一個問題是:若是如今要修改某種投票狀況所對應的具體功能處理,那就須要在那個大雜燴裏面,找到相應的代碼塊,而後進行改動。

另一個問題是:若是要添加新的功能,好比投票超過8次但不足10次的,給個機會,只是禁止登陸和使用系統3天,若是再犯,才永久封掉帳號,該怎麼辦呢?那就須要改動投票管理的源代碼,在上面的if-else結構中再添加一個else if塊進行處理。

無論哪種狀況,都是在一大堆的控制代碼裏面找出須要的部分,而後進行修改,這歷來都不是好方法,那麼該如何實現才能作到:既可以很容易的給vote()方法添加新的功能,又可以很方便的修改已有的功能處理呢?

2  解決方案

2.1  狀態模式來解決

用來解決上述問題的一個合理的解決方案就是狀態模式。那麼什麼是狀態模式呢?

(1)狀態模式定義

wKioL1lptWKANq1FAAClWYkaadY663.png

(2)應用狀態模式來解決的思路

       仔細分析上面的問題,會發現,那幾種用戶投票的類型,就至關因而描述了人員的幾種投票狀態,而各個狀態和對應的功能處理具備很強的對應性,有點相似於「一個蘿蔔一個坑」,各個狀態下的處理基本上都是不同的,也不存在能夠相互替換的可能。

       爲了解決上面提出的問題,很天然的一個設計就是首先把狀態和狀態對應的行爲從原來的大雜燴代碼中分離出來,把每一個狀態所對應的功能處理封裝在一個獨立的類裏面,這樣選擇不一樣處理的時候,其實就是在選擇不一樣的狀態處理類。

       而後爲了統一操做這些不一樣的狀態類,定義一個狀態接口來約束它們,這樣外部就能夠面向這個統一的狀態接口編程,而無需關心具體的狀態類實現了。

       這樣一來,要修改某種投票狀況所對應的具體功能處理,那就是直接修改或者擴展某個狀態處理類的功能就能夠了。而要添加新的功能就更簡單,直接添加新的狀態處理類就能夠了,固然在使用Context的時候,須要設置使用這個新的狀態類的實例。

2.2  模式結構和說明

狀態模式的結構如圖所示:

wKiom1lptbvAdEOuAAFTSNCER78388.png

Context

       環境,也稱上下文,一般用來定義客戶感興趣的接口,同時維護一個來具體處理當前狀態的實例對象。

State

       狀態接口,用來封裝與上下文的一個特定狀態所對應的行爲。

ConcreteState

       具體實現狀態處理的類,每一個類實現一個跟上下文相關的狀態的具體處理。

2.3  狀態模式示例代碼

(1)首先來看狀態接口,示例代碼以下:

/**
 * 封裝與Context的一個特定狀態相關的行爲
 */
public interface State {
    /**
     * 狀態對應的處理
     * @param sampleParameter 示例參數,說明能夠傳入參數,具體傳入
     *             什麼樣的參數,傳入幾個參數,由具體應用來具體分析
     */
    public void handle(String sampleParameter);
}

(2)再來看看具體的狀態實現,目前具體的實現ConcreteStateA和ConcreteStateB示範的是同樣的,只是名稱不一樣,示例代碼以下:

/**
 * 實現一個與Context的一個特定狀態相關的行爲
 */
public class ConcreteStateA implements State {
    public void handle(String sampleParameter) {
       //實現具體的處理
    }
}
/**
 * 實現一個與Context的一個特定狀態相關的行爲
 */
public class ConcreteStateB implements State {
    public void handle(String sampleParameter) {
       //實現具體的處理
    }
}

(3)再來看看上下文的具體實現,上下文一般用來定義客戶感興趣的接口,同時維護一個具體的處理當前狀態的實例對象。示例代碼以下:

/**
 * 定義客戶感興趣的接口,一般會維護一個State類型的對象實例
 */
public class Context {
	/**
	 * 持有一個State類型的對象實例
	 */
	private State state;
	/**
	 * 設置實現State的對象的實例 
	 * @param state 實現State的對象的實例 
	 */
	public void setState(State state) {
		this.state = state;
	}
	/**
	 * 用戶感興趣的接口方法
	 * @param sampleParameter 示意參數
	 */
	public void request(String sampleParameter) {
		//在處理中,會轉調state來處理
		state.handle(sampleParameter);
	}
}

2.4  使用狀態模式重寫示例

       看完了上面的狀態模式的知識,有些朋友躍躍欲試,打算使用狀態模式來重寫前面的示例,要使用狀態模式,首先就須要把投票過程的各類狀態定義出來,而後把這些狀態對應的處理從原來大雜燴的實現中分離出來,造成獨立的狀態處理對象。而原來的投票管理的對象就至關於Context了。

       把狀態對應的行爲分離出去事後,怎麼調用呢?

       按照狀態模式的示例,是在Context中,處理客戶請求的時候,轉調相應的狀態對應的具體的狀態處理類來進行處理。

       那就引出下一個問題:那麼這些狀態怎麼變化呢?

       看原來的實現,就是在投票方法裏面,根據投票的次數進行判斷,並維護投票類型的變化。那好,也依葫蘆畫瓢,就在投票方法裏面來維護狀態變化。

       這個時候的程序結構如圖所示:

wKiom1lptnWB3zSzAAGgiiaXjAY955.png

(1)先來看狀態接口的代碼實現,示例代碼以下:

/**
 * 封裝一個投票狀態相關的行爲
 */
public interface VoteState {
    /**
     * 處理狀態對應的行爲
     * @param user 投票人
     * @param voteItem 投票項
     * @param voteManager 投票上下文,用來在實現狀態對應的功能處理的時候,
     *                    能夠回調上下文的數據
     */
    public void vote(String user,String voteItem
,VoteManager voteManager);
}

(2)定義了狀態接口,那就該來看看如何實現各個狀態對應的處理了,如今的實現很簡單,就是把原來的實現從投票管理類裏面分離出來就能夠了。先看正常投票狀態對應的處理,示例代碼以下:

public class NormalVoteState implements VoteState{
	public void vote(String user, String voteItem, VoteManager voteManager) {
		//正常投票
		//記錄到投票記錄中
		voteManager.getMapVote().put(user, voteItem);
		System.out.println("恭喜你投票成功");
	}
}

 接下來看看重複投票狀態對應的處理,示例代碼以下:

public class RepeatVoteState implements VoteState{
    public void vote(String user, String voteItem
,VoteManager voteManager) {
       //重複投票
       //暫時不作處理
       System.out.println("請不要重複投票");
    }
}

接下來看看惡意投票狀態對應的處理,示例代碼以下:

public class SpiteVoteState implements VoteState{
    public void vote(String user, String voteItem
,VoteManager voteManager) {
       //惡意投票
       //取消用戶的投票資格,並取消投票記錄
       String s = voteManager.getMapVote().get(user);
       if(s!=null){
           voteManager.getMapVote().remove(user);
       }
       System.out.println("你有惡意刷票行爲,取消投票資格");
    }
}

接下來看看黑名單狀態對應的處理,示例代碼以下:

public class BlackVoteState implements VoteState{
    public void vote(String user, String voteItem
,VoteManager voteManager) {
       //黑名單
       //記入黑名單中,禁止登陸系統了
       System.out.println("進入黑名單,將禁止登陸和使用本系統");
    }
}

(3)定義好了狀態接口和狀態實現,看看如今的投票管理,至關於狀態模式中的上下文,相對而言,它的改變以下:

  • 添加持有狀態處理對象

  • 添加能獲取記錄用戶投票結果的Map的方法,各個狀態處理對象,在進行狀態對應的處理的時候,須要獲取上下文中的記錄用戶投票結果的Map數據

  • 在vote()方法實現裏面,原來判斷投票類型就變成了判斷投票的狀態,而原來每種投票類型對應的處理,如今已經封裝到對應的狀態對象裏面去了,所以直接轉調對應的狀態對象的方法便可

示例代碼以下:

/**
 * 投票管理
 */
public class VoteManager {
    /**
     * 持有狀態處理對象
     */
    private VoteState state = null;
    /**
     * 記錄用戶投票的結果,Map<String,String>對應Map<用戶名稱,投票的選項>
     */
    private Map<String,String> mapVote =
new HashMap<String,String>();
    /**
     * 記錄用戶投票次數,Map<String,Integer>對應Map<用戶名稱,投票的次數>
     */
    private Map<String,Integer> mapVoteCount =
new HashMap<String,Integer>();
    /**
     * 獲取記錄用戶投票結果的Map
     * @return 記錄用戶投票結果的Map
     */
    public Map<String, String> getMapVote() {
       return mapVote;
    }
   
    /**
     * 投票
     * @param user 投票人,爲了簡單,就是用戶名稱
     * @param voteItem 投票的選項
     */
    public void vote(String user,String voteItem){
       //1:先爲該用戶增長投票的次數
       //先從記錄中取出已有的投票次數
       Integer oldVoteCount = mapVoteCount.get(user);
       if(oldVoteCount==null){
           oldVoteCount = 0;
       }
       oldVoteCount = oldVoteCount + 1;
       mapVoteCount.put(user, oldVoteCount); 
       //2:判斷該用戶投票的類型,就至關因而判斷對應的狀態
       //究竟是正常投票、重複投票、惡意投票仍是上黑名單的狀態
       if(oldVoteCount==1){
           state = new NormalVoteState();
       }else if(oldVoteCount>1 && oldVoteCount<5){
           state = new RepeatVoteState();
       }else if(oldVoteCount >= 5 && oldVoteCount<8){
           state = new SpiteVoteState();
       }else if(oldVoteCount>=8){
           state = new BlackVoteState();
       }
 
       //而後轉調狀態對象來進行相應的操做
       state.vote(user, voteItem, this);
    }
}

(4)該寫個客戶端來測試一下了,通過這麼修改事後,好用嗎?試試看就知道了。客戶端沒有任何的改變,跟前面實現的同樣,示例代碼以下:

public class Client {
    public static void main(String[] args) {
       VoteManager vm = new VoteManager();
       for(int i=0;i<8;i++){
           vm.vote("u1", "A");
       }
    }
}

運行一下試試吧,結果應該是跟前面同樣的,也就是說都是實現同樣的功能,只是採用了狀態模式來實現。測試結果以下:

恭喜你投票成功
請不要重複投票
請不要重複投票
請不要重複投票
你有惡意刷票行爲,取消投票資格
你有惡意刷票行爲,取消投票資格
你有惡意刷票行爲,取消投票資格
進入黑名單,將禁止登陸和使用本系統

從上面的示例能夠看出,狀態的轉換基本上都是內部行爲,主要在狀態模式內部來維護。好比對於投票的人員,任什麼時候候他的操做都是投票,可是投票管理對象的處理卻不必定同樣,會根據投票的次數來判斷狀態,而後根據狀態去選擇不一樣的處理。

3  模式講解

3.1  認識狀態模式

(1)狀態和行爲

       所謂對象的狀態,一般指的就是對象實例的屬性的值;而行爲指的就是對象的功能,再具體點說,行爲多半能夠對應到方法上。

       狀態模式的功能就是分離狀態的行爲,經過維護狀態的變化,來調用不一樣的狀態對應的不一樣的功能。

       也就是說,狀態和行爲是相關聯的,它們的關係能夠描述爲:狀態決定行爲

       因爲狀態是在運行期被改變的,所以行爲也會在運行期,根據狀態的改變而改變,看起來,同一個對象,在不一樣的運行時刻,行爲是不同的,就像是類被修改了同樣。

(2)行爲的平行性

       注意是平行性而不是平等性。所謂平行性指的是各個狀態的行爲所處的層次是同樣的,相互是獨立的、沒有關聯的,是根據不一樣的狀態來決定到底走平行線的那一條,行爲是不一樣的,固然對應的實現也是不一樣的,相互之間是不可替換的。如圖所示:

wKiom1lpt6SzQ2ZsAACyAFLF0w8113.png

而平等性強調的是可替換性,你們是同一行爲的不一樣描述或實現,所以在同一個行爲發生的時候,能夠根據條件來挑選任意一個實現來進行相應的處理。如圖所示:

wKiom1lpt9WzxZY9AAB-9-YwnqU035.png

你們可能會發現狀態模式的結構和策略模式的結構徹底同樣,可是,它們的目的、實現、本質都是徹底不同的。這個行爲之間的特性也是狀態模式和策略模式一個很重要的區別,狀態模式的行爲是平行性的,不可相互替換的;而策略模式的行爲是平等性的,是能夠相互替換的。

(3)上下文和狀態處理對象

       在狀態模式中,上下文是持有狀態的對象,可是上下文自身並不處理跟狀態相關的行爲,而是把處理狀態的功能委託給了狀態對應的狀態處理類來處理。

       在具體的狀態處理類裏面常常須要獲取上下文自身的數據,甚至在必要的時候會回調上下文的方法,所以,一般將上下文自身看成一個參數傳遞給具體的狀態處理類。

       客戶端通常只和上下文交互,客戶端能夠用狀態對象來配置一個上下文,一旦配置完畢,就再也不須要和狀態對象打交道了,客戶端一般不負責運行期間狀態的維護,也不負責決定到底後續使用哪個具體的狀態處理對象。

(4)不完美的OCP體驗

好了,已經使用狀態模式來重寫了前面的示例,那麼到底能不能解決前面提出的問題呢?也就是修改和擴展方不方便呢?一塊兒來看一下。

       先看修改已有的功能吧,因爲如今每一個狀態對應的處理已經封裝到對應的狀態類裏面了,要修改已有的某個狀態的功能,直接擴展某個類進行修改就行了,對其它的程序沒有影響。好比:如今要修改正常投票狀態對應的功能,對於正常投票的用戶給予積分獎勵,那麼只須要擴展正常投票狀態對應的類,而後進行修改,示例代碼以下:

public class NormalVoteState2 extends NormalVoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //先調用已有的功能
       super.vote(user, voteItem, voteManager);
       //給予積分獎勵,示意一下
       System.out.println("獎勵積分10分");
    }
}

  一切良好,對吧,但是怎麼讓VoteManager能使用這個新的實現類呢?按照目前的實現,沒有辦法,只好去修改VoteManager的vote()方法中對狀態的維護代碼了,把使用NormalVoteState的地方換成使用NormalVoteState2。

再看看如何添加新的功能,好比投票超過8次但不足10次的,給個機會,只是禁止登陸和使用系統3天,若是再犯,才進入黑名單。要實現這個功能,先要對原來的投票超過8次進入黑名單的功能進行修改,修改爲投票超過10次才進入黑名單;而後新加入一個功能,實現超過8次但不足10次的,只是禁止登陸和使用系統3天的功能。把這個新功能實現出來,示例代碼以下:

public class BlackWarnVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //待進黑名單警告狀態
       System.out.println("禁止登陸和使用系統3天");
    }
}

 實現好了這個類,該怎樣加入到已有的系統呢?

       一樣須要去修改上下文的vote()方法中對於狀態判斷和維護的代碼,示例代碼以下:

if(oldVoteCount==1){
           state = new NormalVoteState2();
       }else if(oldVoteCount>1 && oldVoteCount<5){
           state = new RepeatVoteState();
       }else if(oldVoteCount >= 5 && oldVoteCount<8){
           state = new SpiteVoteState();
       }else if(oldVoteCount>=8 && oldVoteCount<10){
           state = new BlackWarnVoteState();
       }else if(oldVoteCount>10){
           state = new BlackVoteState();
       }

好像也實現了功能是否是,並且改動起來確實也變得簡單點了,可是仔細想一想,是否是沒有徹底遵循OCP原則?結論是很顯然的,明顯沒有徹底遵循OCP原則

這裏要說明一點,設計原則是你們在設計和開發中儘可能去遵照的,但不是必定要遵照,尤爲是徹底遵照,在實際開發中,徹底遵照那些設計原則幾乎是不可能完成的任務。

就像狀態模式的實際實現中,因爲狀態的維護和轉換在狀態模式結構裏面,無論你是擴展了狀態實現類,仍是新添加了狀態實現類,都須要修改狀態維護和轉換的地方,以使用新的實現。

雖然能夠有好幾個地方來維護狀態的變化,這個後面會講到,可是都是在狀態模式結構裏面的,因此都有這個問題,算是不完美的OCP體驗吧。

(5)建立和銷燬狀態對象

在應用狀態模式的時候,有一個常見的考慮,那就是:究竟什麼時候建立和銷燬狀態對象。常見的有幾個選擇:

  • 一個是當須要使用狀態對象的時候建立,使用完後就銷燬它們

  • 另外一個是提早建立它們而且始終不銷燬

  • 還有一種是採用延遲加載和緩存合用的方式,就是當第一次須要使用狀態對象的時候建立,使用完後並不銷燬對象,而是把這個對象緩存起來,等待下一次使用,並且在合適的時候,會由緩存框架銷燬狀態對象

怎麼選擇呢?下面給出選擇建議:

若是要進入的狀態在運行時是不可知的,並且上下文是比較穩定的,不會常常改變狀態,並且使用也不頻繁,這個時候建議選第一種方案。

若是狀態改變很頻繁,也就是須要頻繁的建立狀態對象,並且狀態對象還存儲着大量的信息數據,這種狀況建議選第二種方案。

若是沒法肯定狀態改變是否頻繁,並且有些狀態對象的狀態數據量大,有些比較小,一切都是未知的,建議選第三種方案。

事實上,在實際工程開發中,第三種方案是首選,由於它兼顧了前面兩種方案的優勢,而又避免了它們的缺點,幾乎能適應各類狀況的須要。只是這個方案在實現的時候,要實現一個合理的緩存框架,並且要考慮多線程併發的問題,由於須要由緩存框架來在合適的時候銷燬狀態對象,所以實現上難度稍高點。另外在實現中還能夠考慮結合享元模式,經過享元模式來共享狀態對象。

(6)狀態模式的調用順序示意圖

狀態模式在實現上,對於狀態的維護有不一樣的實現方式,前面的示例中,採用的是在Context中進行狀態的維護和轉換,這裏就先畫出這種方式的調用順序示意圖,其它的方式在後面講到了再畫。

在Context進行狀態維護和轉換的調用順序示意圖如圖所示:

wKiom1lpuEqhPL87AAC7SqgQTGU872.png在Context進行狀態維護和轉換的調用順序示意圖


3.2  狀態的維護和轉換控制

       所謂狀態的維護,指的就是維護狀態的數據,就是給狀態設置不一樣的狀態值;而狀態的轉換,指的就是根據狀態的變化來選擇不一樣的狀態處理對象。在狀態模式中,一般有兩個地方能夠進行狀態的維護和轉換控制。

       一個就是在上下文當中,由於狀態自己一般被實現爲上下文對象的狀態,所以能夠在上下文裏面進行狀態維護,固然也就能夠控制狀態的轉換了。前面投票的示例就是採用的這種方式。

       另一個地方就是在狀態的處理類裏面,當每一個狀態處理對象處理完自身狀態所對應的功能後,能夠根據須要指定後繼的狀態,以便讓應用能正確處理後續的請求。

       先看看示例,爲了對比學習,就來看看如何把前面投票的例子修改爲:在狀態處理類裏面進行後續狀態的維護和轉換。

(1)一樣先來看投票狀態的接口,沒有變化,示例代碼以下:

/**
 * 封裝一個投票狀態相關的行爲
 */
public interface VoteState {
    /**
     * 處理狀態對應的行爲
     * @param user 投票人
     * @param voteItem 投票項
     * @param voteManager 投票上下文,用來在實現狀態對應的功能處理的時候,
     *                    能夠回調上下文的數據
     */
    public void vote(String user,String voteItem
,VoteManager voteManager);
}

(2)對於各個具體的狀態實現對象,主要的變化在於:在處理完本身狀態對應的功能後,還須要維護和轉換狀態對象。

一個一個來看吧,先看看正常投票的狀態處理對象,示例代碼以下:

public class NormalVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //正常投票,記錄到投票記錄中
       voteManager.getMapVote().put(user, voteItem);
       System.out.println("恭喜你投票成功");
       //正常投票完成,維護下一個狀態,同一我的再投票就重複了
       voteManager.getMapState().put(user,new RepeatVoteState());
    }
}

接下來看看重複投票狀態對應的處理對象,示例代碼以下:

public class RepeatVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //重複投票,暫時不作處理
       System.out.println("請不要重複投票");   
       //重複投票完成,維護下一個狀態,重複投票到5次,就算惡意投票了
       //注意這裏是判斷大於等於4,由於這裏設置的是下一個狀態
       //下一個操做次數就是5了,就應該算是惡意投票了
       if(voteManager.getMapVoteCount().get(user) >= 4){
           voteManager.getMapState().put(user,
new SpiteVoteState());
       }
    }
}

接下來看看惡意投票狀態對應的處理對象,示例代碼以下:

public class SpiteVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //惡意投票,取消用戶的投票資格,並取消投票記錄
       String s = voteManager.getMapVote().get(user);
       if(s!=null){
           voteManager.getMapVote().remove(user);
       }
       System.out.println("你有惡意刷票行爲,取消投票資格");    
       //惡意投票完成,維護下一個狀態,投票到8次,就進黑名單了
       //注意這裏是判斷大於等於7,由於這裏設置的是下一個狀態
       //下一個操做次數就是8了,就應該算是進黑名單了
       if(voteManager.getMapVoteCount().get(user) >= 7){
           voteManager.getMapState().put(user,
new BlackVoteState());
       }
    }
}

接下來看看黑名單狀態對應的處理對象,沒什麼變化,示例代碼以下:

public class BlackVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //黑名單,記入黑名單中,禁止登陸系統了
       System.out.println("進入黑名單,將禁止登陸和使用本系統");
    }
}

(3)該來看看如今的投票管理類該如何實現了,跟在上下文中維護和轉換狀態相比,大體有以下的變化:

  • 須要按照每一個用戶來記錄他們對應的投票狀態,不一樣的用戶,對應的投票狀態是不一樣的,所以使用一個Map來記錄,而再也不是原來的一個單一的投票狀態對象。
        可能有些朋友會問,那爲何前面的實現能夠呢?那是由於投票狀態是由投票管理對象集中控制的,不一樣的人員在進入投票方法的時候,是從新判斷該人員具體的狀態對象的,而如今是要把狀態維護分散到各個狀態類裏面去,所以須要記錄各個狀態類判斷事後的結果。

  • 須要把記錄投票狀態的數據,還有記錄投票次數的數據,提供相應的getter方法,各個狀態在處理的時候須要經過這些方法來訪問數據。

  • 原來在vote()方法裏面進行的狀態控制和轉換去掉,變成直接根據人員來從狀態記錄的Map中獲取對應的狀態對象了。

看看實現代碼吧,示例代碼以下:

public class VoteManager {
    /**
     * 記錄當前每一個用戶對應的狀態處理對象,每一個用戶當前的狀態是不一樣的
     * Map<String,VoteState>對應Map<用戶名稱,當前對應的狀態處理對象>
     */
    private Map<String,VoteState> mapState =
new HashMap<String,VoteState>();
    /**
     * 記錄用戶投票的結果,Map<String,String>對應Map<用戶名稱,投票的選項>
     */
    private Map<String,String> mapVote =
new HashMap<String,String>();
    /**
     * 記錄用戶投票次數,Map<String,Integer>對應Map<用戶名稱,投票的次數>
     */
    private Map<String,Integer> mapVoteCount =
new HashMap<String,Integer>();
    /**
     * 獲取記錄用戶投票結果的Map
     * @return 記錄用戶投票結果的Map
     */
    public Map<String, String> getMapVote() {
       return mapVote;
    }
    /**
     * 獲取記錄每一個用戶對應的狀態處理對象的Map
     * @return 記錄每一個用戶對應的狀態處理對象的Map
     */
    public Map<String, VoteState> getMapState() {
       return mapState;
    }
    /**
     * 獲取記錄每一個用戶對應的投票次數的Map
     * @return 記錄每一個用戶對應的投票次數的Map
     */
    public Map<String, Integer> getMapVoteCount() {
       return mapVoteCount;
    }
    /**
     * 投票
     * @param user 投票人,爲了簡單,就是用戶名稱
     * @param voteItem 投票的選項
     */
    public void vote(String user,String voteItem){
       //1:先爲該用戶增長投票的次數
       //先從記錄中取出已有的投票次數
       Integer oldVoteCount = mapVoteCount.get(user);
       if(oldVoteCount==null){
           oldVoteCount = 0;
       }
       oldVoteCount = oldVoteCount + 1;
       mapVoteCount.put(user, oldVoteCount);    
 
       //2:獲取該用戶的投票狀態
       VoteState state = mapState.get(user);
       //若是沒有投票狀態,說明尚未投過票,就初始化一個正常投票狀態
       if(state==null){
           state = new NormalVoteState();
       }
 
       //而後轉調狀態對象來進行相應的操做
       state.vote(user, voteItem, this);
    }
}

(4)實現得差很少了,該來測試了,客戶端沒有變化,去運行一下,看看效果,看看兩種維護狀態變化的方式實現的結果同樣嗎?答案應該是同樣的。

       那麼到底如何選擇這兩種方式呢?

  • 通常狀況下,若是狀態轉換的規則是必定的,通常不須要進行什麼擴展規則,那麼就適合在上下文中統一進行狀態的維護。

  • 若是狀態的轉換取決於前一個狀態動態處理的結果,或者是依賴於外部數據,爲了加強靈活性,這種狀況下,通常是在狀態處理類裏面進行狀態的維護。

(5)採用讓狀態對象來維護和轉換狀態的調用順序示意圖如圖所示:

wKiom1lpuXKRn0kAAAEJdqzhYZo942.png狀態對象來維護和轉換狀態的調用順序示意圖


(6)再來看看這種實現方式下,如何修改已有的功能,或者是添加新的狀態處理。

要修改已有的功能,一樣是找到對應的狀態處理對象,要麼直接修改,要麼擴展,前面已經示例過了,就再也不贅述了。

對於添加新的狀態處理的功能,這種實現方式會比較簡單。先直接添加新的狀態處理的類,而後去找到須要轉換到這個新狀態的狀態處理類,修改那個處理類,讓其轉換到這個新狀態就能夠了。

好比仍是來實現那個:投票超過8次但不足10次的,給個機會,只是禁止登陸和使用系統3天,若是再犯,才進入黑名單的功能。按照如今的方式,示例代碼以下:

public class BlackWarnVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //待進黑名單警告狀態
       System.out.println("禁止登陸和使用系統3天");
       //待進黑名單警告處理完成,維護下一個狀態,投票到10次,就進黑名單了
       //注意這裏是判斷大於等於9,由於這裏設置的是下一個狀態
       //下一個操做次數就是10了,就應該算是進黑名單了
       if(voteManager.getMapVoteCount().get(user) >= 9){
           voteManager.getMapState().put(user
, new BlackVoteState());
       }
    }
}

那麼如何加入系統呢?

再也不是去修改VoteManger了,而是找到應該轉換到這個新狀態的那個狀態,修改它的狀態維護和轉換。應該是在惡意投票處理裏面,讓它轉換到這個新的狀態,也就是把惡意投票處理裏面的下面這句話:

voteManager.getMapState().put(user, new BlackVoteState());

替換成:

voteManager.getMapState().put(user, new BlackWarnVoteState());

這樣就天然的把如今新的狀態處理添加到了已有的應用中。

3.3 使用數據庫來維護狀態

       在實際開發中,還有一個方式來維護狀態,那就是使用數據庫,在數據庫中存儲下一個狀態的識別數據,也就是說,維護下一個狀態,演化成了維護下一個狀態的識別數據,好比狀態編碼。

       這樣在程序中,經過查詢數據庫中的數據來獲得狀態編碼,而後再根據狀態編碼來建立出相應的狀態對象,而後再委託相應的狀態對象進行功能處理。

       仍是用前面投票的示例來講明,若是使用數據庫來維護狀態的話,大體如何實現。

(1)首先,就是每一個具體的狀態處理類中,本來在處理完成後,要判斷下一個狀態是什麼,而後建立下一個狀態對象,並設置回到上下文中。

若是使用數據庫的方式,那就不用建立下一個狀態對象,也不用設置回到上下文中了,而是把下一個狀態對應的編碼記入數據庫中,這樣就能夠了。仍是示意一個,看看重複投票狀態下的實現吧,示例代碼以下:

public class RepeatVoteState implements VoteState{
    public void vote(String user, String voteItem
, VoteManager voteManager) {
       //重複投票,暫時不作處理
       System.out.println("請不要重複投票");
       //重複投票完成,維護下一個狀態,重複投票到5次,就算惡意投票了
       if(voteManager.getMapVoteCount().get(user) >= 4){
           voteManager.getMapState().put(user,
new SpiteVoteState());
           //直接把下一個狀態的編碼記錄入數據庫就行了
       }
    }
}

這裏只是示意一下,並不真的去寫和數據庫操做的代碼。其它的狀態實現類,也作一樣相似的修改,就不去贅述了。

(2)在Context裏面,也就是投票管理對象裏面,就不須要那個記錄全部用戶狀態的Map了,直接從數據庫中獲取該用戶當前對應的狀態編碼,而後根據狀態編碼來建立出狀態對象來。原有的示例代碼以下:

//2:獲取該用戶的投票狀態
       VoteState state = mapState.get(user);
       //若是沒有投票狀態,說明尚未投過票,就初始化一個正常投票狀態
       if(state==null){
           state = new NormalVoteState();
       }

如今被修改爲,示例代碼以下:

VoteState state = null;
       //2:直接從數據庫獲取該用戶對應的下一個狀態的狀態編碼
       String stateId = "從數據庫中獲取這個狀態編碼";
       //開始根據狀態編碼來建立需用的狀態對象
       if(stateId==null || stateId.trim().length()==0){
           //若是沒有值,說明尚未投過票,就初始化一個正常投票狀態
           state = new NormalVoteState();
       }else if("重複投票".equals(stateId)){
           state = new RepeatVoteState();
       }else if("惡意投票".equals(stateId)){
           state = new SpiteVoteState();
       }else if("黑名單".equals(stateId)){
           state = new BlackVoteState();
       }

 可能有些朋友會發現,若是向數據庫裏面存儲下一個狀態對象的狀態編碼,那麼上下文中就不須要再持有狀態對象了,有點至關於把這個功能放到數據庫中了。有那麼點類似性,不過要注意,數據庫存儲的只是狀態編碼,而不是狀態對象,獲取到數據庫中的狀態編碼事後,在程序裏面仍是須要根據狀態編碼去真正建立對應的狀態對象。

       固然,要想程序更通用一點,能夠經過配置文件來配置狀態編碼和對應的狀態處理類,固然也能夠直接在數據庫中記錄狀態編碼和對應的狀態處理類,這樣的話,在上下文中,先獲取下一個狀態的狀態編碼,而後根據這個狀態編碼去獲取對應的類,而後能夠經過反射來建立對象,這樣實現就避免了那一長串的if-else,並且之後添加新的狀態編碼和狀態處理對象也不用再修改代碼了。示例代碼以下:

       VoteState state = null;
       //2:直接從數據庫獲取該用戶對應的下一個狀態的狀態編碼
       String stateId = "從數據庫中獲取這個值";
       //開始根據狀態編碼來建立需用的狀態對象
      
       //根據狀態編碼去獲取相應的類
       String className = "根據狀態編碼去獲取相應的類";
       //使用反射建立對象實例,簡單示意一下
       Class c = Class.forName(className);
       state = (VoteState)c.newInstance();

直接把「轉移」記錄到數據庫中

還有一種狀況是直接把「轉移」記錄到數據庫中,這樣會更靈活。所謂轉移,指的就是描述從A狀態到B狀態的這麼一個轉換變化。

好比:在正常投票狀態處理對象裏面指定使用「轉移A」,而「轉移A」描述的就是從正常投票狀態轉換成重複投票狀態。這樣一來,假現在後想要讓正常投票處理事後變換成惡意投票狀態,那麼就不須要修改程序,直接修改數據庫中的數據,把數據庫中「轉移A」的描述數據修改一下,使其描述從正常投票狀態轉換成惡意投票狀態就能夠了。

3.4  模擬工做流

       作企業應用的朋友,大多數都接觸過工做流,至少處理過業務流程。固然對於工做流,複雜的應用可能會使用工做流中間件,用工做流引擎來負責流程處理,這個會比較複雜,其實工做流引擎的實現也能夠應用上狀態模式,這裏不去討論。

簡單點的,把流程數據存放在數據庫裏面,而後在程序裏面本身來進行流程控制。對於簡單點的業務流程控制,可使用狀態模式來輔助進行流程控制,由於大部分這種流程都是狀態驅動的。

       舉個例子來講明吧,舉個最多見的「請假流程」,流程是這樣的:當某人提出請假申請事後,先由項目經理來審批,若是項目經理不一樣意,審批就直接結束;若是項目經理贊成了,再看請假的天數是否超過3天,項目經理的審批權限只有3天之內,若是請假天數在3天之內,那麼審批也直接結束,不然就提交給部門經理;部門經理審覈事後,不管是否贊成,審批都直接結束。流程圖如圖所示:

wKiom1lpuoWQLcq9AAESvMzDass368.png

 在實際開發中,若是不考慮使用工做流軟件,按照流程來本身實現的話,這個流程基本的運行過程簡化描述以下:

  • 1:UI操做:請假人填寫請假單,提出請假申請

  • 2:後臺處理:保存請假單數據到數據庫中,而後爲項目經理建立一個工做,把工做信息保存到數據庫中

  • 3:UI操做:項目經理登陸系統,獲取本身的工做列表

  • 4:後臺處理:從數據庫中獲取相應的工做列表

  • 5:UI操做:項目經理完成審覈工做,提交保存

  • 6:後臺處理:處理項目經理審覈的業務,保存審覈的信息到數據庫。同時判斷後續的工做,若是是須要人員參與的,就爲參與下一個工做的人員建立工做,把工做信息保存到數據庫中

  • 7:UI操做:部門經理登陸系統,獲取本身的工做列表,基本上是重複第3步

  • 8:後臺處理:從數據庫中獲取相應的工做列表,基本上是重複第4步

  • 9:UI操做:部門經理完成審覈工做,提交保存,基本上是重複第5步

  • 10:後臺處理:類推,基本上是重複第6步

1:實現思路

       仔細分析上面的流程圖和運行過程,把請假單在流程中的各個階段的狀態分析出來,會發現,整個流程徹底能夠當作是狀態驅動的。

       在上面的流程中,請假單大體有以下狀態:等待項目經理審覈、等待部門經理審覈、審覈結束。若是用狀態驅動來描述上述流程:

  • 當請假人填寫請假單,提出請假申請後,請假單的狀態是等待項目經理審覈狀態

  • 當項目經理完成審覈工做,提交保存後,若是項目經理不一樣意,請假單的狀態是審覈結束狀態;若是項目經理贊成,請假天數又在3天之內,請假單的狀態是審覈結束狀態;若是項目經理贊成,請假天數大於3天,請假單的狀態是等待部門經理審覈狀態

  • 當部門經理完成審覈工做,提交保存後,不管是否贊成,請假單的狀態都是審覈結束狀態

既然能夠把流程當作是狀態驅動的,那麼就能夠天然的使用上狀態模式,每次當相應的工做人員完成工做,請求流程響應的時候,流程處理的對象會根據當前所處的狀態,把流程處理委託給相應的狀態對象去處理。

       又考慮到在一個系統中會有不少流程,雖然不像通用工做流那麼複雜的設計,但仍是稍稍提煉一下,至少把各個不一樣的業務流程,在應用狀態模式時的公共功能,或者是架子給搭出來,以便複用這些功能。

(1)首先提供一個公共的狀態處理機

至關於一個公共的狀態模式的Context,在裏面提供基本的、公共的功能,這樣在實現具體的流程的時候,能夠簡單一些,對於要求不復雜的流程,甚至能夠直接使用。示例代碼以下:

/**
 * 公共狀態處理機,至關於狀態模式的Context
 * 包含全部流程使用狀態模式時的公共功能
 */
public  class StateMachine {
    /**
     * 持有一個狀態對象
     */
    private State state = null;
    /**
     * 包含流程處理須要的業務數據對象,不知道具體類型,爲了簡單,不去使用泛型,
     * 用Object,反正只是傳遞到具體的狀態對象裏面
     */
    private Object businessVO = null;
    /**
     * 執行工做,客戶端處理流程的接口方法。
     * 在客戶完成本身的業務工做後調用
     */
    public void doWork(){
       //轉調相應的狀態對象真正完成功能處理
       this.state.doWork(this);
    }
   
    public State getState() {
       return state;
    }
    public void setState(State state) {
       this.state = state;
    }
    public Object getBusinessVO() {
       return businessVO;
    }
    public void setBusinessVO(Object businessVO) {
       this.businessVO = businessVO;
    }
}

(2)來提供公共的狀態接口,各個狀態對象在處理流程的時候,可使用統一的接口,那麼它們須要的業務數據從何而來呢?那就經過上下文傳遞過來。示例代碼以下:

/**
 * 公共狀態接口
 */
public interface State {
    /**
     * 執行狀態對應的功能處理
     * @param ctx 上下文的實例對象
     */
    public void doWork(StateMachine ctx);
}

好了,如今架子已經搭出來了,在實現具體的流程的時候,能夠分別擴展它們,來加入跟具體流程相關的功能。

2:使用狀態模式來實現流程

(1)定義請假單的業務數據模型,示例代碼以下:

public class LeaveRequestModel {
    /**
     * 請假人
     */
    private String user;
    /**
     * 請假開始時間
     */
    private String beginDate;
    /**
     * 請假天數
     */
    private int leaveDays;
    /**
     * 審覈結果
     */
    private String result;
   
    public String getResult() {
       return result;
    }
    public void setResult(String result) {
       this.result = result;
    }
    public String getUser() {
       return user;
    }
    public String getBeginDate() {
       return beginDate;
    }
    public int getLeaveDays() {
       return leaveDays;
    }
    public void setUser(String user) {
       this.user = user;
    }
    public void setBeginDate(String beginDate) {
       this.beginDate = beginDate;
    }
    public void setLeaveDays(int leaveDays) {
       this.leaveDays = leaveDays;
    }  
}

(2)定義處理客戶端請求的上下文,雖然這裏並不須要擴展功能,但仍是繼承一下狀態機,表示能夠添加本身的處理。示例代碼以下:

public class LeaveRequestContext extends StateMachine{
    //這裏能夠擴展跟本身流程相關的處理
}

(3)來定義處理請假流程的狀態接口,雖然這裏並不須要擴展功能,但仍是繼承一下狀態,表示能夠添加本身的處理。示例代碼以下:

public interface LeaveRequestState extends State{
    //這裏能夠擴展跟本身流程相關的處理
}

(4)接下來該來實現各個狀態具體的處理對象了,先看看處理項目經理審覈的狀態類的實現,示例代碼以下:

/**
 * 處理項目經理的審覈,處理後可能對應部門經理審覈、審覈結束之中的一種
 */
public class ProjectManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把業務對象造型回來
       LeaveRequestModel lrm =
(LeaveRequestModel)request.getBusinessVO();
 
       //業務處理,把審覈結果保存到數據庫中
      
       //根據選擇的結果和條件來設置下一步
       if("贊成".equals(lrm.getResult())){
           if(lrm.getLeaveDays() > 3){
              //若是請假天數大於3天,並且項目經理贊成了,就提交給部門經理
              request.setState(new DepManagerState());
         //爲部門經理增長一個工做
           }else{
              //3天之內的請假,由項目經理作主,
//就不用提交給部門經理了,轉向審覈結束狀態
              request.setState(new  AuditOverState());
              //給申請人增長一個工做,讓他查看審覈結果
           }
       }else{
           //項目經理不一樣意的話,也就不用提交給部門經理了,轉向審覈結束狀態
           request.setState(new  AuditOverState());
          
           //給申請人增長一個工做,讓他查看審覈結果
       }         
    }  
}

接下來看看處理項目經理審覈的狀態類的實現,示例代碼以下:

/**
 * 處理部門經理的審覈,處理後對應審覈結束狀態
 */
public class DepManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把業務對象造型回來
       LeaveRequestModel lrm =
(LeaveRequestModel)request.getBusinessVO();
 
       //業務處理,把審覈結果保存到數據庫中
      
       //部門經理審覈事後,直接轉向審覈結束狀態了
       request.setState(new AuditOverState());
 
       //給申請人增長一個工做,讓他查看審覈結果
    }
}

再來看看處理審覈結束的狀態類的實現,示例代碼以下:

/**
 * 處理審覈結束的類
 */
public class AuditOverState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把業務對象造型回來
       LeaveRequestModel lrm =
(LeaveRequestModel)request.getBusinessVO();
 
       //業務處理,在數據裏面記錄整個流程結束   
    }
}

(5)因爲上面的實現中,涉及到大量須要數據庫支持的功能,同時還須要提供頁面來讓用戶操做,才能驅動流程運行,因此沒法像其它示例那樣,寫個客戶端就能進行測試。固然這個能夠在後面稍稍改變一下,模擬一下實現,就能夠運行起來看效果了。

       先來看看此時用狀態模式實現的這個流程的程序結構示意圖,如圖所示:

wKiom1lpu3mSxSdHAAGeRWiss0M870.png 用狀態模式實現的流程的程序結構示意圖


 下面來看看怎麼改造一下上面的示例,讓它能運轉起來,這樣更加有利於你們去體會在處理這種流程的應用中,如何使用狀態模式。

3:改進上面使用狀態模式來實現流程的示例

       上面的示例不能運行有兩個基本緣由:一是沒有數據庫實現部分,二是沒有界面。要解決這個問題,那就採用字符界面,來讓客戶輸入數據,另外把運行放到同一個線程裏面,這樣就不存在傳遞數據的問題,也就不須要保存數據了,數據在內存裏面。

       原來是提交了請假申請,把數據保存在數據庫裏面,而後項目經理從數據庫去獲取這些數據。如今一步到位,直接把申請數據傳遞過去,就能夠處理了。

(1)根據上面的思路,其實也就只是須要修改那幾個狀態處理對象的實現,先看看處理項目經理審覈的狀態類的實現,使用Scanner來接受命令行輸入數據,示例代碼以下:

import java.util.Scanner;
/**
 * 處理項目經理的審覈,處理後可能對應部門經理審覈、審覈結束之中的一種
 */
public class ProjectManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把業務對象造型回來
       LeaveRequestModel lrm =
(LeaveRequestModel)request.getBusinessVO();
       System.out.println("項目經理審覈中,請稍候......");
       //模擬用戶處理界面,經過控制檯來讀取數據
       System.out.println(lrm.getUser()+"申請從"
+lrm.getBeginDate()+"開始請假"+lrm.getLeaveDays()
+"天,請項目經理審覈(1爲贊成,2爲不一樣意):");
       //讀取從控制檯輸入的數據
       Scanner scanner = new Scanner(System.in);
       if(scanner.hasNext()){
           int a = scanner.nextInt();
           //設置回到上下文中
           String result = "不一樣意";
           if(a==1){
              result = "贊成";
           }
           lrm.setResult("項目經理審覈結果:"+result);
           //根據選擇的結果和條件來設置下一步
           if(a==1){
              if(lrm.getLeaveDays() > 3){
                  //若是請假天數大於3天,並且項目經理贊成了,
//就提交給部門經理
                  request.setState(new DepManagerState());
                  //繼續執行下一步工做
                  request.doWork();
              }else{
                  //3天之內的請假,由項目經理作主,就不用提交給部門經理了,
//轉向審覈結束狀態
                  request.setState(new  AuditOverState());
                  //繼續執行下一步工做
                  request.doWork();
              }            
           }else{
              //項目經理不一樣意,就不用提交給部門經理了,轉向審覈結束狀態
              request.setState(new  AuditOverState());
              //繼續執行下一步工做
              request.doWork();
           }
       }     
    }  
}

接下來看看處理項目經理審覈的狀態類的實現,示例代碼以下:

import java.util.Scanner;
/**
 * 處理部門經理的審覈,處理後對應審覈結束狀態
 */
public class DepManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把業務對象造型回來
       LeaveRequestModel lrm =
(LeaveRequestModel)request.getBusinessVO();
       System.out.println("部門經理審覈中,請稍候......");
       //模擬用戶處理界面,經過控制檯來讀取數據
       System.out.println(lrm.getUser()+"申請從"
+lrm.getBeginDate()+"開始請假"+lrm.getLeaveDays()
+"天,請部門經理審覈(1爲贊成,2爲不一樣意):");
       //讀取從控制檯輸入的數據
       Scanner scanner = new Scanner(System.in);
       if(scanner.hasNext()){
           int a = scanner.nextInt();
           //設置回到上下文中
           String result = "不一樣意";
           if(a==1){
              result = "贊成";
           }
           lrm.setResult("部門經理審覈結果:"+result);
           //部門經理審覈事後,直接轉向審覈結束狀態了
           request.setState(new AuditOverState());
           //繼續執行下一步工做
           request.doWork();
       }     
    }
}

再來看看處理審覈結束的狀態類的實現,示例代碼以下:

public class AuditOverState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把業務對象造型回來
       LeaveRequestModel lrm =
(LeaveRequestModel)request.getBusinessVO();
       System.out.println(lrm.getUser()
+",你的請假申請已經審覈結束,結果是:"+lrm.getResult());
    }
}

(2)萬事俱備,能夠寫個客戶端,來開始咱們的流程之旅了。示例代碼以下:

public class Client {
    public static void main(String[] args) {
       //建立業務對象,並設置業務數據
       LeaveRequestModel lrm = new LeaveRequestModel();
       lrm.setUser("小李");
       lrm.setBeginDate("2010-02-08");
       lrm.setLeaveDays(5);
      
       //建立上下文對象
       LeaveRequestContext request = new LeaveRequestContext();
       //爲上下文對象設置業務數據對象
       request.setBusinessVO(lrm);
       //配置上下文,做爲開始的狀態,之後就無論了
       request.setState(new ProjectManagerState());
      
       //請求上下文,讓上下文開始處理工做
       request.doWork();
    }
}

辛苦了這麼久,必定要好好的運行一下,體會在流程處理中是如何使用狀態模式的。

第一步:運行一下,剛開始會出現以下信息:

項目經理審覈中,請稍候......
小李申請從2010-02-08開始請假5天,請項目經理審覈(1爲贊成,2爲不一樣意):

    第二步:程序並無中止,在等待你輸入項目經理審覈的結果,若是你輸入1,表示贊成,那麼程序會繼續判斷,發現請假天數5天大於項目經理審覈的範圍了,會提交給部門經理審覈。在控制檯輸入1,而後回車看看,會出現以下信息:

項目經理審覈中,請稍候......
小李申請從2010-02-08開始請假5天,請項目經理審覈(1爲贊成,2爲不一樣意):
1
部門經理審覈中,請稍候......
小李申請從2010-02-08開始請假5天,請部門經理審覈(1爲贊成,2爲不一樣意):

 第三步:一樣,程序仍然沒有中止,在等待你輸入部門經理審覈的結果,假如輸入1,而後回車,看看會發生什麼,提示信息以下:

項目經理審覈中,請稍候......
小李申請從2010-02-08開始請假5天,請項目經理審覈(1爲贊成,2爲不一樣意):
1
部門經理審覈中,請稍候......
小李申請從2010-02-08開始請假5天,請部門經理審覈(1爲贊成,2爲不一樣意):
1
小李,你的請假申請已經審覈結束,結果是:部門經理審覈結果:贊成
 這個時候流程運行結束了,程序運行也結束了,有點流程控制的意味了吧。
若是在上面第一步運行事後,在第二步輸入2,也就是項目經理不一樣意,會怎樣呢?應該就不會再到部門經理了吧,試試看,運行提示信息以下:
項目經理審覈中,請稍候......
小李申請從2010-02-08開始請假5天,請項目經理審覈(1爲贊成,2爲不一樣意):
2
小李,你的請假申請已經審覈結束,結果是:項目經理審覈結果:不一樣意

(5)小結一下

       事實上,上面的程序能夠和數據庫結合起來,好比把審覈結果存放在數據庫裏面,也能夠把審覈的步驟也放到數據庫裏面,每次運行的時候從數據庫裏面獲取這些值,而後來判斷是建立哪個狀態處理類,而後執行相應的處理就能夠了。

       如今這些東西都在內存裏,因此程序不能中止,不然流程就運行不下去了。

另外,爲了演示的簡潔性,這裏作了至關的簡化,好比沒有去根據申請人選擇相應的項目經理和部門經理,也沒有去考慮若是申請人就是項目經理或者部門經理怎麼辦,只是爲了讓你們看明白狀態模式在這裏面的應用,主要是爲了體現狀態模式而不是業務。

3.5  狀態模式的優缺點

l          簡化應用邏輯控制
    狀態模式使用單獨的類來封裝一個狀態的處理。若是把一個大的程序控制分紅不少小塊,每塊定義一個狀態來表明,那麼就能夠把這些邏輯控制的代碼分散到不少單獨的狀態類當中去,這樣就把着眼點從執行狀態提升到整個對象的狀態,使得代碼結構化和意圖更清晰,從而簡化應用的邏輯控制。
    對於依賴於狀態的if-else,理論上來說,也能夠改變成應用狀態模式來實現,把每一個if或else塊定義一個狀態來表明,那麼就能夠把塊內的功能代碼移動到狀態處理類去了,從而減小if-else,避免出現巨大的條件語句。

l          更好的分離狀態和行爲
    狀態模式經過設置全部狀態類的公共接口,把狀態和狀態對應的行爲分離開來,把全部與一個特定的狀態相關的行爲都放入一個對象中,使得應用程序在控制的時候,只須要關心狀態的切換,而不用關心這個狀態對應的真正處理。

l          更好的擴展性
    引入了狀態處理的公共接口後,使得擴展新的狀態變得很是容易,只須要新增長一個實現狀態處理的公共接口的實現類,而後在進行狀態維護的地方,設置狀態變化到這個新的狀態便可。

l          顯式化進行狀態轉換
    狀態模式爲不一樣的狀態引入獨立的對象,使得狀態的轉換變得更加明確。並且狀態對象能夠保證上下文不會發生內部狀態不一致的狀況,由於上下文中只有一個變量來記錄狀態對象,只要爲這一個變量賦值就能夠了。

l          引入太多的狀態類
    狀態模式也有一個很明顯的缺點,一個狀態對應一個狀態處理類,會使得程序引入太多的狀態類,使程序變得雜亂。

3.6  思考狀態模式

1:狀態模式的本質

狀態模式的本質:根據狀態來分離和選擇行爲

       仔細分析狀態模式的結構,若是沒有上下文,那麼就退化回到只有接口和實現了,正是經過接口,把狀態和狀態對應的行爲分開,才使得經過狀態模式設計的程序易於擴展和維護。

而上下文主要負責的是公共的狀態驅動,每當狀態發生改變的時候,一般都是回調上下文來執行狀態對應的功能。固然,上下文自身也能夠維護狀態的變化,另外,上下文一般還會做爲多個狀態處理類之間的數據載體,在多個狀態處理類之間傳遞數據。

2:什麼時候選用狀態模式

       建議在以下狀況中,選用狀態模式:

  • 若是一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態來改變它的行爲。可使用狀態模式,來把狀態和行爲分離開,雖然分離開了,但狀態和行爲是有對應關係的,能夠在運行期間,經過改變狀態,就可以調用到該狀態對應的狀態處理對象上去,從而改變對象的行爲。

  • 若是一個操做中含有龐大的多分支語句,並且這些分支依賴於該對象的狀態。可使用狀態模式,把各個分支的處理分散包裝到單獨的對象處理類裏面,這樣,這些分支對應的對象就能夠不依賴於其它對象而獨立變化了。

3.7  相關模式

l          狀態模式和策略模式
    這是兩個結構相同,功能各異的模式,具體的在策略模式裏面講過了,這裏就再也不贅述了。

l          狀態模式和觀察者模式
    這兩個模式乍一看,功能是很類似的,可是又有區別,能夠組合使用。
    這兩個模式都是在狀態發生改變的時候觸發行爲,只不過觀察者模式的行爲是固定的,那就是通知全部的觀察者,而狀態模式是根據狀態來選擇不一樣的處理。
    從表面來看,兩個模式功能類似,觀察者模式中的被觀察對象就比如狀態模式中的上下文,觀察者模式中當被觀察對象的狀態發生改變的時候,觸發的通知全部觀察者的方法;就比如是狀態模式中,根據狀態的變化,選擇對應的狀態處理。
    但實際這兩個模式是不一樣的,觀察者模式的目的是在被觀察者的狀態發生改變的時候,觸發觀察者聯動,具體如何處理觀察者模式無論;而狀態模式的主要目的在於根據狀態來分離和選擇行爲,當狀態發生改變的時候,動態改變行爲。
    這兩個模式是能夠組合使用的,好比在觀察者模式的觀察者部分,當被觀察對象的狀態發生了改變,觸發通知了全部的觀察者事後,觀察者該怎麼處理呢?這個時候就可使用狀態模式,根據通知過來的狀態選擇相應的處理。

l          狀態模式和單例模式
    這兩個模式能夠組合使用,能夠把狀態模式中的狀態處理類實現成單例。

l          狀態模式和享元模式
    這兩個模式能夠組合使用。
    因爲狀態模式把狀態對應的行爲分散到多個狀態對象中,會形成不少細粒度的狀態對象,能夠把這些狀態處理對象經過享元模式來共享,從而節省資源。


轉載至:http://sishuok.com/forum/blogPost/list/5621.html

   cc老師的設計模式是我目前看過最詳細最有實踐的教程。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息