27.其实你不懂老板的心 - 解释器模式(Interpreter)

27.1 其实你不懂老板的心

  • 时间: 7月12日20点   地点: 小菜大鸟住所的客厅   人物: 大鸟, 小菜

     “大鸟, 今天我们大老板找我谈话.” 小菜对大鸟说道.

     “…..”
     “小子, 你真是块做编程的料, 什么问题都想靠编程解决呀? 这种东西要靠感悟的, 时间长了, 你就学会慢慢分析了.” 大鸟提醒道, “不过, 你说到了解释器, 我确实想跟你说解释器模式, 他其实就是用来翻译文法句子的.”
     “是吗, 说来听听.”

27.2 解释器模式

解释其模式(interpreter), 给定一个语言, 定义它的文法的一种表示, 并定义一个解释器, 这个解释器使用该表示来解释语言中的句子.[DP]

     “解释器模式需要解决的是, 如果一种特定的问题发生的频率足够高, 那么可能就值得将该问题的各个实例表述为一个简单语言中的句子. 这样就可以构建一个解释器, 该解释器通过解释这些句子来解决问题.[DP] 比方说, 我们常常会在字符串中所搜一个匹配的字符, 或判断一个字符串是否符合我们规定的格式, 此时我们一般会用什么技术?”
     “正则表达式?”
     “对, 非常好, 因为这个匹配字符的需求在软件的很多地方都会用到, 而且行为之间非常类似, 过去的做法是针对特定的需求, 编写特定的函数, 比如判断 email, 匹配电话号码等等, 与其为每一个特定的需求都写一个算法函数, 不如使用一个通用的所搜算法来执行一个正则表达式, 该正则表达式定义了待匹配字符串的集合[DP]. 而所谓解释器模式, 正则表达式就是他的一种应用, 这其实为正则表达式提供了一种文法, 如何定义一个正则表达式, 以及如何解释这个正则表达式.”
     “我的理解, 是不是像 IE, FireFox, 其实也是在解释 HTML 语法, 将下载到客户端的 HTML 标记文本转换成网页格式显示到用户?”
     “哈, 是可以这么说, 不过编写一个浏览器的程序, 当然要复杂得多.”
     “下面我们来看看解释器模式的基本结构和实现代码.”



     AbstractExpression (抽象表达式), 声明一个抽象类解释操作, 这个接口为抽象语法树中所有类共享.

1
2
3
public abstract class AbstractExpression {
public abstract void interpret(Context context);
}

     TerminalExpression(终结符表达式), 实现与文法中终结符相关的解释操作. 实现抽象表达式中所要求的接口, 主要是一个interpret() 方法. 文法中每一个终结符都一个具体终结符表达式与之对应.

1
2
3
4
5
6
7
public class TerminalExpression extends AbstractExpression {
@Override
public void interpret(Context context) {
System.out.println("终端解释器");
}
}

     NonterminalExpression(非终结符表达式), 为文法中非终结符实现解释操作. 对文法中每一条规则 R1, R2…Rn 都需要一个具体的非终结符表达式类. 通过实现抽象的表达式 interpret() 方法实现解释操作. 解释操作以递归的方式调用上面所代表的 R1, R2…Rn 中各个符号的实例变量.

1
2
3
4
5
6
7
public class NonterminalExpression extends AbstractExpression {
@Override
public void interpret(Context context) {
System.out.println("非终端解释器");
}
}

     Context, 包含解释器之外的一些全局信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Context {
private String input;
private String output;
public String getInput() {
return input;
}
public void setInput(String input) {
this.input = input;
}
public String getOutput() {
return output;
}
public void setOutput(String output) {
this.output = output;
}
}

     客户端代码, 构建表示该文法定义语言中的一个特定的句子的抽象语法树. 调用解释操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
Context context = new Context();
List<AbstractExpression> list = new LinkedList<>();
list.add(new TerminalExpression());
list.add(new NonterminalExpression());
list.add(new TerminalExpression());
list.add(new TerminalExpression());
for (AbstractExpression exp : list) {
exp.interpret(context);
}
}

     结果显示

1
2
3
4
终端解释器
非终端解释器
终端解释器
终端解释器

27.3 解释器模式的好处.

     “看起来好像不难, 但其实真正做起来应该还是很难的吧.”
     “是的, 你想, 用解释器模式, 就相当于开发一个编程语言或脚本给别人用, 这当然是很难的.”
     “我的理解是, 解释器模式就是用 ‘迷你语言’ 来表现程序要解决的问题, 以 ‘迷你语言’ 写成 ‘迷你程序’ 来表现具体的问题.”
     “恩, 迷你这个词用的好, 就是这样的意思. 通常当有一个语言需要解释执行, 并且你可以将该语言中的句子表示成一个抽象语法树时, 可使用解释器模式.[DP]
     “解释器模式有什么好处呢?”
     “用了解释器模式, 就意味着可以很容易的改变和扩展文法, 因为该模式用类来表示文法规则, 你可以使用继承来改变和扩展该文法. 也比较容易实现文法, 因为定义抽象语法书中各个节点的类的实现大体类似, 这些类都易于直接编写.[DP]
     “除了向正则表达式, 浏览器等应用, 解释器模式还用在了什么地方?”
     “只要使用语言来描述的, 都可以应用呀. 比如只对机器人, 如果为了让她走段路还需要取电难面前调用前走, 左转, 右转的方法, 那也太傻了吧. 当然应该直接跟他说, ‘哥们儿, 向前走十步, 然后左转90度, 再向前走5步.’”
     “哈, 机器人听得懂 ‘哥们儿’ 是什么意思吗?”
     “这就看你写的解释器够不够用了, 如果你增加了 ‘哥们儿’ 的文法, 他就听得懂. 说白了, 解释器就是将这样的一句话, 转换成可以执行的命令程序而已. 而不用解释器模式本来也可以分析, 但通过继承抽象表达式的方式, 由于依赖倒转原则, 使得对文法的扩展和维护都带来了方便.”
     “哈, 难道说, C#, Java 这些高级语言, 都是用解释器的方式开发的?”
     “当然不是那么简单了, 解释器模式也有不足之处, 解释器模式为文法中每一条规则至少定义了一个类, 因此包含许多规则的文法可能难以管理和维护. 建议当文法非常复杂时, 使用其他技术如语法分析程序或编译生成器来处理[DP].
     “哦, 原来还有语法分析器, 编译生成器这样的东东.”

27.4 音乐解释器

     “好了, 要真正掌握, 还需要练习, 我们来做一个小型的解释器程序.”
     “好呀, 程序的需求是什么?”
     “你以前有没有用过 QBASIC ?”
     “没有, 听说那是 VB 以前, DOS 状态下的编程语言.”
     “是的, 大鸟我以前整天用它来学习写程序, QBASIC 就是早期的 BASIC, 它当中提供了专门的演奏音乐的语句 PLAY, 不过由于那会多媒体并不像如今这般流行, 所以所谓的音乐也仅仅相当于手机中的单音铃声.” 大鸟说到.
     “你一说这个我知道了, 我以前手机里就有编辑铃声的功能, 通过输入一些简单的字母数字, 就可以让手机发出音乐. 我还试着找了些歌谱编了几首流行歌进去呢.” 小菜接话道.
     “哈, 那就好, 你想呀, 那就是典型的解释器模式的应用, 你用 QB 或者手机中定义的规则去编写音乐程序, 不就是一段文法让 QB 或手机去翻译称具体的指令来执行吗!”
     “我明白了, 这就是解释器的应用呀.”
     “现在我定义一条规则, 和 QB 的有点类似, 但为了简单起见, 我做了改动, 你就按我定义的规则来编程. 我的规则是 O 表示音阶, ‘O1’表示低音阶, ‘O2’ 表示中音阶, ‘O3’ 表示高音阶; ‘P’ 表示休止符, ‘C D E F G A B’ 表示 ‘Do-Re-Mi-Fa-So-La-Ti’; 音符长度 1 表示一拍, 2表示二拍, 0.5 表示半拍, 0.25表示四分之一拍, 以此类推; 注意: 所有的字符和数字都要用半角空格分开. 例如上海滩第一句, ‘浪奔’, 可以写成 ‘O 2 E 0.5 G 0.5 A 3’ 表示中银开始, 演奏的是 mi so la.”



     “好的, 我试试编编看.”
     “为了只关注设计模式编程, 而不是具体播放实现, 你只需要用控制台根据实现编写的语句解释成简谱就成了.”
     “OK!”

27.5 音乐解释器的实现

     一个小时候, 小菜通过几番改良, 给出了答案.
     代码结构图.



     演奏内容类(PlayContext)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 演奏内容类
public class PlayContext {
// 演奏文本
private String text;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

     表达式类(Expression)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Expression {
public void interpret(PlayContext context) {
if (context.getText().length() <= 0) {
return;
} else {
// 当前演奏文本的第一条命令字母和参数值
// 例如 O 3 G 0.5 A 0.5 E 3
// 则 playKey 为 O, playValue 为 3
String playKey = context.getText().substring(0, 1);
context.setText(context.getText().substring(2));
double playValue = Double.valueOf(context.getText().substring(0, context.getText().indexOf(" ")));
execute(playKey, playValue);
context.setText(context.getText().substring(context.getText().indexOf(" ") + 1));
}
}
// 执行
public abstract void execute(String key, double value);
}

     音符类(Note)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Note extends Expression {
@Override
public void execute(String key, double value) {
String note = "";
// 如果是C 则演奏 1,
// 如果是 D 则演奏 2, 一次类推...
switch (key) {
case "C":
note = "1";
break;
case "D":
note = "2";
break;
case "E":
note = "3";
break;
case "F":
note = "4";
break;
case "G":
note = "5";
break;
case "A":
note = "6";
break;
case "B":
note = "7";
break;
}
System.out.print(note + " ");
}
}

     音符类(TerminalExpression)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Scale extends Expression {
@Override
public void execute(String key, double value) {
// 如果获得的 key 为O, value 为1 则演奏低音
String note = "";
switch ((int)value) {
case 1:
note = "低音";
break;
case 2:
note = "中音";
break;
case 3:
note = "高音";
break;
}
System.out.print(note + " ");
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
PlayContext playContext = new PlayContext();
System.out.println("上海滩");
playContext.setText("O 2 E 0.5 G 0.5 A 3 E 0.5 G 0.5 D 3 E 0.5 G 0.5 A 0.5 O 3 C 1 O 2 A 0.5 G 1 C 0.5 E 0.5 D 3 ");
Expression expression = null;
while (playContext.getText().length() > 0) {
String str = playContext.getText().substring(0, 1);
switch (str) {
case "O":
expression = new Scale();
break;
case "C":
case "D":
case "E":
case "F":
case "G":
case "A":
case "B":
case "P":
expression = new Note();
break;
}
expression.interpret(playContext);
}
}

     结果显示

1
2
上海滩
中音 3 5 6 3 5 2 3 5 6 高音 1 中音 6 5 3 2

     “写的非常不错, 现在我需要增加一个文法, 就是演奏速度, 要求是 T 代表速度, 一毫秒为单位, ‘T 1000’ 表示每节拍一秒, ‘T500’ 表示每节拍半秒. 你如何做?”
     “学了设计模式这么久, 这点感觉难道还没有, 首先增加一个表达式的子类叫音速. 然后再在客户端的分支判断中增加一个 case 分支就可以了.”
     音速类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Speed extends Expression {
@Override
public void execute(String key, double value) {
String speed = "";
switch ((int) value) {
case 1000:
speed = "慢速";
break;
case 500:
speed = "快速";
break;
}
System.out.print(speed + " ");
}
}

     客户端代码(部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.......
// 增加 T 500
playContext.setText("T 500 O 2 E 0.5 G 0.5 A 3 E 0.5 G 0.5 D 3 E 0.5 G 0.5 A 0.5 O 3 C 1 O 2 A 0.5 G 1 C 0.5 E 0.5 D 3 ");
Expression expression = null;
while (playContext.getText().length() > 0) {
String str = playContext.getText().substring(0, 1);
switch (str) {
case "O":
expression = new Scale();
break;
// 增加 speed 分类.
case "T":
expression = new Speed();
break;
case "C":
case "D":
case "E":
........

     结果显示

1
2
上海滩
快速 中音 3 5 6 3 5 2 3 5 6 高音 1 中音 6 5 1 3 2

     “但是小菜, 在增加一个文法时, 你除了扩展一个类外, 还是改动了客户端.” 大鸟质疑道.
     “哈, 这不就是实例化的问题吗, 只要在客户端的 switch 那里应用简单工厂加反射就可以做到不改动客户端了.”
     “说的好, 看来你的确是学明白了, 在这里讲解解释器模式, 也就不那么追究了, 只要知道可以这样重构程序就行. 其实这个例子是不能代表解释器模式的全貌的, 因为它只有终结符表达式, 而没有非终结符表达式的子类, 如果想真正理解解释器模式, 还需要去研究其他例子. 另外这个是控制台程序, 如果我给你钢琴所有的按键声音的文件, MP3格式, 你可以利用 Media Player 控件, 写出真实的音乐语言解释器程序吗?”
     “你的意思是说, 只要我按照简谱编好这样的语句, 就可以让电脑模拟刚请弹奏出来?”
     “是的, 可以吗?”
     “当然是可以, 连设计模式都学得会, 这点算什么.”
     “OK, 那我就等你哪天给我写出这样的钢琴模拟程序哦.”
     “没问题.”
     (注: 由于钢琴模拟程序过于复杂, 书中篇幅有限, 代码不在书中显示, 但程序的功能有一定趣味性, 在随书提供的源代码中有参考代码, 有兴趣的读者可以下载研究.)

27.6 料事如神

  • 时间: 7月16日20点   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟

     小菜: “大鸟, 你真是料事如神啊, 尽管都不是什么好消息, 但两件事都让你猜对了.”
     “哦, 那两件事?”
     “……”

    

~感谢捧场,您的支持将鼓励我继续创作~