当前位置: 首页 > 科技观察

去掉项目中那些乱七八糟的if-else,试试状态模式,这才是优雅的实现方式!

时间:2023-03-15 16:06:50 科技观察

IF-ELSE方法本来以为写一个简单的类型翻译器不会花太多时间,但是真正做到的时候才发现要注意的点太多了。首先是处理容器的打开和关闭,这需要使用栈来保存期望的下一个字符类型,然后将栈顶的字符类型与当前处理的字符进行比较,判断结果分析。另外需要注意的是,在类型嵌套的情况下,当内部嵌套容器被解析为外部容器的元素时,需要修改外部容器的期望字符。而且,作为相对于Set和List的特殊容器,Map也需要处理它的左右元素。同时,我们也不能忘记处理各种异常,比如未知字符、容器中的原始类型、错误关闭的容器等。这些逻辑混合在一起增加了复杂性。通常,代码一次就写得很顺利。找了几个特例验证后,往往有没有考虑到的点。你觉得解决这个点就好了。殊不知,这个问题点的解决又引发了另一个问题。最后修修补补了很多次,终于把代码写完了,连优化的念头都没有了,担心引入新的问题。更多Java核心技术教程:https://github.com/javastacks/javastack,一起来学习吧。最终伪代码如下:publicStringparseToFullType()throwsIllegalStateException{StringBuildersb=newStringBuilder();for(;;this.scanner.next()){CharactercurrentChar=scanner.current();if(currentChar=='\uFFFF'){returnsb.toString();}if(isCollection()){if(CollectionEnd()){dealCollectionEleEnd();}else{thrownewIllegalStateException("unexpectedchar'"+currentChar+"'atposition"+scanner.getIndex());}}elseif(isWrapperType()){dealSingleEleEnd();}elseif(parseStart()){if(collectionStart()){putCollecitonExpectEle()}}else{thrownewIllegalStateException("unknownchar'"+currentChar+"'atposition"+scanner.getIndex());}}状态机方法是不是看起来很乱,各个方法中的条件判断语句还没列出来。这么多逻辑混在一起,问题是很难改变,因为你不知道改变会影响到其他哪些逻辑。面对这种问题,当然有一套代码设计经验总结,反复使用,为大多数人所知,归类整理,就是状态机。状态机有限状态机(finite-statemachine,缩写:FSM),也称为有限状态自动化(缩写:FSA),简称状态机,是对有限数量的状态和之间的转换的表示这些状态和动作等行为的数学计算模型。例如,在我们的生活中,在路上开车就像在维护一个状态机。遇到红灯停车喝酒,红灯后继续行驶,遇到黄灯减速慢行。在实现状态机之前,首先要定义四个主体:State状态:状态是系统在其生命周期中某一时刻的运行状态。例如,在驾驶的例子中,状态包括三种状态:正常速度驾驶、停车和低速驾驶。事件事件:事件是在特定时刻施加于系统的信号。在上面的例子中,一个事件指的是红灯、绿灯和黄灯。所有状态的改变都依赖于事件,但事件也可能导致状态不改变。比如正常行驶遇到绿灯,就不需要反应。TransformationTransition:一个transformation是系统在一个事件发生后,如上例中的减速、停车或加速等,将进行的状态改变。Action动作:动作也是事件发生后系统的反应。不同之处在于该操作不会更改系统状态。比如开车遇到红灯停车喝水,不会影响系统状态。把状态机的四大要素抽取出来后,状态和事件就可以轻松解耦了。对于状态分裂,我还是分析一下自己的需求。首先画出状态变化图,从整体上把握状态之间的关系。通过上图一步步拆解状态机:1、首先确定状态。我定义了八种基本状态:Start/SetStart/SetEle/ListStart/ListEel/MapStart/MapLeft/MapRight。由于一次只解析一种类型,容器关闭就意味着解析结束,所以没有为每个容器设置结束状态。并且由于有状态嵌套的存在,一个状态不能表达状态机的确切状态,需要用一个栈来存储整体的解析状态。我用这个堆栈来表示End状态并省略另一个状态。2.再次拆分事件。事件是扫描的每个字符。由于字符的类型很多,对integer和double、String和Long的处理没有区别,我把事件类型抽象成wrapping类型元素(WRAPPED_ELE)、原始类型元素(PRIMITIVE_ELE)、MAP、List和设置五。3.变化和动作是事件发生后系统的反应。在我的需求中,我需要更改分析状态并保存结构结果。这里我将它们整体抽象为一个事件处理接口,如:publicinterfaceStateHandler{/***@paramevent待处理事件*@paramstates系统整体状态*@paramresult分析结果*/voidhandle(Eventevent,Stackstates,StringBuilderresult);}代码示例提取出状态机的所有元素后,分别完善各个StateHandler的处理逻辑即可。这部分非常简单。以下是MapLeftHandler的详细信息。publicclassMapLeftHandlerimplementsStateHandler{@Overridepublicvoidhandle(Eventevent,Stackstates,StringBuilderresult){//这里是核心Action,将单步分析结果放入最终结果result.append(",");result.append(event.getParsedVal());//状态机的典型处理方法,逻辑开关(event.getEventType()){caseMAP:states.push(State.MAP_START);break;caseSET:states.push(State.SET_START);break;caseLIST:states.push(State.LIST_START);break;caseWRAPPED_ELE://使用pop或push修改栈顶状态来修改解析器的整体状态states.pop();states.push(State.MAP_RIGHT);break;casePRIMITIVE_ELE://当前状态不能接受的事件类型需要抛出异常中断thrownewIllegalStateException("unexpectedprimitivechar'"+event.getCharacter()+"'atposition"+event.getIndex());default:}}}主类中的代码如下:<>();states.push(State.START);为了(;;scanner.next()){charcurrentChar=scanner.current();if(currentChar=='\uFFFF'){returnresult.toString();}//使用整体状态为空表示解析结束if(states.isEmpty()){thrownewIllegalStateException("unexpectedchar'"+currentChar+"'atposition"+scanner.getIndex());}//将字符组成事件对象,有利于参数传递Eventevent=Event.parseToEvent(currentChar,scanner.getIndex());if(event==null){thrownewIllegalStateException("unknownchar'"+currentChar+"'atposition"+scanner.getIndex());}//这里需要一个Map来映射state和状态处理器STATE_TO_HANDLER_MAPPING.get(states.peek()).handle(event,states,result);}}状态模式小结如果熟悉设计模式,你会发现这是状态模式的再解耦,即就是,StateHandler中的方法不是只有一个,而是八个方法,分别是handleStart、HandleListEle等,但是我觉得mode不是一个固定的公式,一个slig也没有问题高温变形。如果单个事件类型的处理已经足够复杂,再拆分一次更合理。代码结构最后,与if-else实现相比,状态机实现在代码量上增加了很多,这就是解耦的代价。当然也有很多重复的代码,比如关闭容器时检查当前容器是否嵌入。容器,和处理嵌入式容器的逻辑是完全一样的。为了代码清晰,我没有提取方法。在可维护性方面,状态机的实现由于逻辑上的清晰划分,在增加或删除状态时更加方便。添加状态和状态处理器即可,但是添加事件类型时比较复杂,需要修改。在所有状态处理器中实现,但总体来说,利大于弊。毕竟,最重要的是代码清晰易改。了解了状态机实现的固定套路后,还可以编写高级状态机代码。赶紧搞定,把项目中乱七八糟的if-else替换掉。