干掉if-else,试试状态模式!
互联网架构师后台回复 2T 有特别礼包
背景
玩转 Java 动态编译,实现了 Java 代码的动态编译后,接下来就要将原来使用注释配置的 Java 数据类型改为使用缩写替代。
为了便于缩写,能直观地看出完整类型,我设计的方案是:
对简单类型如 String、int、Double,就使用类型的首字母替代,如 i -> int / D -> Double; 对于容器类型如 List、Map,使用两个首字母分别标志容器的开始和闭合,如 LDL -> List<Double> / MDDM -> Map<Double,Double>; 由于 Set 的首字母和 String 首字母冲突,将 String 的缩写修改为 T,同时处理了 Long 和 List 的冲突; 支持容器类型的嵌套,如 LLTLL -> List<List<String>> / MTLDLM -> Map<String,List<Double>>;
我使用普通的 if-else 方式和状态机方式各实现了一遍,更深切地理解了状态机在处理这种多状态的复杂问题时的优越性。
原来以为写一个简单的类型翻译器花不了太多时间,可是真做起来,才发现要注意的点太多了。
首先是处理容器的开启和闭合,这就需要使用栈来保存预期的下一个字符类型,再对比栈顶字符类型和当前处理字符,决定解析的结果。
还要注意类型嵌套的情况下,内层嵌套的容器作为外层容器的元素被解析完成时,需要修改外层容器的预期字符。而且 Map 作为一种相对 Set 和 List 比较特殊的容器,还要处理它的左右元素。
同时还不能忘记处理各种异常,如未知字符、容器内是原始类型、容器未正确闭合等。
而这些逻辑混杂在一块就更添复杂度了,通常是一遍代码写下来挺顺畅,找几个特殊的 case 一验证,往往就有没有考虑到的点,你以为解决了这个点就好了,殊不知这个问题点的解决方案又引起了另一个问题。
最终修修补补好多次,终于把代码写完了,连优化的想法都没了,担心又引入新的问题。
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());}}
状态机
状态拆分

首先是确定状态,我定义了 Start/SetStart/SetEle/ListStart/ListEel/MapStart/MapLeft/MapRight 八种基础状态,由于一次只解析一个类型,容器闭合就代表着解析结束,所以没有对各个容器设置结束状态。又因为有状态嵌套的存在,而一个状态没法表达状态机的准确状态,需要使用栈来存储整体的解析状态,我使用这个栈为空来代表 End 状态,又省略了一个状态。另外,搜索公众号互联网架构师回复关键字"2T”获取一份惊喜礼包。
再拆分事件,事件是扫描到的每一个字符,由于字符种类较多,而像 integer 和 double、String 和 Long 的处理又没有什么区别,我将事件类型抽象为 包装类型元素(WRAPPED_ELE),原始类型元素(PRIMITIVE_ELE),MAP、List 和 Set 五种。 变幻和动作都是事件发生后系统的反应,在我的需要里需要转变解析状态,并将结构结果保存起来。这里我将它们整体抽象为一个事件处理器接口,如:
public interface StateHandler {/*** @param event 要处理的事件* @param states 系统整体状态* @param result 解析的结果*/void handle(Event event, Stack<State> states, StringBuilder result);}
将状态机的各个要素都抽出来之后,再分别完善每个 StateHandler 的处理逻辑就行,这部分就非常简单了,下面是 MapLeftHandler 的详情。
public class MapLeftHandler implements StateHandler {@Overridepublic void handle(Event event, Stack<State> states, StringBuilder result) {// 这里是核心的 Action,将单步解析结果放到最终结果内result.append(",");result.append(event.getParsedVal());// 状态机的典型处理方式,处理各种事件发生在当前状态时的逻辑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 修改栈顶状态来修改解析器的整体状态states.pop();states.push(State.MAP_RIGHT);break;case PRIMITIVE_ELE:// 当前状态不能接受的事件类型要抛异常中断throw new IllegalStateException("unexpected primitive char '" + event.getCharacter() + "' at position " + event.getIndex());default:}}}
public static String parseToFullType(String shortenType) throws IllegalStateException {StringBuilder result = new StringBuilder();StringCharacterIterator scanner = new StringCharacterIterator(shortenType);Stack<State> states = new Stack<>();states.push(State.START);for (; ; scanner.next()) {char currentChar = scanner.current();if (currentChar == '\uFFFF') {return result.toString();}// 使用整体状态为空来代表解析结束if (states.isEmpty()) {throw new IllegalStateException("unexpected char '" + currentChar + "' at position " + scanner.getIndex());}// 将字符规整成事件对象,有利于参数的传递Event event = Event.parseToEvent(currentChar, scanner.getIndex());if (event == null) {throw new IllegalStateException("unknown char '" + currentChar + "' at position " + scanner.getIndex());}// 这里需要一个 Map 来映射状态和状态处理器STATE_TO_HANDLER_MAPPING.get(states.peek()).handle(event, states, result);}}
有解释说,状态模式会将事件类型也再解耦,即 StateHandler 里不只有一个方法,而是会有八个方法,分别为 handleStart,HandleListEle 等,但我觉得模式并不是定式,稍微的变形是没有问题的,如果单个事件类型的处理足够复杂,将其再拆分更合理一些。
代码结构
最后,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。
正文结束
1.心态崩了!税前2万4,到手1万4,年终奖扣税方式1月1日起施行~

