天天看點

狀态模式

考慮一個線上投票的應用,要實作控制同一個使用者隻能投一票,如果一個使用者反複投票,而且投票次數超過5次,則判定為惡意刷票,要取消該使用者投票的資格,當然同時也要取消他所投的票。如果一個使用者的投票次數超過8次,将進入黑名單,禁止再登入和使用系統。

該怎麼實作這樣的功能呢?

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

       在投票的過程中,又有四種情況:

一是使用者是正常投票

二是使用者正常投票過後,有意或者無意的重複投票

三是使用者惡意投票

四是黑名單使用者

這幾種情況下對應的處理是不一樣的。看看代碼吧,示例代碼如下:

寫個用戶端來測試看看,是否能滿足功能要求,示例代碼如下:

運作結果如下:

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

       一個問題是:如果現在要修改某種投票情況所對應的具體功能處理,那就需要在那個大雜燴裡面,找到相應的代碼塊,然後進行改動。

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

不管哪一種情況,都是在一大堆的控制代碼裡面找出需要的部分,然後進行修改,這從來都不是好方法,那麼該如何實作才能做到:既能夠很容易的給vote()方法添加新的功能,又能夠很友善的修改已有的功能處理呢?

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

(1)狀态模式定義

狀态模式

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

       仔細分析上面的問題,會發現,那幾種使用者投票的類型,就相當于是描述了人員的幾種投票狀态,而各個狀态和對應的功能處理具有很強的對應性,有點類似于“一個蘿蔔一個坑”,各個狀态下的處理基本上都是不一樣的,也不存在可以互相替換的可能。

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

       然後為了統一操作這些不同的狀态類,定義一個狀态接口來限制它們,這樣外部就可以面向這個統一的狀态接口程式設計,而無需關心具體的狀态類實作了。

       這樣一來,要修改某種投票情況所對應的具體功能處理,那就是直接修改或者擴充某個狀态處理類的功能就可以了。而要添加新的功能就更簡單,直接添加新的狀态處理類就可以了,當然在使用Context的時候,需要設定使用這個新的狀态類的執行個體。

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

狀态模式

Context:

       環境,也稱上下文,通常用來定義客戶感興趣的接口,同時維護一個來具體處理目前狀态的執行個體對象。

State:

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

ConcreteState:

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

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

(2)再來看看具體的狀态實作,目前具體的實作ConcreteStateA和ConcreteStateB示範的是一樣的,隻是名稱不同,示例代碼如下:

(3)再來看看上下文的具體實作,上下文通常用來定義客戶感興趣的接口,同時維護一個具體的處理目前狀态的執行個體對象。示例代碼如下:

       看完了上面的狀态模式的知識,有些朋友躍躍欲試,打算使用狀态模式來重寫前面的示例,要使用狀态模式,首先就需要把投票過程的各種狀态定義出來,然後把這些狀态對應的處理從原來大雜燴的實作中分離出來,形成獨立的狀态處理對象。而原來的投票管理的對象就相當于Context了。

       把狀态對應的行為分離出去過後,怎麼調用呢?

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

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

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

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

狀态模式

(1)先來看狀态接口的代碼實作,示例代碼如下:

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

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

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

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

(3)定義好了狀态接口和狀态實作,看看現在的投票管理,相當于狀态模式中的上下文,相對而言,它的改變如下:

添加持有狀态處理對象

添加能擷取記錄使用者投票結果的Map的方法,各個狀态處理對象,在進行狀态對應的處理的時候,需要擷取上下文中的記錄使用者投票結果的Map資料

在vote()方法實作裡面,原來判斷投票類型就變成了判斷投票的狀态,而原來每種投票類型對應的處理,現在已經封裝到對應的狀态對象裡面去了,是以直接轉調對應的狀态對象的方法即可

示例代碼如下:

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

運作一下試試吧,結果應該是跟前面一樣的,也就是說都是實作一樣的功能,隻是采用了狀态模式來實作。測試結果如下:

從上面的示例可以看出,狀态的轉換基本上都是内部行為,主要在狀态模式内部來維護。比如對于投票的人員,任何時候他的操作都是投票,但是投票管理對象的處理卻不一定一樣,會根據投票的次數來判斷狀态,然後根據狀态去選擇不同的處理。

(1)狀态和行為

       所謂對象的狀态,通常指的就是對象執行個體的屬性的值;而行為指的就是對象的功能,再具體點說,行為多半可以對應到方法上。

       狀态模式的功能就是分離狀态的行為,通過維護狀态的變化,來調用不同的狀态對應的不同的功能。

       也就是說,狀态和行為是相關聯的,它們的關系可以描述為:狀态決定行為。

       由于狀态是在運作期被改變的,是以行為也會在運作期,根據狀态的改變而改變,看起來,同一個對象,在不同的運作時刻,行為是不一樣的,就像是類被修改了一樣。

(2)行為的平行性

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

狀态模式

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

狀态模式

大家可能會發現狀态模式的結構和政策模式的結構完全一樣,但是,它們的目的、實作、本質都是完全不一樣的。這個行為之間的特性也是狀态模式和政策模式一個很重要的差別,狀态模式的行為是平行性的,不可互相替換的;而政策模式的行為是平等性的,是可以互相替換的。

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

       在狀态模式中,上下文是持有狀态的對象,但是上下文自身并不處理跟狀态相關的行為,而是把處理狀态的功能委托給了狀态對應的狀态處理類來處理。

       在具體的狀态處理類裡面經常需要擷取上下文自身的資料,甚至在必要的時候會回調上下文的方法,是以,通常将上下文自身當作一個參數傳遞給具體的狀态處理類。

       用戶端一般隻和上下文互動,用戶端可以用狀态對象來配置一個上下文,一旦配置完畢,就不再需要和狀态對象打交道了,用戶端通常不負責運作期間狀态的維護,也不負責決定到底後續使用哪一個具體的狀态處理對象。

(4)不完美的OCP體驗

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

       先看修改已有的功能吧,由于現在每個狀态對應的處理已經封裝到對應的狀态類裡面了,要修改已有的某個狀态的功能,直接擴充某個類進行修改就好了,對其它的程式沒有影響。比如:現在要修改正常投票狀态對應的功能,對于正常投票的使用者給予積分獎勵,那麼隻需要擴充正常投票狀态對應的類,然後進行修改,示例代碼如下:

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

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

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

       同樣需要去修改上下文的vote()方法中對于狀态判斷和維護的代碼,示例代碼如下:

好像也實作了功能是不是,而且改動起來确實也變得簡單點了,但是仔細想想,是不是沒有完全遵循OCP原則?結論是很顯然的,明顯沒有完全遵循OCP原則。

這裡要說明一點,設計原則是大家在設計和開發中盡量去遵守的,但不是一定要遵守,尤其是完全遵守,在實際開發中,完全遵守那些設計原則幾乎是不可能完成的任務。

就像狀态模式的實際實作中,由于狀态的維護和轉換在狀态模式結構裡面,不管你是擴充了狀态實作類,還是新添加了狀态實作類,都需要修改狀态維護和轉換的地方,以使用新的實作。

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

(5)建立和銷毀狀态對象

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

一個是當需要使用狀态對象的時候建立,使用完後就銷毀它們

另一個是提前建立它們并且始終不銷毀

還有一種是采用延遲加載和緩存合用的方式,就是當第一次需要使用狀态對象的時候建立,使用完後并不銷毀對象,而是把這個對象緩存起來,等待下一次使用,而且在合适的時候,會由緩存架構銷毀狀态對象

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

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

如果狀态改變很頻繁,也就是需要頻繁的建立狀态對象,而且狀态對象還存儲着大量的資訊資料,這種情況建議選第二種方案。

如果無法确定狀态改變是否頻繁,而且有些狀态對象的狀态資料量大,有些比較小,一切都是未知的,建議選第三種方案。

事實上,在實際工程開發中,第三種方案是首選,因為它兼顧了前面兩種方案的優點,而又避免了它們的缺點,幾乎能适應各種情況的需要。隻是這個方案在實作的時候,要實作一個合理的緩存架構,而且要考慮多線程并發的問題,因為需要由緩存架構來在合适的時候銷毀狀态對象,是以實作上難度稍高點。另外在實作中還可以考慮結合享元模式,通過享元模式來共享狀态對象。

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

狀态模式在實作上,對于狀态的維護有不同的實作方式,前面的示例中,采用的是在Context中進行狀态的維護和轉換,這裡就先畫出這種方式的調用順序示意圖,其它的方式在後面講到了再畫。

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

狀态模式

在Context進行狀态維護和轉換的調用順序示意圖

       所謂狀态的維護,指的就是維護狀态的資料,就是給狀态設定不同的狀态值;而狀态的轉換,指的就是根據狀态的變化來選擇不同的狀态處理對象。在狀态模式中,通常有兩個地方可以進行狀态的維護和轉換控制。

       一個就是在上下文當中,因為狀态本身通常被實作為上下文對象的狀态,是以可以在上下文裡面進行狀态維護,當然也就可以控制狀态的轉換了。前面投票的示例就是采用的這種方式。

       另外一個地方就是在狀态的處理類裡面,當每個狀态處理對象處理完自身狀态所對應的功能後,可以根據需要指定後繼的狀态,以便讓應用能正确處理後續的請求。

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

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

(2)對于各個具體的狀态實作對象,主要的變化在于:在處理完自己狀态對應的功能後,還需要維護和轉換狀态對象。

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

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

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

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

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

需要按照每個使用者來記錄他們對應的投票狀态,不同的使用者,對應的投票狀态是不同的,是以使用一個Map來記錄,而不再是原來的一個單一的投票狀态對象。

    可能有些朋友會問,那為什麼前面的實作可以呢?那是因為投票狀态是由投票管理對象集中控制的,不同的人員在進入投票方法的時候,是重新判斷該人員具體的狀态對象的,而現在是要把狀态維護分散到各個狀态類裡面去,是以需要記錄各個狀态類判斷過後的結果。

需要把記錄投票狀态的資料,還有記錄投票次數的資料,提供相應的getter方法,各個狀态在處理的時候需要通過這些方法來通路資料。

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

看看實作代碼吧,示例代碼如下:

(4)實作得差不多了,該來測試了,用戶端沒有變化,去運作一下,看看效果,看看兩種維護狀态變化的方式實作的結果一樣嗎?答案應該是一樣的。

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

一般情況下,如果狀态轉換的規則是一定的,一般不需要進行什麼擴充規則,那麼就适合在上下文中統一進行狀态的維護。

如果狀态的轉換取決于前一個狀态動态處理的結果,或者是依賴于外部資料,為了增強靈活性,這種情況下,一般是在狀态處理類裡面進行狀态的維護。

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

狀态模式

狀态對象來維護和轉換狀态的調用順序示意圖

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

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

對于添加新的狀态處理的功能,這種實作方式會比較簡單。先直接添加新的狀态處理的類,然後去找到需要轉換到這個新狀态的狀态處理類,修改那個處理類,讓其轉換到這個新狀态就可以了。

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

那麼如何加入系統呢?

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

替換成:

這樣就自然的把現在新的狀态處理添加到了已有的應用中。

       在實際開發中,還有一個方式來維護狀态,那就是使用資料庫,在資料庫中存儲下一個狀态的識别資料,也就是說,維護下一個狀态,演化成了維護下一個狀态的識别資料,比如狀态編碼。

       這樣在程式中,通過查詢資料庫中的資料來得到狀态編碼,然後再根據狀态編碼來建立出相應的狀态對象,然後再委托相應的狀态對象進行功能處理。

       還是用前面投票的示例來說明,如果使用資料庫來維護狀态的話,大緻如何實作。

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

如果使用資料庫的方式,那就不用建立下一個狀态對象,也不用設定回到上下文中了,而是把下一個狀态對應的編碼記入資料庫中,這樣就可以了。還是示意一個,看看重複投票狀态下的實作吧,示例代碼如下:

這裡隻是示意一下,并不真的去寫和資料庫操作的代碼。其它的狀态實作類,也做同樣類似的修改,就不去贅述了。

(2)在Context裡面,也就是投票管理對象裡面,就不需要那個記錄所有使用者狀态的Map了,直接從資料庫中擷取該使用者目前對應的狀态編碼,然後根據狀态編碼來建立出狀态對象來。原有的示例代碼如下:

現在被修改成,示例代碼如下:

 可能有些朋友會發現,如果向資料庫裡面存儲下一個狀态對象的狀态編碼,那麼上下文中就不需要再持有狀态對象了,有點相當于把這個功能放到資料庫中了。有那麼點相似性,不過要注意,資料庫存儲的隻是狀态編碼,而不是狀态對象,擷取到資料庫中的狀态編碼過後,在程式裡面還是需要根據狀态編碼去真正建立對應的狀态對象。

       當然,要想程式更通用一點,可以通過配置檔案來配置狀态編碼和對應的狀态處理類,當然也可以直接在資料庫中記錄狀态編碼和對應的狀态處理類,這樣的話,在上下文中,先擷取下一個狀态的狀态編碼,然後根據這個狀态編碼去擷取對應的類,然後可以通過反射來建立對象,這樣實作就避免了那一長串的if-else,而且以後添加新的狀态編碼和狀态處理對象也不用再修改代碼了。示例代碼如下:

直接把“轉移”記錄到資料庫中

還有一種情況是直接把“轉移”記錄到資料庫中,這樣會更靈活。所謂轉移,指的就是描述從A狀态到B狀态的這麼一個轉換變化。

比如:在正常投票狀态處理對象裡面指定使用“轉移A”,而“轉移A”描述的就是從正常投票狀态轉換成重複投票狀态。這樣一來,假如今後想要讓正常投票處理過後變換成惡意投票狀态,那麼就不需要修改程式,直接修改資料庫中的資料,把資料庫中“轉移A”的描述資料修改一下,使其描述從正常投票狀态轉換成惡意投票狀态就可以了。

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

簡單點的,把流程資料存放在資料庫裡面,然後在程式裡面自己來進行流程控制。對于簡單點的業務流程控制,可以使用狀态模式來輔助進行流程控制,因為大部分這種流程都是狀态驅動的。

       舉個例子來說明吧,舉個最常見的“請假流程”,流程是這樣的:當某人提出請假申請過後,先由項目經理來審批,如果項目經理不同意,審批就直接結束;如果項目經理同意了,再看請假的天數是否超過3天,項目經理的審批權限隻有3天以内,如果請假天數在3天以内,那麼審批也直接結束,否則就送出給部門經理;部門經理稽核過後,無論是否同意,審批都直接結束。流程圖如圖所示:

狀态模式

 在實際開發中,如果不考慮使用工作流軟體,按照流程來自己實作的話,這個流程基本的運作過程簡化描述如下:

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

2:背景處理:儲存請假單資料到資料庫中,然後為項目經理建立一個工作,把工作資訊儲存到資料庫中

3:UI操作:項目經理登入系統,擷取自己的工作清單

4:背景處理:從資料庫中擷取相應的工作清單

5:UI操作:項目經理完成稽核工作,送出儲存

6:背景處理:處理項目經理稽核的業務,儲存稽核的資訊到資料庫。同時判斷後續的工作,如果是需要人員參與的,就為參與下一個工作的人員建立工作,把工作資訊儲存到資料庫中

7:UI操作:部門經理登入系統,擷取自己的工作清單,基本上是重複第3步

8:背景處理:從資料庫中擷取相應的工作清單,基本上是重複第4步

9:UI操作:部門經理完成稽核工作,送出儲存,基本上是重複第5步

10:背景處理:類推,基本上是重複第6步

1:實作思路

       仔細分析上面的流程圖和運作過程,把請假單在流程中的各個階段的狀态分析出來,會發現,整個流程完全可以看成是狀态驅動的。

       在上面的流程中,請假單大緻有如下狀态:等待項目經理稽核、等待部門經理稽核、稽核結束。如果用狀态驅動來描述上述流程:

當請假人填寫請假單,提出請假申請後,請假單的狀态是等待項目經理稽核狀态

當項目經理完成稽核工作,送出儲存後,如果項目經理不同意,請假單的狀态是稽核結束狀态;如果項目經理同意,請假天數又在3天以内,請假單的狀态是稽核結束狀态;如果項目經理同意,請假天數大于3天,請假單的狀态是等待部門經理稽核狀态

當部門經理完成稽核工作,送出儲存後,無論是否同意,請假單的狀态都是稽核結束狀态

既然可以把流程看成是狀态驅動的,那麼就可以自然的使用上狀态模式,每次當相應的從業人員完成工作,請求流程響應的時候,流程處理的對象會根據目前所處的狀态,把流程處理委托給相應的狀态對象去處理。

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

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

相當于一個公共的狀态模式的Context,在裡面提供基本的、公共的功能,這樣在實作具體的流程的時候,可以簡單一些,對于要求不複雜的流程,甚至可以直接使用。示例代碼如下:

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

好了,現在架子已經搭出來了,在實作具體的流程的時候,可以分别擴充它們,來加入跟具體流程相關的功能。

2:使用狀态模式來實作流程

(1)定義請假單的業務資料模型,示例代碼如下:

(2)定義處理用戶端請求的上下文,雖然這裡并不需要擴充功能,但還是繼承一下狀态機,表示可以添加自己的處理。示例代碼如下:

(3)來定義處理請假流程的狀态接口,雖然這裡并不需要擴充功能,但還是繼承一下狀态,表示可以添加自己的處理。示例代碼如下:

(4)接下來該來實作各個狀态具體的處理對象了,先看看處理項目經理稽核的狀态類的實作,示例代碼如下:

接下來看看處理項目經理稽核的狀态類的實作,示例代碼如下:

再來看看處理稽核結束的狀态類的實作,示例代碼如下:

(5)由于上面的實作中,涉及到大量需要資料庫支援的功能,同時還需要提供頁面來讓使用者操作,才能驅動流程運作,是以無法像其它示例那樣,寫個用戶端就能進行測試。當然這個可以在後面稍稍改變一下,模拟一下實作,就可以運作起來看效果了。

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

狀态模式

 用狀态模式實作的流程的程式結構示意圖

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

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

       上面的示例不能運作有兩個基本原因:一是沒有資料庫實作部分,二是沒有界面。要解決這個問題,那就采用字元界面,來讓客戶輸入資料,另外把運作放到同一個線程裡面,這樣就不存在傳遞資料的問題,也就不需要儲存資料了,資料在記憶體裡面。

       原來是送出了請假申請,把資料儲存在資料庫裡面,然後項目經理從資料庫去擷取這些資料。現在一步到位,直接把申請資料傳遞過去,就可以處理了。

(1)根據上面的思路,其實也就隻是需要修改那幾個狀态處理對象的實作,先看看處理項目經理稽核的狀态類的實作,使用Scanner來接受指令行輸入資料,示例代碼如下:

(2)萬事俱備,可以寫個用戶端,來開始我們的流程之旅了。示例代碼如下:

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

第一步:運作一下,剛開始會出現如下資訊:

    第二步:程式并沒有停止,在等待你輸入項目經理稽核的結果,如果你輸入1,表示同意,那麼程式會繼續判斷,發現請假天數5天大于項目經理稽核的範圍了,會送出給部門經理稽核。在控制台輸入1,然後回車看看,會出現如下資訊:

 第三步:同樣,程式仍然沒有停止,在等待你輸入部門經理稽核的結果,假如輸入1,然後回車,看看會發生什麼,提示資訊如下:

(5)小結一下

       事實上,上面的程式可以和資料庫結合起來,比如把稽核結果存放在資料庫裡面,也可以把稽核的步驟也放到資料庫裡面,每次運作的時候從資料庫裡面擷取這些值,然後來判斷是建立哪一個狀态處理類,然後執行相應的處理就可以了。

       現在這些東西都在記憶體裡,是以程式不能停止,否則流程就運作不下去了。

另外,為了示範的簡潔性,這裡做了相當的簡化,比如沒有去根據申請人選擇相應的項目經理和部門經理,也沒有去考慮如果申請人就是項目經理或者部門經理怎麼辦,隻是為了讓大家看明白狀态模式在這裡面的應用,主要是為了展現狀态模式而不是業務。

l          簡化應用邏輯控制

    狀态模式使用單獨的類來封裝一個狀态的處理。如果把一個大的程式控制分成很多小塊,每塊定義一個狀态來代表,那麼就可以把這些邏輯控制的代碼分散到很多單獨的狀态類當中去,這樣就把着眼點從執行狀态提高到整個對象的狀态,使得代碼結構化和意圖更清晰,進而簡化應用的邏輯控制。

    對于依賴于狀态的if-else,理論上來講,也可以改變成應用狀态模式來實作,把每個if或else塊定義一個狀态來代表,那麼就可以把塊内的功能代碼移動到狀态處理類去了,進而減少if-else,避免出現巨大的條件語句。

l          更好的分離狀态和行為

    狀态模式通過設定所有狀态類的公共接口,把狀态和狀态對應的行為分離開來,把所有與一個特定的狀态相關的行為都放入一個對象中,使得應用程式在控制的時候,隻需要關心狀态的切換,而不用關心這個狀态對應的真正處理。

l          更好的擴充性

    引入了狀态處理的公共接口後,使得擴充新的狀态變得非常容易,隻需要新增加一個實作狀态處理的公共接口的實作類,然後在進行狀态維護的地方,設定狀态變化到這個新的狀态即可。

l          顯式化進行狀态轉換

    狀态模式為不同的狀态引入獨立的對象,使得狀态的轉換變得更加明确。而且狀态對象可以保證上下文不會發生内部狀态不一緻的情況,因為上下文中隻有一個變量來記錄狀态對象,隻要為這一個變量指派就可以了。

l          引入太多的狀态類

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

1:狀态模式的本質

狀态模式的本質:根據狀态來分離和選擇行為。

       仔細分析狀态模式的結構,如果沒有上下文,那麼就退化回到隻有接口和實作了,正是通過接口,把狀态和狀态對應的行為分開,才使得通過狀态模式設計的程式易于擴充和維護。

而上下文主要負責的是公共的狀态驅動,每當狀态發生改變的時候,通常都是回調上下文來執行狀态對應的功能。當然,上下文自身也可以維護狀态的變化,另外,上下文通常還會作為多個狀态處理類之間的資料載體,在多個狀态處理類之間傳遞資料。

2:何時選用狀态模式

       建議在如下情況中,選用狀态模式:

如果一個對象的行為取決于它的狀态,而且它必須在運作時刻根據狀态來改變它的行為。可以使用狀态模式,來把狀态和行為分離開,雖然分離開了,但狀态和行為是有對應關系的,可以在運作期間,通過改變狀态,就能夠調用到該狀态對應的狀态處理對象上去,進而改變對象的行為。

如果一個操作中含有龐大的多分支語句,而且這些分支依賴于該對象的狀态。可以使用狀态模式,把各個分支的處理分散包裝到單獨的對象處理類裡面,這樣,這些分支對應的對象就可以不依賴于其它對象而獨立變化了。

l          狀态模式和政策模式

    這是兩個結構相同,功能各異的模式,具體的在政策模式裡面講過了,這裡就不再贅述了。

l          狀态模式和觀察者模式

    這兩個模式乍一看,功能是很相似的,但是又有差別,可以組合使用。

    這兩個模式都是在狀态發生改變的時候觸發行為,隻不過觀察者模式的行為是固定的,那就是通知所有的觀察者,而狀态模式是根據狀态來選擇不同的處理。

    從表面來看,兩個模式功能相似,觀察者模式中的被觀察對象就好比狀态模式中的上下文,觀察者模式中當被觀察對象的狀态發生改變的時候,觸發的通知所有觀察者的方法;就好比是狀态模式中,根據狀态的變化,選擇對應的狀态處理。

    但實際這兩個模式是不同的,觀察者模式的目的是在被觀察者的狀态發生改變的時候,觸發觀察者關聯,具體如何處理觀察者模式不管;而狀态模式的主要目的在于根據狀态來分離和選擇行為,當狀态發生改變的時候,動态改變行為。

    這兩個模式是可以組合使用的,比如在觀察者模式的觀察者部分,當被觀察對象的狀态發生了改變,觸發通知了所有的觀察者過後,觀察者該怎麼處理呢?這個時候就可以使用狀态模式,根據通知過來的狀态選擇相應的處理。

l          狀态模式和單例模式

    這兩個模式可以組合使用,可以把狀态模式中的狀态處理類實作成單例。

l          狀态模式和享元模式

    這兩個模式可以組合使用。

    由于狀态模式把狀态對應的行為分散到多個狀态對象中,會造成很多細粒度的狀态對象,可以把這些狀态處理對象通過享元模式來共享,進而節省資源。

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

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

繼續閱讀