文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

干掉项目中杂乱的 if-else,试试状态模式,这才是优雅的实现方式!

2024-12-02 09:50

关注

首先是处理容器的开启和闭合,这就需要使用栈来保存预期的下一个字符类型,再对比栈顶字符类型和当前处理字符,决定解析的结果。

还要注意类型嵌套的情况下,内层嵌套的容器作为外层容器的元素被解析完成时,需要修改外层容器的预期字符。而且 Map 作为一种相对 Set 和 List 比较特殊的容器,还要处理它的左右元素。同时还不能忘记处理各种异常,如未知字符、容器内是原始类型、容器未正确闭合等。

而这些逻辑混杂在一块就更添复杂度了,通常是一遍代码写下来挺顺畅,找几个特殊的 case 一验证,往往就有没有考虑到的点,你以为解决了这个点就好了,殊不知这个问题点的解决方案又引起了另一个问题。

最终修修补补好多次,终于把代码写完了,连优化的想法都没了,担心又引入新的问题。更多 Java 核心技术教程:https://github.com/javastacks/javastack,一起来学习吧。

最终的伪代码如下: 

  1. public String parseToFullType() throws IllegalStateException {  
  2.     StringBuilder sb = new StringBuilder();  
  3.     for (; ; this.scanner.next()) {  
  4.         Character currentChar = scanner.current();  
  5.         if (currentChar == '\uFFFF') {  
  6.             return sb.toString();  
  7.         }  
  8.         if (isCollection()) {  
  9.             if (CollectionEnd()) {  
  10.                 dealCollectionEleEnd();  
  11.             }else {  
  12.                 throw new IllegalStateException("unexpected char '" + currentChar + "' at position " + scanner.getIndex());  
  13.             }  
  14.         } else if (isWrapperType()) {  
  15.             dealSingleEleEnd();  
  16.         } else if (parseStart()) {  
  17.             if (collectionStart()) {  
  18.                 putCollecitonExpectEle()  
  19.             }  
  20.         } else {  
  21.             throw new IllegalStateException("unknown char '" + currentChar + "' at position " + scanner.getIndex());  
  22.         }  
  23.     }    

状态机方式

是不是看起来非常乱,这还没有列出各个方法里的条件判断语句呢。这么多逻辑混杂,造成的问题就是很难改动,因为你不知道改动会影响哪些其他逻辑。

面对这种问题,当然有一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,就是状态机。

状态机

有限状态机(finite-state machine,缩写:FSM)又称有限状态自动机(finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

像我们生活中在公路上驾驶汽车就像在维护一个状态机,遇到红灯就停车喝口水,红灯过后再继续行车,遇到了黄灯就要减速慢行。而实现状态机前要首先明确四个主体:

将状态机的四种要素提取之后,就可以很简单地将状态和事件进行解耦了。

状态拆分

还是拿我的这个需求来分析,先画出状态变化图从整体上把握状态间的关系。

通过上面的图一步步拆解状态机:

    1. 首先是确定状态,我定义了 Start/SetStart/SetEle/ListStart/ListEel/MapStart/MapLeft/MapRight 八种基础状态,由于一次只解析一个类型,容器闭合就代表着解析结束,所以没有对各个容器设置结束状态。又因为有状态嵌套的存在,而一个状态没法表达状态机的准确状态,需要使用栈来存储整体的解析状态,我使用这个栈为空来代表 End 状态,又省略了一个状态。

    2. 再拆分事件,事件是扫描到的每一个字符,由于字符种类较多,而像 integer 和 double、String 和 Long 的处理又没有什么区别,我将事件类型抽象为 包装类型元素(WRAPPED_ELE),原始类型元素(PRIMITIVE_ELE),MAP、List 和 Set 五种。

    3. 变幻和动作都是事件发生后系统的反应,在我的需要里需要转变解析状态,并将结构结果保存起来。这里我将它们整体抽象为一个事件处理器接口,如: 

  1. public interface StateHandler {  
  2.       
  3.     void handle(Event event, Stack<State> states, StringBuilder result);  

代码示例

将状态机的各个要素都抽出来之后,再分别完善每个 StateHandler 的处理逻辑就行,这部分就非常简单了,下面是 MapLeftHandler 的详情。 

  1. public class MapLeftHandler implements StateHandler {  
  2.     @Override  
  3.     public void handle(Event event, Stack<State> states, StringBuilder result) {  
  4.         // 这里是核心的 Action,将单步解析结果放到最终结果内  
  5.         result.append(",");  
  6.         result.append(event.getParsedVal());  
  7.         // 状态机的典型处理方式,处理各种事件发生在当前状态时的逻辑  
  8.         switch (event.getEventType()) {  
  9.             case MAP:  
  10.                 states.push(State.MAP_START);  
  11.                 break;  
  12.             case SET:  
  13.                 states.push(State.SET_START);  
  14.                 break;  
  15.             case LIST:  
  16.                 states.push(State.LIST_START);  
  17.                 break;  
  18.             case WRAPPED_ELE:  
  19.                 // 使用 pop 或 push 修改栈顶状态来修改解析器的整体状态  
  20.                 states.pop();  
  21.                 states.push(State.MAP_RIGHT);  
  22.                 break;  
  23.             case PRIMITIVE_ELE:  
  24.                 // 当前状态不能接受的事件类型要抛异常中断  
  25.                 throw new IllegalStateException("unexpected primitive char '" + event.getCharacter() + "' at position " + event.getIndex());  
  26.             default:  
  27.         }  
  28.     }  

主类内的代码如下: 

  1. public static String parseToFullType(String shortenType) throws IllegalStateException {  
  2.     StringBuilder result = new StringBuilder();  
  3.     StringCharacterIterator scanner = new StringCharacterIterator(shortenType);  
  4.     Stack<State> states = new Stack<>();  
  5.     states.push(State.START);  
  6.     for (; ; scanner.next()) { 
  7.         char currentChar = scanner.current();  
  8.         if (currentChar == '\uFFFF') {  
  9.             return result.toString();  
  10.         }  
  11.         // 使用整体状态为空来代表解析结束  
  12.         if (states.isEmpty()) {  
  13.             throw new IllegalStateException("unexpected char '" + currentChar + "' at position " + scanner.getIndex());  
  14.         }  
  15.         // 将字符规整成事件对象,有利于参数的传递  
  16.         Event event = Event.parseToEvent(currentChar, scanner.getIndex());  
  17.         if (event == null) {  
  18.             throw new IllegalStateException("unknown char '" + currentChar + "' at position " + scanner.getIndex());  
  19.         }  
  20.         // 这里需要一个 Map 来映射状态和状态处理器  
  21.         STATE_TO_HANDLER_MAPPING.get(states.peek()).handle(event, states, result);  
  22.     }  
  23.  

小结

状态模式

如果你对设计模式较熟的话,会发现这不就是状态模式嘛。

有解释说,状态模式会将事件类型也再解耦,即 StateHandler 里不只有一个方法,而是会有八个方法,分别为 handleStart,HandleListEle 等,但我觉得模式并不是定式,稍微的变形是没有问题的,如果单个事件类型的处理足够复杂,将其再拆分更合理一些。

代码结构

最后,对比 if-else 实现,从代码量上来看,状态机实现增加了很多,这是解耦的代价,当然也有很多重复代码的缘故,比如在容器闭合时校验当前容器是否内嵌容器,并针对内嵌容器做处理的逻辑就完全一样,为了代码清晰我就没有再抽取方法。

从可维护性上来说,状态机实现由于逻辑拆分比较清晰,在添加或删除一种状态时比较方便,添加一个状态和状态处理器就行,但在添加一种事件类型时较为复杂,需要修改所有状态处理器里的实现,不过从整体上来看是利大于弊的,毕竟代码清晰易改动最重要。

了解了状态机实现的固定套路之后,你也可以写出高大上的状态机代码了,快 Get 起来替换掉项目里杂乱的 if-else 吧。 

 

来源:Hollis内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯