溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

發(fā)布時(shí)間:2020-08-11 08:32:18 來(lái)源:ITPUB博客 閱讀:170 作者:技術(shù)瑣話(huà) 欄目:軟件技術(shù)

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

一、需求

針對(duì)某通信產(chǎn)品,我們需要開(kāi)發(fā)一個(gè)版本升級(jí)管理系統(tǒng)。該系統(tǒng)通過(guò)Java開(kāi)發(fā)后臺(tái)管理,由Telnet發(fā)起向前端基站設(shè)備的命令,以獲取基站設(shè)備的版本信息,并在后臺(tái)比較與當(dāng)前最新版本的差異,以確定執(zhí)行什么樣的命令對(duì)基站設(shè)備的軟件文件進(jìn)行操作?;驹O(shè)備分為兩種:

  • 主控板(Master Board)

  • 受控板(Slave Board)

基站設(shè)備允許執(zhí)行的命令包括transfer、active、inactive等。這些命令不僅受到設(shè)備類(lèi)型的限制,還要受制于該設(shè)備究竟運(yùn)行在什么樣的終端。類(lèi)型分為:

  • Shell

  • UShell

對(duì)命令的約束條件大體如下表所示(不代表真實(shí)需求):

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通


通過(guò)登錄可以連接到主控板的Shell終端,此時(shí),若執(zhí)行enterUshell命令則進(jìn)入U(xiǎn)Shell終端,執(zhí)行enterSlaveBoard則進(jìn)入受控板的Shell終端。在受控板同樣可以執(zhí)行enterUshell進(jìn)入它的UShell終端。系統(tǒng)還提供了對(duì)應(yīng)的退出操作。整個(gè)操作引起的變遷如下圖所示:

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通


執(zhí)行升級(jí)的流程是在讓基站設(shè)備處于失效狀態(tài)下,獲取基站設(shè)備的軟件版本信息,然后在后端基于最新版本進(jìn)行比較。得到版本之間的差異后,通過(guò)transfer命令傳輸新文件,put命令更新文件,deleteFiles命令刪除多余的文件。成功更新后,再激活基站設(shè)備。因此,一個(gè)典型的升級(jí)流程如下所示:

  • login (Master Board Shell)

  • inactive (Master Board UShell)

  • get (Slave Board Shell)

  • transfer(Master Board Shell)

  • put(Slave Board Shell)

  • deleteFiles(Slave Board Ushell)

  • active(Master Board UShell)

  • logout

整個(gè)版本升級(jí)系統(tǒng)要求:無(wú)論當(dāng)前基站設(shè)備屬于哪種分類(lèi),處于哪種終端,只要Telnet連接沒(méi)有中斷,在要求升級(jí)執(zhí)行的命令必須執(zhí)行成功。如果當(dāng)前所處的設(shè)備與終端不滿(mǎn)足要求,系統(tǒng)就需要遷移到正確的狀態(tài),以確保命令的執(zhí)行成功。

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

二、尋找解決方案

根據(jù)這個(gè)需求,我們期待的客戶(hù)端調(diào)用為(為簡(jiǎn)便起見(jiàn),省略了所有的方法參數(shù)):


//client 
public void upgrade() {
  TelnetService service = new TelnetService();

  service.login();
  service.inactive();
  service.get();
  service.transfer();
  service.put();
  service.deleteFiles();
  service.active();
  service.logout();
}

這樣簡(jiǎn)便直觀(guān)的調(diào)用,實(shí)則封裝了復(fù)雜的規(guī)則和轉(zhuǎn)換邏輯。我們應(yīng)該怎么設(shè)計(jì)才能達(dá)到這樣的效果呢?

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

使用條件分支

一種解決方法是使用條件分支,因?yàn)閷?duì)于每條Telnet命令而言,都需要判斷當(dāng)前的狀態(tài),以決定執(zhí)行不同的操作,例如:


public class TelnetService {
  private String currentState = "INITIAL";

  public void transfer() {
      swich (currentState.toUpperCase()) {
          case "INITIAL":
              login();
              currentState = "MASTER_SHELL";
              break;
          case "MASTER_SHELL":
              // ignore
              ......
      }

      // 執(zhí)行transfer命令
  }
}

然而這樣的實(shí)現(xiàn)是不可接受的,因?yàn)槲覀冃枰獙?duì)每條命令都要編寫(xiě)相似的條件分支語(yǔ)句,這就導(dǎo)致出現(xiàn)了重復(fù)代碼。我們可以將這樣的邏輯封裝到一個(gè)方法中:


public class TelnetService {
  private String currentState = "INITIAL";
  public void transfer() {
      swichState("MASTER_SHELL");
      // 執(zhí)行transfer命令
  }
  private void switchState(String targetState) {
      switch (currentState.toUpperCase()) {
          case "INITIAL":
              switch (targetState.toUpperCase()) {
                  case "INITIAL":
                      break;
                  case "MASTER_SHELL":
                      login();
                      break;
                  // 其他分支略
              }
              break;
          // 其他分支略
      }
  }
}

switchState()方法避免了條件分支的重復(fù)代碼,但是它同時(shí)也加重了方法實(shí)現(xiàn)的復(fù)雜度,因?yàn)樗枰瑫r(shí)針對(duì)當(dāng)前狀態(tài)與目標(biāo)狀態(tài)進(jìn)行判斷,這相當(dāng)于是一個(gè)條件組合。

Kent Beck認(rèn)為:“(條件分支的)所有邏輯仍然在同一個(gè)類(lèi)里,閱讀者不必四處尋找所有可能的計(jì)算路徑。但條件語(yǔ)句的缺點(diǎn)是:除了修改對(duì)象本身的代碼之外,沒(méi)有其他辦法修改它的邏輯。……條件語(yǔ)句的好處在于簡(jiǎn)單和局部化。”顯然,由于條件分支的集中化,導(dǎo)致變化發(fā)生時(shí),我們只需要修改這一處;但問(wèn)題在于任何變化都需要對(duì)此進(jìn)行修改,這實(shí)際上是重構(gòu)中“發(fā)散式變化(Divergent Change)”壞味道。

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

引入職責(zé)驅(qū)動(dòng)設(shè)計(jì)

職責(zé)驅(qū)動(dòng)設(shè)計(jì)強(qiáng)調(diào)從“職責(zé)”的角度思考設(shè)計(jì)。職責(zé)是“擬人化”的思考模式,這實(shí)際上是面向?qū)ο蠓治雠c設(shè)計(jì)的思維模式:將對(duì)象看作是有思想有判斷有知識(shí)有能力的“四有青年”。這也就是我所謂的“智能對(duì)象”。只要分辨出職責(zé),就可以從知識(shí)和能力的角度入手,尋找哪個(gè)對(duì)象具備履行該職責(zé)的能力?

回到版本升級(jí)系統(tǒng)這個(gè)例子,從諸如transfer、put等命令的角度思考職責(zé),則可以識(shí)別職責(zé)為:

  • 執(zhí)行Telnet命令

    • 遷移到正確的狀態(tài)

    • 運(yùn)行Telnet命令

TelnetService具有執(zhí)行Telnet命令的能力,如果要運(yùn)行的命令太多,也可以考慮將運(yùn)行各個(gè)命令的職責(zé)再分派給對(duì)應(yīng)的Command對(duì)象。那么,又該誰(shuí)來(lái)執(zhí)行“遷移到正確的狀態(tài)”呢?看能力?——誰(shuí)具有遷移狀態(tài)的能力?一個(gè)對(duì)象能夠履行某個(gè)職責(zé),必須具備履行職責(zé)的知識(shí),所以就要看知識(shí)。

遷移到正確狀態(tài)需要哪些知識(shí)?——當(dāng)前狀態(tài)、目標(biāo)狀態(tài)以及如何遷移狀態(tài)。只要確定了當(dāng)前狀態(tài)和目標(biāo)狀態(tài),根據(jù)前面的狀態(tài)變遷圖就可以知道該如何遷移狀態(tài)了。那么,誰(shuí)確定地知道當(dāng)前狀態(tài)呢?——只有狀態(tài)對(duì)象自身才知道!在條件分支實(shí)現(xiàn)中,狀態(tài)是通過(guò)字符串表達(dá)的,字符串對(duì)象自身并不知道其值到底是什么,需要取出其值進(jìn)行判斷,這就是使用條件分支的原因。當(dāng)狀態(tài)從一個(gè)字符串升級(jí)為狀態(tài)對(duì)象時(shí),狀態(tài)的值就是狀態(tài)對(duì)象“自己知道”的知識(shí)。當(dāng)每種狀態(tài)都知道自己的狀態(tài)值時(shí),它們?nèi)粢男小斑w移狀態(tài)”的職責(zé),就無(wú)需再對(duì)當(dāng)前狀態(tài)進(jìn)行判斷了,這正是為何多態(tài)能夠替代條件分支的原因。

我們可以定義一個(gè)狀態(tài)的繼承樹(shù):


public interface NodeState {
  void switchTo(???);
}
public class InitialState implements NodeState {}
public class MasterShellState implements NodeState {}


當(dāng)狀態(tài)變?yōu)閷?duì)象且具有職責(zé)時(shí),對(duì)象就是有思想的職能對(duì)象。遺憾的是,它具有的知識(shí)還不足以完全履行“遷移到正確狀態(tài)”的職責(zé),因?yàn)樗⒉恢涝撨w移到哪個(gè)目標(biāo)狀態(tài)。這個(gè)知識(shí)只有具體的Telnet命令才知道,因而需要傳遞給它。一種做法是作為方法參數(shù)傳入,但這會(huì)導(dǎo)致方法體內(nèi)需要對(duì)傳入的參數(shù)作條件分支判斷。另一種方法則利用方法的多態(tài),顯式地定義多種方法來(lái)履行遷移到不同目標(biāo)狀態(tài)的職責(zé):


interface NodeState {
   void switchToInitial();
   void switchToMasterShell();
   void switchToMasterUshell();
   void switchToSlaveShell();
   void switchToSlaveUshell();
}

public class InitialState implements NodeState {
   public InitialState(TelnetService service) {
       this.service = service;
   }

   public void switchToInitial() {
       // do nothing
   }

   public void switchToMasterShell() {
       service.login();
       service.setCurrentState(new MasterShellState(service));
   }

   public void switchToMasterUshell() {
       service.login();
       service.enterUshell();
       service.setCurrentState(new MasterUshellState(service));
   }
   public void switchToSlaveShell() {
       service.login();
       service.enterSlave();
       service.setCurrentState(new SlaveShellState(service));
   }

   public void switchToSlaveUshell() {
       service.login();
       service.enterSlave();
       service.enterUshell();
       service.setCurrentState(new SlaveShellState(service));
   }
}

public class MasterShellState implement NodeState {
   public MasterShell(TelnetService service) {
       this.service = service;
   }

   public void switchToInitial() {
       service.logout();
       service.setCurrentState(new InitialState(service));
   }

   public void switchToMasterShell() {
       //do nothing
   }

   public void switchToMasterUshell() {
       service.enterUshell();
       service.setCurrentState(new MasterUshellState(service));
   }

   public void switchToSlaveShell() {
       service.enterSlave();
       service.setCurrentState(new SlaveShellState(service));
   }

   public void switchToSlaveUshell() {
       service.enterSlave();
       service.enterUshell();
       service.setCurrentState(new SlaveShellState(service));
   }
}

class TelnetService {
   private NodeState currentState = new InitialState(this);
   public void setCurrentState(NodeState state) {
       this.currentState = state;
   }
   public void inactive() {
       currentState.switchToMasterUshell();
       //inactive impl
   }
   public void transfer() {
       currentState.switchToMasterShell();
       //real transfer impl
   }
       
   public void active() {
       currentState.switchToMasterUshell();
       // real active impl
   }

   public void get() {
       currentState.switchToSlaveShell();
       // get
   }
}


這樣的設(shè)計(jì)并沒(méi)有做到“開(kāi)放封閉原則”,當(dāng)增加了新的狀態(tài)時(shí),由于需要在NodeState接口中增加新的方法,使得所有實(shí)現(xiàn)該接口的狀態(tài)類(lèi)都需要修改。這相當(dāng)于從條件分支的“發(fā)散式變化”壞味道變成了“霰彈式修改(Shotgun Surgery)”壞味道,即一個(gè)變化引起多處修改。然而比起條件分支方案而言,由于不用再判斷當(dāng)前狀態(tài),復(fù)雜度降低了許多,可以有效減少bug的產(chǎn)生。

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

狀態(tài)模式

將一個(gè)狀態(tài)進(jìn)化為對(duì)象,這種設(shè)計(jì)思想是狀態(tài)模式的設(shè)計(jì)。根據(jù)GOF的《設(shè)計(jì)模式》,一個(gè)標(biāo)準(zhǔn)的狀態(tài)模式類(lèi)圖如下所示:

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

當(dāng)我們要設(shè)計(jì)的業(yè)務(wù)具有復(fù)雜的狀態(tài)變遷時(shí),往往通過(guò)狀態(tài)圖來(lái)表現(xiàn)。利用狀態(tài)圖,可以非常容易地將其轉(zhuǎn)換為狀態(tài)模式。狀態(tài)圖的每個(gè)狀態(tài)被封裝一個(gè)狀態(tài)對(duì)象,所有狀態(tài)對(duì)象實(shí)現(xiàn)同一個(gè)抽象接口。該抽象接口的方法則為狀態(tài)圖上觸發(fā)狀態(tài)遷移的命令。Context對(duì)象持有一個(gè)全局變量,用以保存當(dāng)前狀態(tài)對(duì)象。每個(gè)狀態(tài)對(duì)象持有Context對(duì)象,通過(guò)Context訪(fǎng)問(wèn)全局的當(dāng)前狀態(tài)變量,以完成狀態(tài)的遷移。具體的狀態(tài)對(duì)象在實(shí)現(xiàn)狀態(tài)接口時(shí),倘若是不符合條件的命令,則實(shí)現(xiàn)為空,或者拋出異常。

依據(jù)狀態(tài)圖,可以實(shí)現(xiàn)為狀態(tài)模式:


interface NodeState {
   void login();
   void logout();
   void enterUshell();
   void exitUshell();
   void enterSlaveBoard();
   void exitSlaveBoard();
}

public class InitialState implements NodeState {
   private TelnetService telnetService;
   public InitialState(TelnetService telnetService) {
       this.telnetService = telnetService;
   }
   public void login() {
       //login
       telnetService.login();
       this.telnetService.setCurrentState(new MasterShellState(telnetService));
   }
   public void logout() //do nothing }
   public void enterUshell() {
       throw new IlegalStateException();
   }
   //其他方法略
}
// 其他狀態(tài)對(duì)象略

在實(shí)現(xiàn)Telnet的transfer等命令時(shí),這一設(shè)計(jì)卻未達(dá)到意料的效果:

public class TelnetService {
   private NodeState currentState = new InitialState();
   public void setCurrentState(NodeState state) {    
       this.currentState = state;
   }

   public void transfer() {
       // currentState到底是哪個(gè)狀態(tài)?
       if (!currentState.isMasterShell()) {
           // 需要遷移到正確的狀態(tài)
       }
       // transfer implementation
   }
}

引入了狀態(tài)模式后,在transfer()方法中仍然需要判斷當(dāng)前狀態(tài),這與條件分支方案何異?是狀態(tài)模式存在問(wèn)題嗎?非也!這實(shí)際上是應(yīng)用場(chǎng)景的問(wèn)題。讓我們聯(lián)想一下地鐵刷卡進(jìn)站的場(chǎng)景,該場(chǎng)景只有Opened和Closed兩個(gè)狀態(tài),其狀態(tài)遷移如下圖所示:

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

比較兩個(gè)狀態(tài)圖。對(duì)于地鐵場(chǎng)景,當(dāng)?shù)罔F門(mén)處于Closed狀態(tài)時(shí),需要支付刷卡才能切換到Opened狀態(tài),如果不滿(mǎn)足條件,這個(gè)狀態(tài)將一直保持。也就是說(shuō),對(duì)于客戶(hù)端調(diào)用者而言,合法的調(diào)用只能是pay(),如果調(diào)用行為是pass()或者timeout(),狀態(tài)對(duì)象將不給予響應(yīng)。版本升級(jí)系統(tǒng)則不然。當(dāng)系統(tǒng)處于Initial狀態(tài)時(shí),系統(tǒng)無(wú)法限制客戶(hù)端調(diào)用者只能發(fā)起正確的login()方法。因?yàn)樘峁┙o客戶(hù)端的命令操作并非login()、enterUShell()等引起狀態(tài)變遷的方法,而是transfer、put等命令。同時(shí),需求又要求無(wú)論當(dāng)前處于什么狀態(tài),執(zhí)行什么命令,都要遷移到正確的狀態(tài)。這正是版本升級(jí)管理系統(tǒng)無(wú)法按照標(biāo)準(zhǔn)狀態(tài)模式進(jìn)行設(shè)計(jì)的原因所在。

職責(zé)驅(qū)動(dòng)設(shè)計(jì)及狀態(tài)模式的融會(huì)貫通

三、結(jié)論

如果我們熟悉狀態(tài)模式,針對(duì)本文的業(yè)務(wù)場(chǎng)景,或許會(huì)首先想到狀態(tài)模式。然而,設(shè)計(jì)模式是有應(yīng)用場(chǎng)景的,我們不能一味蠻干,或者按照模式的套路去套用,這是會(huì)出現(xiàn)問(wèn)題的。通過(guò)分辨職責(zé)的設(shè)計(jì)方法,同時(shí)明確所謂“智能對(duì)象”的意義,我們照樣可以推導(dǎo)出一個(gè)好的設(shè)計(jì)。我們雖然抽象出了狀態(tài)對(duì)象,但抽象的方法并非引起狀態(tài)遷移的行為,而是遷移狀態(tài)的行為。我們沒(méi)有從設(shè)計(jì)模式開(kāi)始,而是從“職責(zé)”開(kāi)始對(duì)設(shè)計(jì)進(jìn)行驅(qū)動(dòng),這是職責(zé)驅(qū)動(dòng)設(shè)計(jì)的設(shè)計(jì)驅(qū)動(dòng)力。

當(dāng)我們引入狀態(tài)智能對(duì)象時(shí),我們并沒(méi)有獲得一個(gè)完全遵循開(kāi)放封閉原則的設(shè)計(jì)方案。實(shí)際上,當(dāng)狀態(tài)發(fā)生變化時(shí),要做到對(duì)擴(kuò)展完全開(kāi)放是非常困難的。即使可行,在狀態(tài)變化的需求是未知的情況下,為此付出太多的設(shè)計(jì)與開(kāi)發(fā)成本是沒(méi)有必要的。恰如其分的設(shè)計(jì)來(lái)滿(mǎn)足當(dāng)前的需求即可。當(dāng)然,我們可以考慮將抽象的狀態(tài)接口修改為抽象類(lèi),這樣就可以把增加新方法對(duì)實(shí)現(xiàn)類(lèi)帶來(lái)的影響降低。不過(guò),Java 8為接口提供了默認(rèn)方法,已經(jīng)可以規(guī)避這個(gè)問(wèn)題了。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI