当前位置:首页>编程知识库>后端开发知识>设计模式是什么鬼(解释器)
设计模式是什么鬼(解释器)
阅读 1
2019-01-07
Java知音”,选择“置顶公众号”
技术文章第一时间送达!

//本文作者:凸凹里歐

//本文收录菜单栏:《设计模式是什么鬼》专栏中

解释,一定是针对某种语言的拆解、释意,并按照文法翻译、转换成另一种表达形式以达到目标能够理解的目的。比如我们都知道Java编程语言是人类可以理解的语言,程序写好后要先进行编译生成字节码(class文件),然后对此文件解释成机器码,最终机器才可以理解并执行,这就是解释器存在的意义。
就拿我们人类的自然语言来举例,比如我们要进行英文翻译工作,首先要对一句话(表达式)进行拆解,而拆开后的单词就成了不可再分的终极表达式,例如说对英语句子“I love you”(非终极表达式)进行拆解,按空格分割为单词“I”、“love”、“you”(终极表达式),然后翻译每个单词再合并成“我爱你”,貌似很简单的样子。再看句子“I love that you love”,翻译成“我爱你那个你爱”。
这简直太荒谬了,这句明明是“我爱你所爱”的意思,貌似这里的拆解方式是有讲究的。“that you love”在这里应该是作为从句出现,所以它应该属于一个特殊的“非终极表达式”,有自己独特的翻译方式,而不是简单的单词拼接了。我们意识到语言的翻译绝非易事,但至少我们通过思考搞明白了终极与非终极表达式的区别、表达式的多态性、以及表达式的自包含关系结构。
化繁为简,为了保持例子的简约实用风格,我们决定自己发明一种语言,什么语言呢?对于骨灰级网游玩家来说打怪升级是最漫长的过程,既浪费时间又伤害身体,该怎么解决这个问题呢?有网瘾,电电就好。
开个玩笑,所以呢我们研发了一款挂机程序并起名”耗子精“,它可以直接发送指令给鼠标驱动来实现点击、移动操作,从此解放我们的双手让游戏人物自动打怪升级。既然不操作鼠标,那就需要一段脚本告诉“耗子精”怎样去操作鼠标指针,于是我们发明了一种脚本语言“精神食粮”,像是下面这样:
1BEGIN%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20%E8%84%9A%E6%9C%AC%E5%BC%80%E5%A7%8B%0A2MOVE%20500,600;%20%20%20%20%20%20%20%20//%20%E9%BC%A0%E6%A0%87%E7%A7%BB%E5%8A%A8%E5%88%B0%E5%9D%90%E6%A0%87(500,%20600)%0A3%20%20%20%20BEGIN%20LOOP%205%20%20%20%20%20//%20%E5%BC%80%E5%A7%8B%E5%BE%AA%E7%8E%AF5%E6%AC%A1%0A4%20%20%20%20%20%20%20%20LEFT_CLICK;%20%20//%20%E5%BE%AA%E7%8E%AF%E4%BD%93%E5%86%85%E5%8D%95%E5%87%BB%E5%B7%A6%E9%94%AE%0A5%20%20%20%20%20%20%20%20DELAY%201;%20%20%20%20%20//%20%E6%AF%8F%E6%AC%A1%E5%BB%B6%E6%97%B61%E7%A7%92%0A6%20%20%20%20END;%20%20%20%20%20%20%20%20%20%20%20%20%20//%20%E5%BE%AA%E7%8E%AF%E4%BD%93%E7%BB%93%E6%9D%9F%0A7RIGHT_DOWN;%20%20%20%20%20%20%20%20%20%20//%20%E6%8C%89%E4%B8%8B%E5%8F%B3%E9%94%AE%0A8DELAY%207200;%20%20%20%20%20%20%20%20%20%20//%20%E5%BB%B6%E6%97%B62%E5%B0%8F%E6%97%B6%0A9END;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20%E8%84%9A%E6%9C%AC%E7%BB%93%E6%9D%9F%0A
看注释很容易就能理解这是要干什么了,玩家先让鼠标挪动到地图的某个点上,然后不停地点击了n次(比如此处简化为5次)过后人物便到达了刷怪地点了(计算好延时时间),最后按下右键触发技能并一直不松开,直到挂机2小时后结束,这样便实现了自动打怪升级。
我们现在来对这个语言的表达式进行拆解、抽象、建模,可以看到除了循环(非终极表达式)以外其他的都是单个命令不可以拆了,也就是我们之前讲过的终极表达式,按照这个脚本我们先看一张结构图。
可以看到从始发节点“指令序列表达式”(根)开始被拆解成三个分支,第一步和第三步都是执行鼠标动作的终极表达式了(叶),而第二步的“循环”则属于非终极表达式(枝),它的循环体内可以包含多步指令,所以它包括一个子指令集(枝),然后继续往下延续出“左键单击表达式”(枝)和“系统延时表达式”(叶),最后“单击”其实就是“按下”与“松开”的组合了。有没有这个语义树结构好像似曾相识?没错,这就是之前讲过的“组合模式”,我们正是利用了“组合模式”(强调结构型)的结构模型构建了这个语义树(Syntax Tree),来完成我们的翻译工作(这里强调行为型)。
开始写代码,这么多表达式到底应该从哪里开始定义呢?不管三七二十一它们统统都是表达式,先写个表达式总抽象。
1public%20interface%20Expression%20%7B%20//%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%8E%A5%E5%8F%A3%0A2%20%20%20%20public%20void%20interpret();%20//%20%E8%A7%A3%E9%87%8A%E6%96%B9%E6%B3%95%0A3%7D%0A
可以看到这个接口定义了表达式的通用解释方法标准,一切表达式都得符合这个规则。接下来我们先从最基本的原子操作(终极表达式)开始定义实现类,它们应该依次是移动鼠标、左(右)键按下(松开)、系统延时表达式等,雷同的我们不做赘述,读者可以自己实现。
%201public%20class%20Move%20implements%20Expression%20%7B%0A%202%20%20%20%20//%20%E9%BC%A0%E6%A0%87%E4%BD%8D%E7%BD%AE%E5%9D%90%E6%A0%87%0A%203%20%20%20%20private%20int%20x,%20y;%0A%204%0A%205%20%20%20%20public%20Move(int%20x,%20int%20y)%20%7B%0A%206%20%20%20%20%20%20%20%20this.x%20=%20x;%0A%207%20%20%20%20%20%20%20%20this.y%20=%20y;%0A%208%20%20%20%20%7D%0A%209%0A10%20%20%20%20public%20void%20interpret()%20%7B%0A11%20%20%20%20%20%20%20%20System.out.println(%22%E7%A7%BB%E5%8A%A8%E9%BC%A0%E6%A0%87%EF%BC%9A%E3%80%90%22%20 %20x%20 %20%22,%22%20 %20y%20 %20%22%E3%80%91%22);%0A12%20%20%20%20%7D%0A13%0A14%7D
1public%20class%20LeftDown%20implements%20Expression%20%7B%0A2%0A3%20%20%20%20public%20void%20interpret()%20%7B%0A4%20%20%20%20%20%20%20%20System.out.println(%22%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%22);%0A5%20%20%20%20%7D%0A6%0A7%7D
1public%20class%20LeftUp%20implements%20Expression%20%7B%0A2%0A3%20%20%20%20public%20void%20interpret()%20%7B%0A4%20%20%20%20%20%20%20%20System.out.println(%22%E6%9D%BE%E5%BC%80%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%22);%0A5%20%20%20%20%7D%0A6%0A7%7D
%201public%20class%20Delay%20implements%20Expression%20%7B%0A%202%0A%203%20%20%20%20private%20int%20seconds;//%20%E5%BB%B6%E6%97%B6%E7%A7%92%E6%95%B0%0A%204%0A%205%20%20%20%20public%20Delay(int%20seconds)%20%7B%0A%206%20%20%20%20%20%20%20%20this.seconds%20=%20seconds;%0A%207%20%20%20%20%7D%0A%208%0A%209%20%20%20%20public%20int%20getSeconds()%20%7B%0A10%20%20%20%20%20%20%20%20return%20seconds;%0A11%20%20%20%20%7D%0A12%0A13%20%20%20%20public%20void%20interpret()%20%7B%0A14%20%20%20%20%20%20%20%20System.out.println(%22%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A%22%20 %20seconds%20 %20%22%E7%A7%92%E9%92%9F%22);%0A15%20%20%20%20%20%20%20%20try%20%7B%0A16%20%20%20%20%20%20%20%20%20%20%20%20Thread.sleep(seconds%20*%201000);%0A17%20%20%20%20%20%20%20%20%7D%20catch%20(InterruptedException%20e)%20%7B%0A18%20%20%20%20%20%20%20%20%20%20%20%20e.printStackTrace();%0A19%20%20%20%20%20%20%20%20%7D%0A20%20%20%20%20%7D%0A21%0A22%7D%0A
很简单,它们都实现了interpret方法,并进行了相关操作的模拟。照猫画虎,下来实现非终极表达式:左(右)键单击表达式、循环表达式、以及指令集序列表达式等。
%201public%20class%20LeftClick%20implements%20Expression%20%7B%0A%202%0A%203%20%20%20%20private%20Expression%20leftDown;%0A%204%20%20%20%20private%20Expression%20leftUp;%0A%205%0A%206%20%20%20%20public%20LeftClick()%20%7B%0A%207%20%20%20%20%20%20%20%20this.leftDown%20=%20new%20LeftDown();%0A%208%20%20%20%20%20%20%20%20this.leftUp%20=%20new%20LeftUp();%0A%209%20%20%20%20%7D%0A10%0A11%20%20%20%20public%20void%20interpret()%20%7B%0A12%20%20%20%20%20%20%20%20//%E5%8D%95%E5%87%BB=%E5%85%88%E6%8C%89%E4%B8%8B%E5%86%8D%E6%9D%BE%E5%BC%80%0A13%20%20%20%20%20%20%20%20leftDown.interpret();%0A14%20%20%20%20%20%20%20%20leftUp.interpret();%0A15%20%20%20%20%7D%0A16%0A17%7D%0A
这里有点意思了,单击表达式被我们继续拆分成“按下”及“松开”两个原子操作,由于点击是个固定的死操作,并不需要提供给客户端任何灵活性把它们传入进来,所以我们在构造时(第7行)主动实例化了它们。接下来是循环表达式,我们需要知道循环次数,以及循环体内要执行的表达式。
%201public%20class%20Repetition%20implements%20Expression%20%7B%0A%202%0A%203%20%20%20%20private%20int%20loopCount;//%20%E5%BE%AA%E7%8E%AF%E6%AC%A1%E6%95%B0%0A%204%20%20%20%20private%20Expression%20expression;//%20%E5%BE%AA%E7%8E%AF%E4%BD%93%E8%A1%A8%E8%BE%BE%E5%BC%8F%0A%205%0A%206%20%20%20%20public%20Repetition(Expression%20expression,%20int%20loopCount)%20%7B%0A%207%20%20%20%20%20%20%20%20this.expression%20=%20expression;%0A%208%20%20%20%20%20%20%20%20this.loopCount%20=%20loopCount;%0A%209%20%20%20%20%7D%0A10%0A11%20%20%20%20public%20void%20interpret()%20%7B%0A12%20%20%20%20%20%20%20%20while%20(loopCount%20>%200)%20%7B%0A13%20%20%20%20%20%20%20%20%20%20%20%20expression.interpret();%0A14%20%20%20%20%20%20%20%20%20%20%20%20loopCount--;%0A15%20%20%20%20%20%20%20%20%7D%0A16%20%20%20%20%7D%0A17%0A18%7D%0A
清晰明了,循环表达式被初始化后用这些参数进行循环、并调用循环体表达式的解释方法,继续向下传递,至于这个表达式里具体还有什么子表达式我们根本不关心,这里主要负责循环调用,仅此而已。最后就是指令集序列表达式的实现了。
%201public%20class%20Sequence%20implements%20Expression%20%7B%0A%202%20%20%20%20//%20%E6%8C%87%E4%BB%A4%E9%9B%86%E5%BA%8F%E5%88%97%0A%203%20%20%20%20private%20List<Expression>%20expressions;%0A%204%0A%205%20%20%20%20public%20Sequence(List<Expression>%20expressions)%20%7B%0A%206%20%20%20%20%20%20%20%20this.expressions%20=%20expressions;%0A%207%20%20%20%20%7D%0A%208%0A%209%20%20%20%20public%20void%20interpret()%20%7B%0A10%20%20%20%20%20%20%20%20//%20%E5%BE%AA%E7%8E%AF%E6%8C%A8%E4%B8%AA%E8%A7%A3%E6%9E%90%E6%AF%8F%E6%9D%A1%E6%8C%87%E4%BB%A4%0A11%20%20%20%20%20%20%20%20expressions.forEach(exp%20->%20exp.interpret());%0A12%20%20%20%20%7D%0A13%0A14%7D%0A
我们要运行的脚本一定是有先后顺序的,所以这个指令集表达式里包含一个List<Expression>,在构造时(第5行)由客户端传入,并于第11行挨个顺序调用解释方法。貌似脚本用到的表达式已经定义完毕,客户端开始调用。
%201public%20class%20Client%20%7B%0A%202%20%20%20%20public%20static%20void%20main(String%5B%5D%20args)%20%7B%0A%203%20%20%20%20%20%20%20%20/*%0A%204%20%20%20%20%20%20%20%20%20*%20BEGIN%20%20%20%20%20%20%20%20%20%20%20%20%20//%20%E8%84%9A%E6%9C%AC%E5%BC%80%E5%A7%8B%0A%205%20%20%20%20%20%20%20%20%20*%20MOVE%20500,600;%20%20%20%20%20//%20%E9%BC%A0%E6%A0%87%E7%A7%BB%E5%8A%A8%E5%88%B0%E5%9D%90%E6%A0%87(500,%20600)%0A%206%20%20%20%20%20%20%20%20%20*%20%20BEGIN%20LOOP%205%20%20%20%20%20//%20%E5%BC%80%E5%A7%8B%E5%BE%AA%E7%8E%AF5%E6%AC%A1%0A%207%20%20%20%20%20%20%20%20%20*%20%20%20%20%20%20LEFT_CLICK;%20%20//%20%E5%BE%AA%E7%8E%AF%E4%BD%93%E5%86%85%E5%8D%95%E5%87%BB%E5%B7%A6%E9%94%AE%0A%208%20%20%20%20%20%20%20%20%20*%20%20%20%20%20%20DELAY%201;%20%20%20%20%20//%20%E6%AF%8F%E6%AC%A1%E5%BB%B6%E6%97%B61%E7%A7%92%0A%209%20%20%20%20%20%20%20%20%20*%20%20END;%20%20%20%20%20%20%20%20%20%20%20%20%20//%20%E5%BE%AA%E7%8E%AF%E4%BD%93%E7%BB%93%E6%9D%9F%0A10%20%20%20%20%20%20%20%20%20*%20RIGHT_DOWN;%20%20%20%20%20%20%20//%20%E6%8C%89%E4%B8%8B%E5%8F%B3%E9%94%AE%0A11%20%20%20%20%20%20%20%20%20*%20DELAY%207200;%20%20%20%20%20%20%20//%20%E5%BB%B6%E6%97%B62%E5%B0%8F%E6%97%B6%0A12%20%20%20%20%20%20%20%20%20*%20END;%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20%E8%84%9A%E6%9C%AC%E7%BB%93%E6%9D%9F%0A13%20%20%20%20%20%20%20%20%20*/%0A14%0A15%20%20%20%20%20%20%20%20//%20%E6%9E%84%E9%80%A0%E6%8C%87%E4%BB%A4%E9%9B%86%E8%AF%AD%E4%B9%89%E6%A0%91%EF%BC%8C%E5%AE%9E%E9%99%85%E6%83%85%E5%86%B5%E4%BC%9A%E4%BA%A4%E7%BB%99%E8%AF%AD%E6%B3%95%E8%A7%A3%E6%9E%90%E5%99%A8%EF%BC%88Evaluator%20or%20Parser%EF%BC%89%E3%80%82%0A16%20%20%20%20%20%20%20%20Expression%20sequence%20=%20new%20Sequence(Arrays.asList(%0A17%20%20%20%20%20%20%20%20%20%20%20%20new%20Move(500,%20600),%20%0A18%20%20%20%20%20%20%20%20%20%20%20%20new%20Repetition(%0A19%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20new%20Sequence(%0A20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Arrays.asList(new%20LeftClick(),%20%0A21%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20new%20Delay(1))%0A22%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20),%0A23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%205%0A24%20%20%20%20%20%20%20%20%20%20%20%20),%20%0A25%20%20%20%20%20%20%20%20%20%20%20%20new%20RightDown(),%0A26%20%20%20%20%20%20%20%20%20%20%20%20new%20Delay(7200)%0A27%20%20%20%20%20%20%20%20));%0A28%0A29%20%20%20%20%20%20%20%20sequence.interpret();%0A30%20%20%20%20%20%20%20%20/*%E6%89%93%E5%8D%B0%E8%BE%93%E5%87%BA%0A31%20%20%20%20%20%20%20%20%20%20%20%20%E7%A7%BB%E5%8A%A8%E9%BC%A0%E6%A0%87%EF%BC%9A%E3%80%90500,600%E3%80%91%0A32%20%20%20%20%20%20%20%20%20%20%20%20%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A33%20%20%20%20%20%20%20%20%20%20%20%20%E6%9D%BE%E5%BC%80%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A34%20%20%20%20%20%20%20%20%20%20%20%20%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A1%E7%A7%92%E9%92%9F%0A35%20%20%20%20%20%20%20%20%20%20%20%20%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A36%20%20%20%20%20%20%20%20%20%20%20%20%E6%9D%BE%E5%BC%80%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A37%20%20%20%20%20%20%20%20%20%20%20%20%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A1%E7%A7%92%E9%92%9F%0A38%20%20%20%20%20%20%20%20%20%20%20%20%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A39%20%20%20%20%20%20%20%20%20%20%20%20%E6%9D%BE%E5%BC%80%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A40%20%20%20%20%20%20%20%20%20%20%20%20%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A1%E7%A7%92%E9%92%9F%0A41%20%20%20%20%20%20%20%20%20%20%20%20%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A42%20%20%20%20%20%20%20%20%20%20%20%20%E6%9D%BE%E5%BC%80%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A43%20%20%20%20%20%20%20%20%20%20%20%20%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A1%E7%A7%92%E9%92%9F%0A44%20%20%20%20%20%20%20%20%20%20%20%20%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A45%20%20%20%20%20%20%20%20%20%20%20%20%E6%9D%BE%E5%BC%80%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%B7%A6%E9%94%AE%0A46%20%20%20%20%20%20%20%20%20%20%20%20%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A1%E7%A7%92%E9%92%9F%0A47%20%20%20%20%20%20%20%20%20%20%20%20%E6%8C%89%E4%B8%8B%E9%BC%A0%E6%A0%87%EF%BC%9A%E5%8F%B3%E9%94%AE%0A48%20%20%20%20%20%20%20%20%20%20%20%20%E7%B3%BB%E7%BB%9F%E5%BB%B6%E8%BF%9F%EF%BC%9A7200%E7%A7%92%E9%92%9F%0A49%20%20%20%20%20%20%20%20%20*/%0A50%20%20%20%20%7D%0A51%7D%0A
注意看第16行,仔细参照注释中的脚本并实例化我们的语义树结构,最后只需调用根节点的interpret()方法即可完成整个解释工作。其实对于这个脚本转语义树的工作我们完全可以自己实现一个Evaluator来分析这段脚本并生成语义树(类似于编译的过程),由于这并不属于解释器模式的范畴,所以我们就不混淆进来了,这里我们留给读者朋友自己实现。
终于,“耗子精”有了脚本识别的能力,并顺利对接鼠标驱动,帮我们自动完成升级,玩家再也不用没日没夜地做那些无聊至极的重复动作了,并且后期如果需要更多的功能还可以对表达式继续进行扩展(比如对键盘指令的解释),我们只需优雅地植入语义树即可,就这么简单。正是因为我们对语言的语法解析、表达式抽象化,关系结构化,使得让翻译工作变得即插即用,解释器模式不但提高了代码的易读性、易用性、可维护性,更重要的是对未来语言变化的可扩展性。
语言是可以拆解的,句子是可以包括从句(子句)或单词的,单词是具有终极原子性的,它们统统重复出现。





干货!必须好看☟




以上数据来源于网络,如有侵权,请联系删除。
评论 (0)