<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          干掉if-else,試試狀態(tài)模式!

          共 6016字,需瀏覽 13分鐘

           ·

          2022-01-23 23:53

          上一篇:請(qǐng)馬上卸載Notepad++...!

          來源:zhenbianshu.github.io

          背景


          玩轉(zhuǎn) Java 動(dòng)態(tài)編譯,實(shí)現(xiàn)了 Java 代碼的動(dòng)態(tài)編譯后,接下來就要將原來使用注釋配置的 Java 數(shù)據(jù)類型改為使用縮寫替代。


          為了便于縮寫,能直觀地看出完整類型,我設(shè)計(jì)的方案是:



          我使用普通的 if-else 方式和狀態(tài)機(jī)方式各實(shí)現(xiàn)了一遍,更深切地理解了狀態(tài)機(jī)在處理這種多狀態(tài)的復(fù)雜問題時(shí)的優(yōu)越性。


          兩種實(shí)現(xiàn)的代碼我都放在了 github 上,地址是:Github-zhenbianshu-java_shorten_type_parser,有類似需求的可以改改來用。


          IF-ELSE 方式


          原來以為寫一個(gè)簡(jiǎn)單的類型翻譯器花不了太多時(shí)間,可是真做起來,才發(fā)現(xiàn)要注意的點(diǎn)太多了。


          首先是處理容器的開啟和閉合,這就需要使用棧來保存預(yù)期的下一個(gè)字符類型,再對(duì)比棧頂字符類型和當(dāng)前處理字符,決定解析的結(jié)果。


          還要注意類型嵌套的情況下,內(nèi)層嵌套的容器作為外層容器的元素被解析完成時(shí),需要修改外層容器的預(yù)期字符。而且 Map 作為一種相對(duì) Set 和 List 比較特殊的容器,還要處理它的左右元素。


          同時(shí)還不能忘記處理各種異常,如未知字符、容器內(nèi)是原始類型、容器未正確閉合等。


          而這些邏輯混雜在一塊就更添復(fù)雜度了,通常是一遍代碼寫下來挺順暢,找?guī)讉€(gè)特殊的 case 一驗(yàn)證,往往就有沒有考慮到的點(diǎn),你以為解決了這個(gè)點(diǎn)就好了,殊不知這個(gè)問題點(diǎn)的解決方案又引起了另一個(gè)問題。


          最終修修補(bǔ)補(bǔ)好多次,終于把代碼寫完了,連優(yōu)化的想法都沒了,擔(dān)心又引入新的問題。


          最終的偽代碼如下:


              public String parseToFullType() throws IllegalStateException {        StringBuilder sb = new StringBuilder();

          for (; ; this.scanner.next()) { Character currentChar = scanner.current(); if (currentChar == '\uFFFF') { return sb.toString(); } if (isCollection()) { if (CollectionEnd()) { dealCollectionEleEnd(); }else { throw new IllegalStateException("unexpected char '" + currentChar + "' at position " + scanner.getIndex()); } } else if (isWrapperType()) { dealSingleEleEnd(); } else if (parseStart()) { if (collectionStart()) { putCollecitonExpectEle() } } else { throw new IllegalStateException("unknown char '" + currentChar + "' at position " + scanner.getIndex()); } }

          ??

          狀態(tài)機(jī)方式


          是不是看起來非常亂,這還沒有列出各個(gè)方法里的條件判斷語句呢。這么多邏輯混雜,造成的問題就是很難改動(dòng),因?yàn)槟悴恢栏膭?dòng)會(huì)影響哪些其他邏輯。

          面對(duì)這種問題,當(dāng)然有一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié),就是狀態(tài)機(jī)。

          狀態(tài)機(jī)


          有限狀態(tài)機(jī)(finite-state machine,縮寫:FSM)又稱有限狀態(tài)自動(dòng)機(jī)(finite-state automation,縮寫:FSA),簡(jiǎn)稱狀態(tài)機(jī),是表示有限個(gè)狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動(dòng)作等行為的數(shù)學(xué)計(jì)算模型。


          像我們生活中在公路上駕駛汽車就像在維護(hù)一個(gè)狀態(tài)機(jī),遇到紅燈就停車喝口水,紅燈過后再繼續(xù)行車,遇到了黃燈就要減速慢行。而實(shí)現(xiàn)狀態(tài)機(jī)前要首先明確四個(gè)主體:

          狀態(tài) State:狀態(tài)是一個(gè)系統(tǒng)在其生命周期的某一刻時(shí)的運(yùn)行狀態(tài),如駕車的例子中狀態(tài)就包括 正常速度行駛、停車和低速行駛?cè)N狀態(tài)。

          事件 Event:事件就是某一時(shí)刻施加于系統(tǒng)的某個(gè)信號(hào),在上面的例子中事件是指紅燈、綠燈和黃燈。所有的狀態(tài)變化都要依賴事件,但事件也可能導(dǎo)致狀態(tài)不發(fā)生變化,如正常行駛中遇到綠燈就不用做什么反應(yīng)。

          變換 Transition:變換是在事件發(fā)生之后系統(tǒng)要做出的狀態(tài)變化,如上面例子中的減速、停車或加速。

          動(dòng)作 Action:動(dòng)作是同樣是事件發(fā)生之后系統(tǒng)做出的反應(yīng),不同的是,動(dòng)作不會(huì)改變系統(tǒng)狀態(tài),像駕車遇到紅燈停車后,喝水這個(gè)動(dòng)作沒有對(duì)系統(tǒng)狀態(tài)造成影響。

          將狀態(tài)機(jī)的四種要素提取之后,就可以很簡(jiǎn)單地將狀態(tài)和事件進(jìn)行解耦了。


          狀態(tài)拆分


          還是拿我的這個(gè)需求來分析,先畫出狀態(tài)變化圖從整體上把握狀態(tài)間的關(guān)系。


          通過上面的圖一步步拆解狀態(tài)機(jī):


          • 首先是確定狀態(tài),我定義了 Start/SetStart/SetEle/ListStart/ListEel/MapStart/MapLeft/MapRight 八種基礎(chǔ)狀態(tài),由于一次只解析一個(gè)類型,容器閉合就代表著解析結(jié)束,所以沒有對(duì)各個(gè)容器設(shè)置結(jié)束狀態(tài)。又因?yàn)橛袪顟B(tài)嵌套的存在,而一個(gè)狀態(tài)沒法表達(dá)狀態(tài)機(jī)的準(zhǔn)確狀態(tài),需要使用棧來存儲(chǔ)整體的解析狀態(tài),我使用這個(gè)棧為空來代表 End 狀態(tài),又省略了一個(gè)狀態(tài)。另外,搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師回復(fù)關(guān)鍵字"2T”獲取一份驚喜禮包。

           public interface StateHandler {     /**      * @param event 要處理的事件      * @param states 系統(tǒng)整體狀態(tài)      * @param result 解析的結(jié)果      */     void handle(Event event, Stack states, StringBuilder result); }


          代碼示例


          將狀態(tài)機(jī)的各個(gè)要素都抽出來之后,再分別完善每個(gè) StateHandler 的處理邏輯就行,這部分就非常簡(jiǎn)單了,下面是 MapLeftHandler 的詳情。

          public class MapLeftHandler implements StateHandler {    @Override    public void handle(Event event, Stack states, StringBuilder result) {        // 這里是核心的 Action,將單步解析結(jié)果放到最終結(jié)果內(nèi)        result.append(",");        result.append(event.getParsedVal());

          // 狀態(tài)機(jī)的典型處理方式,處理各種事件發(fā)生在當(dāng)前狀態(tài)時(shí)的邏輯 switch (event.getEventType()) { case MAP: states.push(State.MAP_START); break; case SET: states.push(State.SET_START); break; case LIST: states.push(State.LIST_START); break; case WRAPPED_ELE: // 使用 pop 或 push 修改棧頂狀態(tài)來修改解析器的整體狀態(tài) states.pop(); states.push(State.MAP_RIGHT); break; case PRIMITIVE_ELE: // 當(dāng)前狀態(tài)不能接受的事件類型要拋異常中斷 throw new IllegalStateException("unexpected primitive char '" + event.getCharacter() + "' at position " + event.getIndex()); default: } }}


          主類內(nèi)的代碼如下:
              public static String parseToFullType(String shortenType) throws IllegalStateException {        StringBuilder result = new StringBuilder();        StringCharacterIterator scanner = new StringCharacterIterator(shortenType);        Stack states = new Stack<>();        states.push(State.START);

          for (; ; scanner.next()) { char currentChar = scanner.current(); if (currentChar == '\uFFFF') { return result.toString(); } // 使用整體狀態(tài)為空來代表解析結(jié)束 if (states.isEmpty()) { throw new IllegalStateException("unexpected char '" + currentChar + "' at position " + scanner.getIndex()); } // 將字符規(guī)整成事件對(duì)象,有利于參數(shù)的傳遞 Event event = Event.parseToEvent(currentChar, scanner.getIndex()); if (event == null) { throw new IllegalStateException("unknown char '" + currentChar + "' at position " + scanner.getIndex()); }
          // 這里需要一個(gè) Map 來映射狀態(tài)和狀態(tài)處理器 STATE_TO_HANDLER_MAPPING.get(states.peek()).handle(event, states, result); } }


          小結(jié)


          狀態(tài)模式

          如果你對(duì)設(shè)計(jì)模式較熟的話,會(huì)發(fā)現(xiàn)這不就是狀態(tài)模式嘛。

          有解釋說,狀態(tài)模式會(huì)將事件類型也再解耦,即 StateHandler 里不只有一個(gè)方法,而是會(huì)有八個(gè)方法,分別為 handleStart,HandleListEle 等,但我覺得模式并不是定式,稍微的變形是沒有問題的,如果單個(gè)事件類型的處理足夠復(fù)雜,將其再拆分更合理一些。


          代碼結(jié)構(gòu)


          最后,對(duì)比 if-else 實(shí)現(xiàn),從代碼量上來看,狀態(tài)機(jī)實(shí)現(xiàn)增加了很多,這是解耦的代價(jià),當(dāng)然也有很多重復(fù)代碼的緣故,比如在容器閉合時(shí)校驗(yàn)當(dāng)前容器是否內(nèi)嵌容器,并針對(duì)內(nèi)嵌容器做處理的邏輯就完全一樣,為了代碼清晰我就沒有再抽取方法。

          從可維護(hù)性上來說,狀態(tài)機(jī)實(shí)現(xiàn)由于邏輯拆分比較清晰,在添加或刪除一種狀態(tài)時(shí)比較方便,添加一個(gè)狀態(tài)和狀態(tài)處理器就行,但在添加一種事件類型時(shí)較為復(fù)雜,需要修改所有狀態(tài)處理器里的實(shí)現(xiàn),不過從整體上來看是利大于弊的,畢竟代碼清晰易改動(dòng)最重要。

          了解了狀態(tài)機(jī)實(shí)現(xiàn)的固定套路之后,你也可以寫出高大上的狀態(tài)機(jī)代碼了,快 Get 起來替換掉項(xiàng)目里雜亂的 if-else 吧。



          相關(guān)閱讀:2T架構(gòu)師學(xué)習(xí)資料干貨分享


          PS:如果覺得我的分享不錯(cuò),歡迎大家隨手點(diǎn)贊、轉(zhuǎn)發(fā)、在看。

          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕在线资源 | v天堂在线| 青青草成人自拍 | 国产香蕉AV | 在线观看一区二区视频 |