Android開發設計模式之狀態模式解析。
一、介紹
狀態模式中的行為是由狀態來決定的,不同的狀態下有不同的行為。狀態模式和策略模式的結構幾乎完全一樣,但它們的目的、本質卻完全不一樣。狀態模式的行為是平行的、不可替換的,策略模式的行為是彼此獨立、可相互替換的。用一句話來表述,狀態模式把對象的行為包裝在不同的狀態對象裡,每一個狀態對象都有一個共同的抽象狀態基類。狀態模式的意圖是讓一個對象在其內部狀態改變的時候,其行為也隨之改變。
二、定義
當一個對象的內在狀態改變時允許改變其行為,這個對象看起來像是改變瞭其類。
三、使用場景
(1)一個對象的行為取決於它的狀態,並且它必須在運行時根據狀態改變它的行為。
(2)代碼中包含大量與對象狀態有關的條件語句,例如,一個操作中含有龐大的多分支語句(if-else或switch-case),且這些分支依賴於該對象的狀態。
狀態模式將每一個條件分支放入一個獨立的類中,這使得你可以根據對象自身的情況將對象的狀態作為一個對象,這一對象可以不依賴與其他對象而獨立變化,這樣通過多態去除過多的、重復的if-else等分支語句。
四、狀態模式的UML類圖
UML類圖:
角色介紹:
Context:環境類,定義客戶感興趣的接口,維護一個State子類的實例,這個實例定義瞭對象的當前狀態。
State:抽象狀態類或狀態接口,定義一個或者一組接口,表示該狀態下的行為。
ConcreteStateA、ConcreteStateB:具體狀態類,每一個具體的狀態類實現抽象State中定義的接口,從而達到不同狀態下的不同行為。
五、簡單示例
下面我們就以電視遙控器為例來演示一下狀態模式的實現。我們首先將電視的狀態簡單分為開機狀態和關機狀態,在開機狀態下可以通過遙控器進行頻道切換、調整音量等操作,但是,此時重復按開機鍵是無效的;而在關機狀態下,頻道切換、調整音量、關機都是無效的操作,隻有按開機按鈕時才會生效。也就是說電視的內部狀態決定瞭遙控器的行為。
首先是普通的實現方法:
public class TVController { //開機狀態 private final static int POWER_ON = 1; //關機狀態 private final static int POWER_OFF = 2; //默認狀態 private int mState = POWER_OFF; public void powerOn(){ if(mState ==POWER_OFF){ System.out.println("電視開機瞭"); } mState = POWER_ON; } public void powerOff(){ if(mState ==POWER_ON){ System.out.println("電視關機瞭"); } mState = POWER_OFF; } public void nextChannel(){ if(mState ==POWER_ON){ System.out.println("下一頻道"); }else{ System.out.println("沒有開機"); } } public void prevChannel(){ if(mState ==POWER_ON){ System.out.println("上一頻道"); }else{ System.out.println("沒有開機"); } } public void turnUp(){ if(mState ==POWER_ON){ System.out.println("調高音量"); }else{ System.out.println("沒有開機"); } } public void turnDown(){ if(mState ==POWER_ON){ System.out.println("調低音量"); }else{ System.out.println("沒有開機"); } } }
可以看到,每次執行通過判斷當前狀態來進行操作,部分的代碼重復,假設狀態和功能增加,就會越來越難以維護。這時可以使用狀態模式,如下:
電視的狀態接口:
/** * 電視狀態接口,定義瞭電視的操作函數 * **/ public interface TVState { public void nextChannel(); public void prevChannel(); public void turnUp(); public void turnDown(); }
關機狀態:
/** * * 關機狀態,操作無結果 * * */ public class PowerOffState implements TVState{ @Override public void nextChannel() { } @Override public void prevChannel() { } @Override public void turnUp() { } @Override public void turnDown() { } }
開機狀態:
/** * * 開機狀態,操作有效 * * */ public class PowerOnState implements TVState{ @Override public void nextChannel() { System.out.println("下一頻道"); } @Override public void prevChannel() { System.out.println("上一頻道"); } @Override public void turnUp() { System.out.println("調高音量"); } @Override public void turnDown() { System.out.println("調低音量"); } }
電源操作接口:
/** * 電源操作接口 * * */ public interface PowerController { public void powerOn(); public void powerOff(); }
電視遙控器:
/** * 電視遙控器 * * */ public class TVController implements PowerController{ TVState mTVState; public void setTVState(TVState mTVState){ this.mTVState = mTVState; } @Override public void powerOn() { setTVState(new PowerOnState()); System.out.println("開機瞭"); } @Override public void powerOff() { setTVState(new PowerOffState()); System.out.println("關機瞭"); } public void nextChannel(){ mTVState.nextChannel(); } public void prevChannel(){ mTVState.prevChannel(); } public void turnUp(){ mTVState.turnUp(); } public void turnDown(){ mTVState.turnDown(); } }
調用:
public class Client { public static void main(String[] args) { TVController tvController = new TVController(); //設置開機狀態 tvController.powerOn(); //下一頻道 tvController.nextChannel(); //調高音量 tvController.turnUp(); //關機 tvController.powerOff(); //調低音量,此時不會生效 tvController.turnDown(); } }
輸出結果如下:
開機瞭 下一頻道 調高音量 關機瞭
上述實現中,我們抽象瞭一個TVState接口,該接口中有操作電視的所有函數,該接口有兩個實現類,即開機狀態(PowerOnState)和關機狀態(PowerOffState)。開機狀態下隻有開機功能是無效的,也就是說在已經開機的時候用戶在按開機鍵不會產生任何反應;而在關機狀態下,隻有開機功能是可用的,其他功能都不會生效。同一個操作,如調高音量的turnUp函數,在關機狀態下無效,在開機狀態下就會將電視的音量調高,也就是說電視內部狀態影響瞭電視遙控器的行為。狀態模式將這些行為封裝到狀態類中,在進行操作時將這些功能轉發給狀態對象,不同的狀態有不同的實現,這樣就通過多態的形式去除瞭重復、雜亂的if-else語句,這也正是狀態模式的精髓所在。
六、Android實戰中的使用
1、登錄系統,根據用戶是否登錄,判斷事件的處理方式。
2、Wi-Fi管理,在不同的狀態下,WiFi的掃描請求處理不一。
下面以登錄系統為例講解下狀態模式在實戰中的使用:
在android開發中,我們遇到登錄界面是十分常見的,而狀態設計模式在登錄界面的應用十分廣泛,用戶在登錄狀態下和未登錄狀態下,對邏輯的操作是不一樣的。例如最常見的情況就是在玩新浪微博的時候,用戶在登錄的情況下才能完成評論和轉發微博的操作;而當用戶處於未登錄的情況下要執行轉發和評論微博的操作需要進入登錄界面登錄以後才能執行,所以面對這兩者不同的狀況,利用狀態設計模式來設計這個例子最好不過。
1、狀態基類
前面我們講過狀態設計模式的原理實則是多態,在這裡我們用UserState接口表示此基類,包換轉發操作和評論這兩種狀態,代碼如下:
public interface UserState { /** * 轉發操作 * @param context */ public void forword(Context context); /** * 評論操作 * @param context */ public void commit(Context context); }
2、用戶在登錄和未登錄兩種狀況下的實現類LoginState和LogoutState;代碼如下:
在LoginState.java中,用戶是可以執行轉發和評論操作。
public class LoginState implements UserState{ @Override public void forword(Context context) { Toast.makeText(context, "轉發成功", Toast.LENGTH_SHORT).show(); } @Override public void commit(Context context) { Toast.makeText(context, "評論成功", Toast.LENGTH_SHORT).show(); } }
在LogoutState.java中,用戶在未登錄的情況下不允許執行操作,而是應該跳轉到登錄界面執行登錄以後才可以執行。
public class LogoutState implements UserState{ /** * 跳轉到登錄界面登錄以後才能轉發 */ @Override public void forword(Context context) { gotoLohinActivity(context); } /** * 跳轉到登錄界面登錄以後才能評論 */ @Override public void commit(Context context) { gotoLohinActivity(context); } /** * 界面跳轉操作 * @param context */ private void gotoLohinActivity(Context context){ context.startActivity(new Intent(context,LoginActivity.class)); } }
3、操作角色LoginContext
這裡的LoginContext就是在狀態模式的Context角色,是用戶操作對象和管理對象,LoginContext委托相關的操作給狀態對象,在其中狀態的發生改變,LoginContext的行為也發生改變。LoginContext的代碼如*下:
溫馨提示:
這裡我們用到單例就是為瞭全局隻有一個LoginContext去控制用戶狀態;
public class LoginContext { //用戶狀態默認為未登錄狀態 UserState state = new LogoutState(); private LoginContext(){};//私有構造函數,避免外界可以通過new 獲取對象 //單例模式 public static LoginContext getInstance(){ return SingletonHolder.instance; } /** *靜態代碼塊 */ private static class SingletonHolder{ private static final LoginContext instance = new LoginContext(); } public void setState(UserState state){ this.state = state; } //轉發 public void forward(Context context){ state.forword(context); } //評論 public void commit(Context context){ state.commit(context); } }
4、界面展示
LoginActivity.java,此界面執行登錄操作,登錄成後把 LoginContext.getInstance().setState(new LoginState());設置為登錄狀態,在MainActivity中就執行的是登錄狀態下的操作,即可以轉發可評論;
public class LoginActivity extends Activity implements OnClickListener{ private static final String LOGIN_URL = "https://10.10.200.193:8080/Day01/servlet/LoginServlet"; private EditText et_username; private EditText et_password; private Button btn_login; private String username; private String password; private KJHttp http; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); initView(); initData(); } private void initView() { et_username = (EditText) findViewById(R.id.et_username); et_password = (EditText) findViewById(R.id.et_password); btn_login = (Button) findViewById(R.id.btn_login); btn_login.setOnClickListener(LoginActivity.this); } private void initData() { http = new KJHttp(); } /** * 執行登錄操作 * * @param username2 * @param password2 */ protected void sendLogin(String username2, String password2) { HttpParams params = new HttpParams(); params.put("username", "user1"); params.put("password", "123456"); http.post(LOGIN_URL, params, new HttpCallBack() { @Override public void onSuccess(String t) { if ("200".equals(t)) { //設置為登錄狀態 LoginContext.getInstance().setState(new LoginState()); startActivity(new Intent(LoginActivity.this,MainActivity.class)); finish(); Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show(); } } }); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_login: username = et_username.getEditableText().toString().trim(); password = et_password.getEditableText().toString().trim(); if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { Toast.makeText(LoginActivity.this, "用戶名密碼不能為空", Toast.LENGTH_SHORT).show(); return; } sendLogin(username, password); break; } } }
MainActivity.java,在用戶登錄成功後,點擊轉發和評論執行的是登錄狀態下的操作,而當用戶註銷時,我們把LoginContext的狀態設置為未登錄狀態;LoginContext.getInstance().setState(new LogoutState());此時在點擊轉發和評論操作時就會跳到用戶登錄界面。
public class MainActivity extends Activity { private Button btn_forward; private Button btn_commit; private Button btn_logout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initListener(); } private void initView() { btn_forward = (Button) findViewById(R.id.btn_forward); btn_commit = (Button) findViewById(R.id.btn_commit); btn_logout = (Button) findViewById(R.id.btn_logout); } private void initListener() { //轉發操作 btn_forward.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //調用LoginContext裡面的轉發函數 LoginContext.getInstance().forward(MainActivity.this); } }); //評論操作 btn_commit.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //調用LoginContext裡面的轉發函數 LoginContext.getInstance().commit(MainActivity.this); } }); //註銷操作 btn_logout.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //設置為註銷狀態 LoginContext.getInstance().setState(new LogoutState()); } }); } }
七、總結
狀態模式的關鍵點在於不同的狀態下對於同一行為有不同的響應,這其實就是一個將if-else用多態來實現的一個具體示例。在if-else或者switch-case形式下根據不同的狀態進行判斷,如果是狀態A那麼執行方法A,狀態B執行方法B,但這種實現使得邏輯耦合在一起,易於出錯,通過狀態模式能夠很好的消除這類”醜陋“的邏輯處理,當然並不是任何出現if-else的地方都應該通過狀態模式重構,模式的運用一定要考慮所處的情景以及你要解決的問題,隻有符合特定的場景才建議使用對應的模式。
優點:
將所有與一個特定的狀態相關的行為都放入一個狀態對象中,它提供瞭一個更好的方法來組織與特定狀態相關的代碼,將繁瑣的狀態判斷轉換成結構清晰的狀態類族,在避免代碼膨脹的同時也保證瞭可擴展性與可維護性。
缺點:
狀態模式的使用必然會增加系統類和對象的個數。