【行為型模式二十】狀態(tài)模式-1(State)

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)模式呢屑柔?

  1. 狀態(tài)模式定義
狀態(tài)模式定義
  1. 應用狀態(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)模式的結構如圖所示:

狀態(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)模式示例代碼##

  1. 首先來看狀態(tài)接口镣屹,示例代碼如下:
/**
 * 封裝與Context的一個特定狀態(tài)相關的行為
 */
public interface State {
    /**
     * 狀態(tài)對應的處理
     * @param sampleParameter 示例參數圃郊,說明可以傳入參數,具體傳入
     *             什么樣的參數女蜈,傳入幾個參數持舆,由具體應用來具體分析
     */
    public void handle(String sampleParameter);
}
  1. 再來看看具體的狀態(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)具體的處理
    }
}
  1. 再來看看上下文的具體實現(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)模式的示例程序機構示意圖
  1. 先來看狀態(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);
}
  1. 定義了狀態(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)");
    }
}
  1. 定義好了狀態(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);
    }
}
  1. 該寫個客戶端來測試一下了,經過這么修改過后撵颊,好用嗎宇攻?試試看就知道了〕拢客戶端沒有任何的改變逞刷,跟前面實現(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)模式##

  1. 狀態(tài)和行為

所謂對象的狀態(tài)律秃,通常指的就是對象實例的屬性的值爬橡;而行為指的就是對象的功能,再具體點說棒动,行為多半可以對應到方法上糙申。

狀態(tài)模式的功能就是分離狀態(tài)的行為,通過維護狀態(tài)的變化船惨,來調用不同的狀態(tài)對應的不同的功能郭宝。

也就是說,狀態(tài)和行為是相關聯(lián)的掷漱,它們的關系可以描述為:狀態(tài)決定行為

由于狀態(tài)是在運行期被改變的榄檬,因此行為也會在運行期卜范,根據狀態(tài)的改變而改變,看起來鹿榜,同一個對象海雪,在不同的運行時刻,行為是不一樣的舱殿,就像是類被修改了一樣奥裸。

  1. 行為的平行性

注意是平行性而不是平等性。所謂平行性指的是各個狀態(tài)的行為所處的層次是一樣的沪袭,相互是獨立的湾宙、沒有關聯(lián)的,是根據不同的狀態(tài)來決定到底走平行線的那一條冈绊,行為是不同的侠鳄,當然對應的實現(xiàn)也是不同的,相互之間是不可替換的死宣。如圖所示:

狀態(tài)的平行性示意圖

而平等性強調的是可替換性伟恶,大家是同一行為的不同描述或實現(xiàn),因此在同一個行為發(fā)生的時候毅该,可以根據條件來挑選任意一個實現(xiàn)來進行相應的處理博秫。如圖所示:

平等性的示意圖

大家可能會發(fā)現(xiàn)狀態(tài)模式的結構和策略模式的結構完全一樣潦牛,但是,它們的目的挡育、實現(xiàn)巴碗、本質都是完全不一樣的。這個行為之間的特性也是狀態(tài)模式和策略模式一個很重要的區(qū)別静盅,狀態(tài)模式的行為是平行性的良价,不可相互替換的;而策略模式的行為是平等性的蒿叠,是可以相互替換的明垢。

  1. 上下文和狀態(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)處理對象遇八。

  1. 不完美的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體驗吧斋射。

  1. 創(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)對象浪秘。

  1. 狀態(tài)模式的調用順序示意圖

狀態(tài)模式在實現(xiàn)上蒋情,對于狀態(tài)的維護有不同的實現(xiàn)方式,前面的示例中耸携,采用的是在Context中進行狀態(tài)的維護和轉換棵癣,這里就先畫出這種方式的調用順序示意圖,其它的方式在后面講到了再畫夺衍。

在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)的維護和轉換

  1. 同樣先來看投票狀態(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);
}
  1. 對于各個具體的狀態(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)");
    }
}
  1. 該來看看現(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);
    }
}
  1. 實現(xiàn)得差不多了磕瓷,該來測試了盒齿,客戶端沒有變化,去運行一下困食,看看效果边翁,看看兩種維護狀態(tài)變化的方式實現(xiàn)的結果一樣嗎?答案應該是一樣的硕盹。

那么到底如何選擇這兩種方式呢符匾?

一般情況下,如果狀態(tài)轉換的規(guī)則是一定的瘩例,一般不需要進行什么擴展規(guī)則啊胶,那么就適合在上下文中統(tǒng)一進行狀態(tài)的維護

如果狀態(tài)的轉換取決于前一個狀態(tài)動態(tài)處理的結果垛贤,或者是依賴于外部數據焰坪,為了增強靈活性,這種情況下聘惦,一般是在狀態(tài)處理類里面進行狀態(tài)的維護某饰。

  1. 采用讓狀態(tài)對象來維護和轉換狀態(tài)的調用順序示意圖如圖所示:
狀態(tài)對象來維護和轉換狀態(tài)的調用順序示意圖
  1. 再來看看這種實現(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)呼巴。

  1. 首先泽腮,就是每個具體的狀態(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)類,也做同樣類似的修改挽拂,就不去贅述了惭每。

  1. 在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)的這么一個轉換變化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末粉私,一起剝皮案震驚了整個濱河市顽腾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诺核,老刑警劉巖抄肖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窖杀,居然都是意外死亡漓摩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門入客,熙熙樓的掌柜王于貴愁眉苦臉地迎上來管毙,“玉大人,你說我怎么就攤上這事痊项」纾” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵鞍泉,是天一觀的道長皱埠。 經常有香客問我,道長咖驮,這世上最難降的妖魔是什么边器? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任训枢,我火速辦了婚禮,結果婚禮上忘巧,老公的妹妹穿的比我還像新娘恒界。我一直安慰自己,他們只是感情好砚嘴,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布十酣。 她就那樣靜靜地躺著,像睡著了一般际长。 火紅的嫁衣襯著肌膚如雪耸采。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天工育,我揣著相機與錄音虾宇,去河邊找鬼。 笑死如绸,一個胖子當著我的面吹牛嘱朽,可吹牛的內容都是我干的。 我是一名探鬼主播怔接,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼搪泳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扼脐?” 一聲冷哼從身側響起森书,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谎势,沒想到半個月后凛膏,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡脏榆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年猖毫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片须喂。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡吁断,死狀恐怖,靈堂內的尸體忽然破棺而出坞生,到底是詐尸還是另有隱情仔役,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布是己,位于F島的核電站又兵,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜沛厨,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一宙地、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逆皮,春花似錦宅粥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至剿牺,卻和暖如春风纠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背牢贸。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镐捧,地道東北人潜索。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像懂酱,于是被迫代替她去往敵國和親竹习。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容