1 場景問題#
1.1 實現(xiàn)在線投票##
考慮一個在線投票的應用咸灿,要實現(xiàn)控制同一個用戶只能投一票,如果一個用戶反復投票刊驴,而且投票次數超過5次疮薇,則判定為惡意刷票胸墙,要取消該用戶投票的資格,當然同時也要取消他所投的票惦辛。如果一個用戶的投票次數超過8次劳秋,將進入黑名單,禁止再登錄和使用系統(tǒng)胖齐。
該怎么實現(xiàn)這樣的功能呢玻淑?
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){
//黑名單
//記入黑名單中抒蚜,禁止登錄系統(tǒng)了
System.out.println("進入黑名單掘鄙,將禁止登錄和使用本系統(tǒng)");
}
}
}
寫個客戶端來測試看看,是否能滿足功能要求嗡髓,示例代碼如下:
public class Client {
public static void main(String[] args) {
VoteManager vm = new VoteManager();
for(int i=0;i<8;i++){
vm.vote("u1", "A");
}
}
}
運行結果如下:
恭喜你投票成功
請不要重復投票
請不要重復投票
請不要重復投票
你有惡意刷票行為,取消投票資格
你有惡意刷票行為收津,取消投票資格
你有惡意刷票行為饿这,取消投票資格
進入黑名單,將禁止登錄和使用本系統(tǒng)
1.3 有何問題##
看起來很簡單撞秋,是不是长捧?幸虧這里只是示意,否則吻贿,你想想串结,在vote()方法中那么多判斷,還有每個判斷對應的功能處理都放在一起舅列,是不是有點太雜亂了肌割,那簡直就是個大雜燴,如果把每個功能都完整的實現(xiàn)出來帐要,那vote()方法會很長的把敞。
一個問題是:如果現(xiàn)在要修改某種投票情況所對應的具體功能處理,那就需要在那個大雜燴里面榨惠,找到相應的代碼塊奋早,然后進行改動
。
另外一個問題是:如果要添加新的功能赠橙,比如投票超過8次但不足10次的耽装,給個機會,只是禁止登錄和使用系統(tǒng)3天期揪,如果再犯掉奄,才永久封掉賬號,該怎么辦呢横侦?那就需要改動投票管理的源代碼挥萌,在上面的if-else結構中再添加一個else if塊進行處理
。
不管哪一種情況枉侧,都是在一大堆的控制代碼里面找出需要的部分引瀑,然后進行修改,這從來都不是好方法榨馁,那么該如何實現(xiàn)才能做到
:既能夠很容易的給vote()方法添加新的功能憨栽,又能夠很方便的修改已有的功能處理呢?
2 解決方案#
2.1 狀態(tài)模式來解決##
用來解決上述問題的一個合理的解決方案就是狀態(tài)模式。那么什么是狀態(tài)模式呢屑柔?
- 狀態(tài)模式定義
- 應用狀態(tài)模式來解決的思路
仔細分析上面的問題屡萤,會發(fā)現(xiàn),那幾種用戶投票的類型掸宛,就相當于是描述了人員的幾種投票狀態(tài)死陆,而各個狀態(tài)和對應的功能處理具有很強的對應性,有點類似于“一個蘿卜一個坑”唧瘾,各個狀態(tài)下的處理基本上都是不一樣的措译,也不存在可以相互替換的可能
。
為了解決上面提出的問題饰序,很自然的一個設計就是首先把狀態(tài)和狀態(tài)對應的行為從原來的大雜燴代碼中分離出來领虹,把每個狀態(tài)所對應的功能處理封裝在一個獨立的類里面
,這樣選擇不同處理的時候求豫,其實就是在選擇不同的狀態(tài)處理類塌衰。
然后為了統(tǒng)一操作這些不同的狀態(tài)類,定義一個狀態(tài)接口來約束它們蝠嘉,這樣外部就可以面向這個統(tǒng)一的狀態(tài)接口編程最疆,而無需關心具體的狀態(tài)類實現(xiàn)了
。
這樣一來是晨,要修改某種投票情況所對應的具體功能處理六荒,那就是直接修改或者擴展某個狀態(tài)處理類的功能就可以了
犁嗅。而要添加新的功能就更簡單乳蓄,直接添加新的狀態(tài)處理類就可以了缩功,當然在使用Context的時候,需要設置使用這個新的狀態(tài)類的實例箫章。
2.2 模式結構和說明##
狀態(tài)模式的結構如圖所示:
Context:環(huán)境烙荷,也稱上下文,通常用來定義客戶感興趣的接口檬寂,同時維護一個來具體處理當前狀態(tài)的實例對象终抽。
State:狀態(tài)接口,用來封裝與上下文的一個特定狀態(tài)所對應的行為桶至。
ConcreteState:具體實現(xiàn)狀態(tài)處理的類昼伴,每個類實現(xiàn)一個跟上下文相關的狀態(tài)的具體處理。
2.3 狀態(tài)模式示例代碼##
- 首先來看狀態(tài)接口镣屹,示例代碼如下:
/**
* 封裝與Context的一個特定狀態(tài)相關的行為
*/
public interface State {
/**
* 狀態(tài)對應的處理
* @param sampleParameter 示例參數圃郊,說明可以傳入參數,具體傳入
* 什么樣的參數女蜈,傳入幾個參數持舆,由具體應用來具體分析
*/
public void handle(String sampleParameter);
}
- 再來看看具體的狀態(tài)實現(xiàn)色瘩,目前具體的實現(xiàn)ConcreteStateA和ConcreteStateB示范的是一樣的,只是名稱不同逸寓,示例代碼如下:
/**
* 實現(xiàn)一個與Context的一個特定狀態(tài)相關的行為
*/
public class ConcreteStateA implements State {
public void handle(String sampleParameter) {
//實現(xiàn)具體的處理
}
}
/**
* 實現(xiàn)一個與Context的一個特定狀態(tài)相關的行為
*/
public class ConcreteStateB implements State {
public void handle(String sampleParameter) {
//實現(xiàn)具體的處理
}
}
- 再來看看上下文的具體實現(xiàn)居兆,上下文通常用來定義客戶感興趣的接口,同時維護一個具體的處理當前狀態(tài)的實例對象竹伸。示例代碼如下:
/**
* 定義客戶感興趣的接口泥栖,通常會維護一個State類型的對象實例
*/
public class Context {
/**
* 持有一個State類型的對象實例
*/
private State state;
/**
* 設置實現(xiàn)State的對象的實例
* @param state 實現(xiàn)State的對象的實例
*/
public void setState(State state) {
this.state = state;
}
/**
* 用戶感興趣的接口方法
* @param sampleParameter 示意參數
*/
public void request(String sampleParameter) {
//在處理中,會轉調state來處理
state.handle(sampleParameter);
}
}
2.4 使用狀態(tài)模式重寫示例##
看完了上面的狀態(tài)模式的知識佩伤,有些朋友躍躍欲試聊倔,打算使用狀態(tài)模式來重寫前面的示例,要使用狀態(tài)模式生巡,首先就需要把投票過程的各種狀態(tài)定義出來,然后把這些狀態(tài)對應的處理從原來大雜燴的實現(xiàn)中分離出來见妒,形成獨立的狀態(tài)處理對象
孤荣。而原來的投票管理的對象就相當于Context了
。
把狀態(tài)對應的行為分離出去過后须揣,怎么調用呢盐股?
按照狀態(tài)模式的示例,是在Context中耻卡,處理客戶請求的時候疯汁,轉調相應的狀態(tài)對應的具體的狀態(tài)處理類來進行處理。
那就引出下一個問題:那么這些狀態(tài)怎么變化呢卵酪?
看原來的實現(xiàn)幌蚊,就是在投票方法里面,根據投票的次數進行判斷溃卡,并維護投票類型的變化溢豆。那好,也依葫蘆畫瓢瘸羡,就在投票方法里面來維護狀態(tài)變化漩仙。
這個時候的程序結構如圖所示:
- 先來看狀態(tài)接口的代碼實現(xiàn),示例代碼如下:
/**
* 封裝一個投票狀態(tài)相關的行為
*/
public interface VoteState {
/**
* 處理狀態(tài)對應的行為
* @param user 投票人
* @param voteItem 投票項
* @param voteManager 投票上下文犹赖,用來在實現(xiàn)狀態(tài)對應的功能處理的時候队他,
* 可以回調上下文的數據
*/
public void vote(String user,String voteItem, VoteManager voteManager);
}
- 定義了狀態(tài)接口,那就該來看看如何實現(xiàn)各個狀態(tài)對應的處理了峻村,現(xiàn)在的實現(xiàn)很簡單麸折,就是把原來的實現(xiàn)從投票管理類里面分離出來就可以了。先看正常投票狀態(tài)對應的處理雀哨,示例代碼如下:
public class NormalVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//正常投票
//記錄到投票記錄中
voteManager.getMapVote().put(user, voteItem);
System.out.println("恭喜你投票成功");
}
}
接下來看看重復投票狀態(tài)對應的處理磕谅,示例代碼如下:
public class RepeatVoteState implements VoteState {
public void vote(String user, String voteItem, VoteManager voteManager) {
//重復投票
//暫時不做處理
System.out.println("請不要重復投票");
}
}
接下來看看惡意投票狀態(tài)對應的處理私爷,示例代碼如下:
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("你有惡意刷票行為膊夹,取消投票資格");
}
}
接下來看看黑名單狀態(tài)對應的處理衬浑,示例代碼如下:
public class BlackVoteState implements VoteState{
public void vote(String user, String voteItem,VoteManager voteManager) {
//黑名單
//記入黑名單中,禁止登錄系統(tǒng)了
System.out.println("進入黑名單放刨,將禁止登錄和使用本系統(tǒng)");
}
}
- 定義好了狀態(tài)接口和狀態(tài)實現(xiàn)工秩,看看現(xiàn)在的投票管理,相當于狀態(tài)模式中的上下文进统,相對而言助币,它的改變如下:
添加持有狀態(tài)處理對象;
添加能獲取記錄用戶投票結果的Map的方法螟碎,各個狀態(tài)處理對象眉菱,在進行狀態(tài)對應的處理的時候,需要獲取上下文中的記錄用戶投票結果的Map數據掉分;
在vote()方法實現(xiàn)里面俭缓,原來判斷投票類型就變成了判斷投票的狀態(tài),而原來每種投票類型對應的處理酥郭,現(xiàn)在已經封裝到對應的狀態(tài)對象里面去了华坦,因此直接轉調對應的狀態(tài)對象的方法即可。示例代碼如下:
/**
* 投票管理
*/
public class VoteManager {
/**
* 持有狀態(tài)處理對象
*/
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:判斷該用戶投票的類型,就相當于是判斷對應的狀態(tài)
//到底是正常投票椿息、重復投票歹袁、惡意投票還是上黑名單的狀態(tài)
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();
}
//然后轉調狀態(tài)對象來進行相應的操作
state.vote(user, voteItem, this);
}
}
- 該寫個客戶端來測試一下了,經過這么修改過后撵颊,好用嗎宇攻?試試看就知道了〕拢客戶端沒有任何的改變逞刷,跟前面實現(xiàn)的一樣,示例代碼如下:
public class Client {
public static void main(String[] args) {
VoteManager vm = new VoteManager();
for(int i=0;i<8;i++){
vm.vote("u1", "A");
}
}
}
運行一下試試吧妻熊,結果應該是跟前面一樣的夸浅,也就是說都是實現(xiàn)一樣的功能,只是采用了狀態(tài)模式來實現(xiàn)扔役。測試結果如下:
恭喜你投票成功
請不要重復投票
請不要重復投票
請不要重復投票
你有惡意刷票行為帆喇,取消投票資格
你有惡意刷票行為,取消投票資格
你有惡意刷票行為亿胸,取消投票資格
進入黑名單坯钦,將禁止登錄和使用本系統(tǒng)
從上面的示例可以看出预皇,狀態(tài)的轉換基本上都是內部行為,主要在狀態(tài)模式內部來維護
婉刀。比如對于投票的人員吟温,任何時候他的操作都是投票,但是投票管理對象的處理卻不一定一樣突颊,會根據投票的次數來判斷狀態(tài)鲁豪,然后根據狀態(tài)去選擇不同的處理
。
3 模式講解#
3.1 認識狀態(tài)模式##
- 狀態(tài)和行為
所謂對象的狀態(tài)律秃,通常指的就是對象實例的屬性的值爬橡;而行為指的就是對象的功能,再具體點說棒动,行為多半可以對應到方法上糙申。
狀態(tài)模式的功能就是分離狀態(tài)的行為,通過維護狀態(tài)的變化船惨,來調用不同的狀態(tài)對應的不同的功能郭宝。
也就是說,狀態(tài)和行為是相關聯(lián)的掷漱,它們的關系可以描述為:狀態(tài)決定行為
。
由于狀態(tài)是在運行期被改變的榄檬,因此行為也會在運行期卜范,根據狀態(tài)的改變而改變,看起來鹿榜,同一個對象海雪,在不同的運行時刻,行為是不一樣的舱殿,就像是類被修改了一樣奥裸。
- 行為的平行性
注意是平行性而不是平等性
。所謂平行性指的是各個狀態(tài)的行為所處的層次是一樣的沪袭,相互是獨立的湾宙、沒有關聯(lián)的,是根據不同的狀態(tài)來決定到底走平行線的那一條冈绊,行為是不同的侠鳄,當然對應的實現(xiàn)也是不同的,相互之間是不可替換的
死宣。如圖所示:
而平等性強調的是可替換性
伟恶,大家是同一行為的不同描述或實現(xiàn),因此在同一個行為發(fā)生的時候毅该,可以根據條件來挑選任意一個實現(xiàn)來進行相應的處理博秫。如圖所示:
大家可能會發(fā)現(xiàn)狀態(tài)模式的結構和策略模式的結構完全一樣潦牛,但是,它們的目的挡育、實現(xiàn)巴碗、本質都是完全不一樣的
。這個行為之間的特性也是狀態(tài)模式和策略模式一個很重要的區(qū)別静盅,狀態(tài)模式的行為是平行性的良价,不可相互替換的;而策略模式的行為是平等性的蒿叠,是可以相互替換的
明垢。
- 上下文和狀態(tài)處理對象
在狀態(tài)模式中,上下文是持有狀態(tài)的對象市咽,但是上下文自身并不處理跟狀態(tài)相關的行為
痊银,而是把處理狀態(tài)的功能委托給了狀態(tài)對應的狀態(tài)處理類來處理。
在具體的狀態(tài)處理類里面經常需要獲取上下文自身的數據施绎,甚至在必要的時候會回調上下文的方法
溯革,因此,通常將上下文自身當作一個參數傳遞給具體的狀態(tài)處理類谷醉。
客戶端一般只和上下文交互致稀,客戶端可以用狀態(tài)對象來配置一個上下文,一旦配置完畢俱尼,就不再需要和狀態(tài)對象打交道了抖单,客戶端通常不負責運行期間狀態(tài)的維護,也不負責決定到底后續(xù)使用哪一個具體的狀態(tài)處理對象遇八。
- 不完美的OCP體驗
好了矛绘,已經使用狀態(tài)模式來重寫了前面的示例,那么到底能不能解決前面提出的問題呢刃永?也就是修改和擴展方不方便呢货矮?一起來看一下。
先看修改已有的功能吧斯够,由于現(xiàn)在每個狀態(tài)對應的處理已經封裝到對應的狀態(tài)類里面了囚玫,要修改已有的某個狀態(tài)的功能,直接擴展某個類進行修改就好了雳刺,對其它的程序沒有影響劫灶。比如:現(xiàn)在要修改正常投票狀態(tài)對應的功能,對于正常投票的用戶給予積分獎勵掖桦,那么只需要擴展正常投票狀態(tài)對應的類本昏,然后進行修改,示例代碼如下:
public class NormalVoteState2 extends NormalVoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//先調用已有的功能
super.vote(user, voteItem, voteManager);
//給予積分獎勵枪汪,示意一下
System.out.println("獎勵積分10分");
}
}
一切良好涌穆,對吧怔昨,可是怎么讓VoteManager能使用這個新的實現(xiàn)類呢?按照目前的實現(xiàn)宿稀,沒有辦法趁舀,只好去修改VoteManager的vote()方法中對狀態(tài)的維護代碼了
,把使用NormalVoteState的地方換成使用NormalVoteState2祝沸。
再看看如何添加新的功能矮烹,比如投票超過8次但不足10次的,給個機會罩锐,只是禁止登錄和使用系統(tǒng)3天奉狈,如果再犯,才進入黑名單涩惑。要實現(xiàn)這個功能仁期,先要對原來的投票超過8次進入黑名單的功能進行修改,修改成投票超過10次才進入黑名單竭恬;然后新加入一個功能跛蛋,實現(xiàn)超過8次但不足10次的,只是禁止登錄和使用系統(tǒng)3天的功能痊硕。把這個新功能實現(xiàn)出來赊级,示例代碼如下:
public class BlackWarnVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//待進黑名單警告狀態(tài)
System.out.println("禁止登錄和使用系統(tǒng)3天");
}
}
實現(xiàn)好了這個類,該怎樣加入到已有的系統(tǒng)呢岔绸?
同樣需要去修改上下文的vote()方法中對于狀態(tài)判斷和維護的代碼此衅,示例代碼如下:
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();
}
好像也實現(xiàn)了功能是不是,而且改動起來確實也變得簡單點了亭螟,但是仔細想想,是不是沒有完全遵循OCP原則骑歹?結論是很顯然的预烙,明顯沒有完全遵循OCP原則。
這里要說明一點道媚,設計原則是大家在設計和開發(fā)中盡量去遵守的扁掸,但不是一定要遵守,尤其是完全遵守最域,在實際開發(fā)中谴分,完全遵守那些設計原則幾乎是不可能完成的任務
。
就像狀態(tài)模式的實際實現(xiàn)中镀脂,由于狀態(tài)的維護和轉換在狀態(tài)模式結構里面牺蹄,不管你是擴展了狀態(tài)實現(xiàn)類,還是新添加了狀態(tài)實現(xiàn)類薄翅,都需要修改狀態(tài)維護和轉換的地方沙兰,以使用新的實現(xiàn)氓奈。
雖然可以有好幾個地方來維護狀態(tài)的變化,這個后面會講到鼎天,但是都是在狀態(tài)模式結構里面的舀奶,所以都有這個問題,算是不完美的OCP體驗吧斋射。
- 創(chuàng)建和銷毀狀態(tài)對象
在應用狀態(tài)模式的時候育勺,有一個常見的考慮,那就是:究竟何時創(chuàng)建和銷毀狀態(tài)對象罗岖。常見的有幾個選擇:
一個是當需要使用狀態(tài)對象的時候創(chuàng)建涧至,使用完后就銷毀它們;
另一個是提前創(chuàng)建它們并且始終不銷毀呀闻;
還有一種是采用延遲加載和緩存合用的方式化借,就是當第一次需要使用狀態(tài)對象的時候創(chuàng)建,使用完后并不銷毀對象捡多,而是把這個對象緩存起來蓖康,等待下一次使用,而且在合適的時候垒手,會由緩存框架銷毀狀態(tài)對象
怎么選擇呢蒜焊?下面給出選擇建議:
如果要進入的狀態(tài)在運行時是不可知的,而且上下文是比較穩(wěn)定的科贬,不會經常改變狀態(tài)泳梆,而且使用也不頻繁,這個時候建議選第一種方案榜掌。
如果狀態(tài)改變很頻繁优妙,也就是需要頻繁的創(chuàng)建狀態(tài)對象,而且狀態(tài)對象還存儲著大量的信息數據憎账,這種情況建議選第二種方案套硼。
如果無法確定狀態(tài)改變是否頻繁,而且有些狀態(tài)對象的狀態(tài)數據量大胞皱,有些比較小邪意,一切都是未知的,建議選第三種方案反砌。
事實上雾鬼,在實際工程開發(fā)中,第三種方案是首選宴树,因為它兼顧了前面兩種方案的優(yōu)點策菜,而又避免了它們的缺點,幾乎能適應各種情況的需要
。只是這個方案在實現(xiàn)的時候做入,要實現(xiàn)一個合理的緩存框架冒晰,而且要考慮多線程并發(fā)的問題,因為需要由緩存框架來在合適的時候銷毀狀態(tài)對象竟块,因此實現(xiàn)上難度稍高點壶运。另外在實現(xiàn)中還可以考慮結合享元模式,通過享元模式來共享狀態(tài)對象
浪秘。
- 狀態(tài)模式的調用順序示意圖
狀態(tài)模式在實現(xiàn)上蒋情,對于狀態(tài)的維護有不同的實現(xiàn)方式,前面的示例中耸携,采用的是在Context中進行狀態(tài)的維護和轉換
棵癣,這里就先畫出這種方式的調用順序示意圖,其它的方式在后面講到了再畫夺衍。
在Context進行狀態(tài)維護和轉換的調用順序示意圖如圖所示:
3.2 狀態(tài)維護和轉換控制##
所謂狀態(tài)的維護狈谊,指的就是維護狀態(tài)的數據,就是給狀態(tài)設置不同的狀態(tài)值沟沙;而狀態(tài)的轉換河劝,指的就是根據狀態(tài)的變化來選擇不同的狀態(tài)處理對象
。在狀態(tài)模式中矛紫,通常有兩個地方可以進行狀態(tài)的維護和轉換控制牲迫。
一個就是在上下文當中参歹,因為狀態(tài)本身通常被實現(xiàn)為上下文對象的狀態(tài)辆雾,因此可以在上下文里面進行狀態(tài)維護趋距,當然也就可以控制狀態(tài)的轉換了。前面投票的示例就是采用的這種方式喳篇。
另外一個地方就是在狀態(tài)的處理類里面敞临,當每個狀態(tài)處理對象處理完自身狀態(tài)所對應的功能后,可以根據需要指定后繼的狀態(tài)麸澜,以便讓應用能正確處理后續(xù)的請求哟绊。
先看看示例,為了對比學習痰憎,就來看看如何把前面投票的例子修改成:在狀態(tài)處理類里面進行后續(xù)狀態(tài)的維護和轉換
。
- 同樣先來看投票狀態(tài)的接口攀涵,沒有變化铣耘,示例代碼如下:
/**
* 封裝一個投票狀態(tài)相關的行為
*/
public interface VoteState {
/**
* 處理狀態(tài)對應的行為
* @param user 投票人
* @param voteItem 投票項
* @param voteManager 投票上下文,用來在實現(xiàn)狀態(tài)對應的功能處理的時候以故,
* 可以回調上下文的數據
*/
public void vote(String user,String voteItem,VoteManager voteManager);
}
- 對于各個具體的狀態(tài)實現(xiàn)對象蜗细,主要的變化在于:在處理完自己狀態(tài)對應的功能后,還需要維護和轉換狀態(tài)對象。
一個一個來看吧炉媒,先看看正常投票的狀態(tài)處理對象踪区,示例代碼如下:
public class NormalVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//正常投票,記錄到投票記錄中
voteManager.getMapVote().put(user, voteItem);
System.out.println("恭喜你投票成功");
//正常投票完成吊骤,維護下一個狀態(tài)缎岗,同一個人再投票就重復了
voteManager.getMapState().put(user,new RepeatVoteState());
}
}
接下來看看重復投票狀態(tài)對應的處理對象,示例代碼如下:
public class RepeatVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//重復投票白粉,暫時不做處理
System.out.println("請不要重復投票");
//重復投票完成传泊,維護下一個狀態(tài),重復投票到5次鸭巴,就算惡意投票了
//注意這里是判斷大于等于4眷细,因為這里設置的是下一個狀態(tài)
//下一個操作次數就是5了,就應該算是惡意投票了
if(voteManager.getMapVoteCount().get(user) >= 4){
voteManager.getMapState().put(user,new SpiteVoteState());
}
}
}
接下來看看惡意投票狀態(tài)對應的處理對象鹃祖,示例代碼如下:
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("你有惡意刷票行為恬口,取消投票資格");
//惡意投票完成校读,維護下一個狀態(tài),投票到8次楷兽,就進黑名單了
//注意這里是判斷大于等于7地熄,因為這里設置的是下一個狀態(tài)
//下一個操作次數就是8了,就應該算是進黑名單了
if(voteManager.getMapVoteCount().get(user) >= 7){
voteManager.getMapState().put(user,new BlackVoteState());
}
}
}
接下來看看黑名單狀態(tài)對應的處理對象芯杀,沒什么變化端考,示例代碼如下:
public class BlackVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//黑名單,記入黑名單中揭厚,禁止登錄系統(tǒng)了
System.out.println("進入黑名單却特,將禁止登錄和使用本系統(tǒng)");
}
}
- 該來看看現(xiàn)在的投票管理類該如何實現(xiàn)了,跟在上下文中維護和轉換狀態(tài)相比筛圆,大致有如下的變化:
需要按照每個用戶來記錄他們對應的投票狀態(tài)裂明,不同的用戶,對應的投票狀態(tài)是不同的太援,因此使用一個Map來記錄闽晦,而不再是原來的一個單一的投票狀態(tài)對象。
可能有些朋友會問提岔,那為什么前面的實現(xiàn)可以呢仙蛉?那是因為投票狀態(tài)是由投票管理對象集中控制的,不同的人員在進入投票方法的時候碱蒙,是重新判斷該人員具體的狀態(tài)對象的荠瘪,而現(xiàn)在是要把狀態(tài)維護分散到各個狀態(tài)類里面去夯巷,因此需要記錄各個狀態(tài)類判斷過后的結果。
需要把記錄投票狀態(tài)的數據哀墓,還有記錄投票次數的數據趁餐,提供相應的getter方法,各個狀態(tài)在處理的時候需要通過這些方法來訪問數據篮绰。
原來在vote()方法里面進行的狀態(tài)控制和轉換去掉后雷,變成直接根據人員來從狀態(tài)記錄的Map中獲取對應的狀態(tài)對象了。
看看實現(xiàn)代碼吧阶牍,示例代碼如下:
public class VoteManager {
/**
* 記錄當前每個用戶對應的狀態(tài)處理對象喷面,每個用戶當前的狀態(tài)是不同的
* Map<String,VoteState>對應Map<用戶名稱,當前對應的狀態(tài)處理對象>
*/
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;
}
/**
* 獲取記錄每個用戶對應的狀態(tài)處理對象的Map
* @return 記錄每個用戶對應的狀態(tài)處理對象的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:獲取該用戶的投票狀態(tài)
VoteState state = mapState.get(user);
//如果沒有投票狀態(tài)惧辈,說明還沒有投過票,就初始化一個正常投票狀態(tài)
if(state==null){
state = new NormalVoteState();
}
//然后轉調狀態(tài)對象來進行相應的操作
state.vote(user, voteItem, this);
}
}
- 實現(xiàn)得差不多了磕瓷,該來測試了盒齿,客戶端沒有變化,去運行一下困食,看看效果边翁,看看兩種維護狀態(tài)變化的方式實現(xiàn)的結果一樣嗎?答案應該是一樣的硕盹。
那么到底如何選擇這兩種方式呢符匾?
一般情況下,如果狀態(tài)轉換的規(guī)則是一定的瘩例,一般不需要進行什么擴展規(guī)則啊胶,那么就適合在上下文中統(tǒng)一進行狀態(tài)的維護
。
如果狀態(tài)的轉換取決于前一個狀態(tài)動態(tài)處理的結果垛贤,或者是依賴于外部數據
焰坪,為了增強靈活性,這種情況下聘惦,一般是在狀態(tài)處理類里面進行狀態(tài)的維護
某饰。
- 采用讓狀態(tài)對象來維護和轉換狀態(tài)的調用順序示意圖如圖所示:
- 再來看看這種實現(xiàn)方式下,如何修改已有的功能善绎,或者是添加新的狀態(tài)處理黔漂。
要修改已有的功能,同樣是找到對應的狀態(tài)處理對象禀酱,要么直接修改炬守,要么擴展,前面已經示例過了比勉,就不再贅述了。
對于添加新的狀態(tài)處理的功能,這種實現(xiàn)方式會比較簡單浩聋。先直接添加新的狀態(tài)處理的類观蜗,然后去找到需要轉換到這個新狀態(tài)的狀態(tài)處理類,修改那個處理類衣洁,讓其轉換到這個新狀態(tài)就可以了
墓捻。
比如還是來實現(xiàn)那個:投票超過8次但不足10次的,給個機會坊夫,只是禁止登錄和使用系統(tǒng)3天砖第,如果再犯,才進入黑名單的功能环凿。按照現(xiàn)在的方式梧兼,示例代碼如下:
public class BlackWarnVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//待進黑名單警告狀態(tài)
System.out.println("禁止登錄和使用系統(tǒng)3天");
//待進黑名單警告處理完成,維護下一個狀態(tài)智听,投票到10次羽杰,就進黑名單了
//注意這里是判斷大于等于9,因為這里設置的是下一個狀態(tài)
//下一個操作次數就是10了到推,就應該算是進黑名單了
if(voteManager.getMapVoteCount().get(user) >= 9){
voteManager.getMapState().put(user, new BlackVoteState());
}
}
}
那么如何加入系統(tǒng)呢考赛?
不再是去修改VoteManger了,而是找到應該轉換到這個新狀態(tài)的那個狀態(tài)莉测,修改它的狀態(tài)維護和轉換
颜骤。應該是在惡意投票處理里面,讓它轉換到這個新的狀態(tài)捣卤,也就是把惡意投
票處理里面的下面這句話:
voteManager.getMapState().put(user, new BlackVoteState());
替換成:
voteManager.getMapState().put(user, new BlackWarnVoteState());
這樣就自然的把現(xiàn)在新的狀態(tài)處理添加到了已有的應用中忍抽。
3.3 使用數據庫來維護狀態(tài)##
在實際開發(fā)中,還有一個方式來維護狀態(tài)腌零,那就是使用數據庫
梯找,在數據庫中存儲下一個狀態(tài)的識別數據,也就是說益涧,維護下一個狀態(tài)锈锤,演化成了維護下一個狀態(tài)的識別數據,比如狀態(tài)編碼闲询。
這樣在程序中久免,通過查詢數據庫中的數據來得到狀態(tài)編碼,然后再根據狀態(tài)編碼來創(chuàng)建出相應的狀態(tài)對象扭弧,然后再委托相應的狀態(tài)對象進行功能處理阎姥。
還是用前面投票的示例來說明,如果使用數據庫來維護狀態(tài)的話鸽捻,大致如何實現(xiàn)呼巴。
- 首先泽腮,就是每個具體的狀態(tài)處理類中,原本在處理完成后衣赶,要判斷下一個狀態(tài)是什么诊赊,然后創(chuàng)建下一個狀態(tài)對象,并設置回到上下文中府瞄。
如果使用數據庫的方式碧磅,那就不用創(chuàng)建下一個狀態(tài)對象,也不用設置回到上下文中了遵馆,而是把下一個狀態(tài)對應的編碼記入數據庫中鲸郊,這樣就可以了。還是示意一個货邓,看看重復投票狀態(tài)下的實現(xiàn)吧秆撮,示例代碼如下:
public class RepeatVoteState implements VoteState{
public void vote(String user, String voteItem, VoteManager voteManager) {
//重復投票,暫時不做處理
System.out.println("請不要重復投票");
//重復投票完成逻恐,維護下一個狀態(tài)像吻,重復投票到5次,就算惡意投票了
if(voteManager.getMapVoteCount().get(user) >= 4){
voteManager.getMapState().put(user,new SpiteVoteState());
//直接把下一個狀態(tài)的編碼記錄入數據庫就好了
}
}
}
這里只是示意一下复隆,并不真的去寫和數據庫操作的代碼拨匆。其它的狀態(tài)實現(xiàn)類,也做同樣類似的修改挽拂,就不去贅述了惭每。
- 在Context里面,也就是投票管理對象里面亏栈,就不需要那個記錄所有用戶狀態(tài)的Map了台腥,直接從數據庫中獲取該用戶當前對應的狀態(tài)編碼,然后根據狀態(tài)編碼來創(chuàng)建出狀態(tài)對象來绒北。原有的示例代碼如下:
//2:獲取該用戶的投票狀態(tài)
VoteState state = mapState.get(user);
//如果沒有投票狀態(tài)黎侈,說明還沒有投過票,就初始化一個正常投票狀態(tài)
if(state==null){
state = new NormalVoteState();
}
現(xiàn)在被修改成闷游,示例代碼如下:
VoteState state = null;
//2:直接從數據庫獲取該用戶對應的下一個狀態(tài)的狀態(tài)編碼
String stateId = "從數據庫中獲取這個狀態(tài)編碼";
//開始根據狀態(tài)編碼來創(chuàng)建需用的狀態(tài)對象
if(stateId==null || stateId.trim().length()==0){
//如果沒有值峻汉,說明還沒有投過票,就初始化一個正常投票狀態(tài)
state = new NormalVoteState();
}else if("重復投票".equals(stateId)){
state = new RepeatVoteState();
}else if("惡意投票".equals(stateId)){
state = new SpiteVoteState();
}else if("黑名單".equals(stateId)){
state = new BlackVoteState();
}
可能有些朋友會發(fā)現(xiàn)脐往,如果向數據庫里面存儲下一個狀態(tài)對象的狀態(tài)編碼休吠,那么上下文中就不需要再持有狀態(tài)對象了,有點相當于把這個功能放到數據庫中了业簿。有那么點相似性瘤礁,不過要注意,數據庫存儲的只是狀態(tài)編碼梅尤,而不是狀態(tài)對象柜思,獲取到數據庫中的狀態(tài)編碼過后岩调,在程序里面還是需要根據狀態(tài)編碼去真正創(chuàng)建對應的狀態(tài)對象
。
當然赡盘,要想程序更通用一點誊辉,可以通過配置文件來配置狀態(tài)編碼和對應的狀態(tài)處理類,當然也可以直接在數據庫中記錄狀態(tài)編碼和對應的狀態(tài)處理類
亡脑,這樣的話,在上下文中邀跃,先獲取下一個狀態(tài)的狀態(tài)編碼霉咨,然后根據這個狀態(tài)編碼去獲取對應的類,然后可以通過反射來創(chuàng)建對象拍屑,這樣實現(xiàn)就避免了那一長串的if-else途戒,而且以后添加新的狀態(tài)編碼和狀態(tài)處理對象也不用再修改代碼了。示例代碼如下:
VoteState state = null;
//2:直接從數據庫獲取該用戶對應的下一個狀態(tài)的狀態(tài)編碼
String stateId = "從數據庫中獲取這個值";
//開始根據狀態(tài)編碼來創(chuàng)建需用的狀態(tài)對象
//根據狀態(tài)編碼去獲取相應的類
String className = "根據狀態(tài)編碼去獲取相應的類";
//使用反射創(chuàng)建對象實例僵驰,簡單示意一下
Class c = Class.forName(className);
state = (VoteState)c.newInstance();
直接把“轉移”記錄到數據庫中喷斋。還有一種情況是直接把“轉移”記錄到數據庫中,這樣會更靈活蒜茴。所謂轉移星爪,指的就是描述從A狀態(tài)到B狀態(tài)的這么一個轉換變化
。