其实你不懂老板的心——解释器模式

其实你不懂老板的心——解释器模式

继上一篇“项目多也别傻做——享元模式”后,本文继续讲解《大话设计模式》第27章“其实你不懂老板的心——解释器模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 其实你不懂老板的心

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

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

“哦,大老板找员工谈话,多少有些事情要发生。”大鸟猜测道。

“不知道呀,反正以前我从来没有直接和他面对面说过话,突然他让秘书叫我去他的办公室。我开始多少有些紧张的。”

“他对你说什么了?”

“他问我最近的工作进展情况如何,有没有什么困难,同事相处如何等等。其实也就是简单的调查民情吧。”

“哦,就这么简单?”

“对了,他夸我来着,说‘小蔡,你在公司表现格外出色,要继续好好努力。’”小菜面有得意。

“哈,你原来是想告诉我你们老板夸奖你呀。”大鸟笑道,“你可别太得意了,这句话可以简单地理解为夸奖,也可能有更深层次的含义哦。”

“那还能怎么理解?”

“老板私下对某员工大加夸奖时,多半是‘最近有更多的任务需要你去完成’的意思。”

“啊,不会吧,已经加班够多的了,难道还要加任务?”

“谁叫你平时表现积极呢,年轻人,多做点也没什么关系,积累经验比什么都强,老板赏识你是好事情呀。他还说什么了吗?”

“他还问我另一个叫梅星的同事的情况。我说他工作也很努力。老板最后评价了句‘梅星是个普通员工。’”小菜说。

“有这样的说法?哦,这个梅星情况不妙了。”大鸟一脸深沉。

“咦,为什么?老板也没说他不好呀,只是说他是普通员工。”

“通常老板说某个员工是普通员工,其实他的意思是说,这个员工不够聪明,工作能力不足。”

“啊,有这种事,我可真是听不懂了。难道老板所说的话,都是有潜台词的?”

“当然,当然。在职场上混,这些都不懂如何干得出名堂!职场菜鸟与老手的一大区别就在于,是否能察言观色,见风使舵,是否听得懂别人尤其是老板上司的弦外之音。”

“大鸟不但在编程技术上有所造诣,连这等为人处世之道也深有研究?”

“略知一二吧,其实自己用点心,这也不算什么难事的。”

“要是有一个翻译机,或解释器就好了,省得每次讲话还需要多动脑筋,多烦呀。”

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

“是吗,说来听听。”

2 解释器模式

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

“解释器模式需要解决的是,**如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题[DP]。**比方说,我们常常会在字符串中搜索匹配的字符或判断一个字符串是否符合我们规定的格式,此时一般我们会用什么技术?”

“是不是正则表达式?”

“对,非常好,因为这个匹配字符的需求在软件的很多地方都会使用,而且行为之间都非常类似,过去的做法是针对特定的需求,编写特定的函数,比如判断Email、匹配电话号码等等,与其为每一个特定需求都写一个算法函数,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合[DP]。而所谓的解释器模式,正则表达式就是它的一种应用,解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。”

“我的理解,是不是像IE、Firefox这些浏览器,其实也是在解释HTML文法,将下载到客户端的HTML标记文本转换成网页格式显示到用户?”

“哈,是可以这么说,不过编写一个浏览器的程序,当然要复杂得多。”

“下面我们来看看解释器模式实现的结构图和基本代码。”

解释器模式(interpreter)结构图

其实你不懂老板的心——解释器模式

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

abstract class AbstractExpression
{
  public abstract void Interpret(Context context);
}

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

class TerminalExpression : AbstractExpression
{
  public override void Interpret(Context context)
  {
    Console.WriteLine("终端解释器");
  }
}

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

class NonterminalExpression : AbstractExpression
{
  public override void Interpret(Context context)
  {
    Console.WriteLine(”非终端解释器");
  }
}

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

class Context
{
  private string input;
  public string Input
  {
    get { return input; }
    set { input = value; }
  }  
  
  private string output;
  public string Output
  {
    get { return output; }
    set { output = value; }
  }
}

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

static void Main(string []args)
{
  Context context = new Context();
  IList<AbstractExpression> list = new List<AbstractExpression>();
  list.Add(new TerminalExpression());
  list.Add(new NonterminalExpression());
  list.Add(new TerminalExpression());
  list.Add(new TerminalExpression());
  
  foreach(AbstractExpression exp in list)
  {
    exp.Interpret(context);
  }
  
  Console.Read();
}

结果显示

终端解释器
非终端解释器
终端解释器
终端解释器

3 解释器模式好处

“看起来好像不难,但其实真正做起来应该还是很难的吧。”

“是的,你想,用解释器模式,就如同你开发了一个编程语言或脚本给自己或别人用。这当然是难了。”

“我的理解是,解释器模式就是用‘迷你语言’来表现程序要解决的问题,以迷你语言写成‘迷你程序’来表现具体的问题。”

“嗯,迷你这个词用得很好,就是这样的意思。通常当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式[DP]。

“解释器模式有什么好处呢?”

“用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写[DP]。”

“除了像正则表达式、浏览器等应用,解释器模式还能用在什么地方呢?”

“只要是可以用语言来描述的,都可以是应用呀。比如针对机器人,如果为了让它走段路还需要去电脑面前调用向前走、左转、右转的方法,那也就太傻了吧。当然应该直接是对它说,‘哥们儿,向前走10步,然后左转90度,再向前走5步。’”

“哈,机器人听得懂‘哥们儿’是什么意思吗?”

“这就看你写的解释器够不够用了,如果你增加这个‘哥们儿’的文法,它就听得懂呀。说白了,解释器模式就是将这样的一句话,转变成实际的命令程序执行而已。而不用解释器模式本来也可以分析,但通过继承抽象表达式的方式,由于依赖倒转原则,使得对文法的扩展和维护都带来了方便。”

“哈,难道说,C#、Java这些高级语言都是用解释器模式的方式开发的?”

“当然不是那么简单了,解释器模式也有不足的,解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理[DP]”。

“哦,原来还有语法分析器、编译器生成器这样的东东。”

4 音乐解释器

“好了,要真正掌握,还需要练习,我们来做个小型的解释器程序。”

“好呀,程序需求是什么?”

“你以前有没有用过QBASIC? ”

“没有,听说那是在VB 以前,DOS状态下的编程语言。”

“是的,大鸟我以前就是整天用它来学习写程序的,QBASIC就是早期的BASIC,它当中提供了专门的演奏音乐的语句PLAY,不过由于那会儿多媒体并不像如今这般流行,所以所谓的音乐也仅仅相当于手机中的单音铃声。”大鸟说道。

“你一说这个我知道了,我以前的手机里就有编辑铃声的功能,通过输入一些简单的字母数字,就可以让手机发出音乐。我还试着找了些歌谱编了几首流行歌进去呢。”小菜接话道。

‘“哈,那就好,你想呀,那就是典型的解释器模式的应用,你用QB或者手机说明书中定义的规则去编写音乐程序,不就是一段文法让QB或手机去翻译成具体的指令来执行吗!”

“我明白了,这就是解释器的应用呀。”

“现在我定义一套规则,和QB的有点类似,但为了简便起见,我做了改动,你就按我定义的规则来编程。我的规则是O表示音阶‘O 1’表示低音阶,‘O 2’表示中音阶,‘O 3’表示高音阶;‘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!”

5 音乐解释器实现

一个小时后,小菜通过了几番改良,给出了答案。

代码结构图

其实你不懂老板的心——解释器模式

演奏内容类(context)

//演奏内容
class PlayContext
{
  //演奏文本
  private string text;
  public string PlayText
  {
    get { return text; }
    set { text = value; }
  }
}

表达式类(AbstractExpression)

abstract class Expression
{
  //解释器
  public void Interpret(PlayContext context)
  {
    if(context.PlayText.Length == 0)
    {
      return;
    }
    else
    {
      // 此方法用于将当前的演奏文本第一条命令获得命令字母和其参数值。例如“O3 E 0.5 G 0.5 A 3 ” 则playKey 为O,而 playValue为3
      string playKey = context.PlayText.Substring(0,1); 
      context.PlayText = context.PlayText.substring(2);
      double playVvalue =Convert.ToDouble(context.PlayText.Substring(0, context.PlayText.Indexof(" ")));
      
      // 获得playKey和 playValue后将其从演奏文本中移除。例如“O 3 E 0.5 G 0.5 A 3 ”变成了“E 0.5 G 0.5 A 3 ”
      context.PlayText = context.PlayText.Substring(context.PlayText.IndexOf(" ") + 1);
      Excute(playKey, playvalue);
    }
  }
    
  //执行,抽象方法“执行”,不同的文法子类,有不同的执行处理
  public abstract void Excute(string key, double value);
}

音符类(TerminalExpression)

class Note : Expression
{
  public override void Excute(string key, double value)
  {
    string note = "";
    switch(key)
    {
      // 表示如果获得的key是C则演奏1(do),如果是D则演奏2(Re)
      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;
    }
    Console.Write("{0}", note);
  }
}

高中低音类(Scale)

class Scale : Expression
{
  public override void Excute(string key, double value)
  {
    string scale = "";
    switch(Convert.ToInt32(value))
    {
      // 表示如果获得的key是O并且 value是1则演奏低音,2则是中音,3则是高音
      case 1:
        scale ="低音";      
        break;
      case 2:
        scale ="中音";
        break;
      case 3:
        scale ="高音";
        break;
    }
    Console.Write("{0]", scale);
  }
}

客户端代码

static void Main (string[] args)
{
  PlayContext context = new PlayContext();
  
  // 音乐-上海滩
  Console.WriteLine("上海滩:");
  context.PlayText = " 0 2 E 0.5 G 0.5 A 3 E 0.5 G 0.5 D 2 A 0.5 G 1 C 0.5 E 0.5 D 3 ";
  Expression expression=null;
  
  try
  {
    while (context.PlayText.Length > 0)
    {
      string str = context.PlayText.Substring(0,1);
      switch(str)
      {
        // 当首字段是О时,则表达式实例化为音阶
        case "O":        
          expression = new scale();        
          break;
          
        // 当首字母是 CDEFGAB,以及休止符P时,则实例化音符
        case "C":
        case "D":
        case "E":
        case "F":
        case "G":
        case "A":
        case "B":        
        case "P":
          expression = new Note();        
          break;
      }
      expression.Interpret(context);
    }
  }
  catch(Exception ex)
  {
    Console.WriteLine(ex.Message);
  }
  Console.Read();
}

结果显示

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

“写得非常不错,现在我需要增加一个文法,就是演奏速度,要求是T代表速度,以毫秒为单位,‘T 1000’表示每节拍一秒,‘T 500’表示每节拍半秒。你如何做?”

“学了设计模式这么久,这点感觉难道还没有,首先加一个表达式的子类叫音速。然后再在客户端的分支判断中增加一个case分支就可以了。”

音速类

class Speed : Expression
{
  public override void Excute(string key, double value)
  {
    string speed;
    if (value < 500)
    {
      speed =“快速";
    }
    else if (value >=1000)
    {
      speed ="慢速";
    }
    else
    {
      speed ="中速";
    }
    
    Console.Write("{O}", speed);
  }
}

客户端代码(局部)

......
// 增加速度的设置
context.演奏文本= "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 ";
......
switch(str)
{
  case "O":
    expression = new Scale();
    break;
  // 增加对T的判断
  case "T"":
    expression = new Speed();
    break;
  case "C":
  case "D"":
  case "E":
  case "F":
  case "G":
  case "A":
  case "B":
  case "P":
    expression = new Note();
    break;
}
......

结果显示

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

“但是小菜,在增加一个文法时,你除了扩展一个类外,还是改动了客户端。”大鸟质疑道。

“哈,这不就是实例化的问题吗,只要在客户端的switch那里应用简单工厂加反射就可做到不改动客户端了。”

“说得好,看来你的确是学明白了,在这里是讲解释器模式,也就不那么追究了,只要知道可以这样重构程序就行。其实这个例子是不能代表解释器模式的全貌的,因为它只有终结符表达式,而没有非终结符表达式的子类,因为如果想真正理解解释器模式,还需要去研究其他的例子。另外这是个控制台的程序,如果我给你钢琴所有按键的声音文件,MP3格式,你可以利用Media Player控件,写出真实的音乐语言解释器程序吗?”

“你的意思是说,只要我按照简谱编好这样的语句,就可以让电脑模拟钢琴弹奏出来?”

“是的。可以吗?”

“当然是可以,连设计模式都学得会,这点算什么。”

“OK,那我就等你哪天给我写出这样的钢琴模拟程序哦。”

“没问题。”

(注:由于钢琴模拟程序相对复杂,书中篇幅所限,代码不在书中显示,但程序功能有一定趣味性,在随书提供的源代码中有参考代码,有兴趣的读者可下载研究。)

6 料事如神

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

小菜:“大鸟,你真是料事如神呀,尽管都不是什么好消息,但两件事都让你猜对了。”

“哦,哪两件事?”

“第一,梅星离职了,说是他辞职,可听说实际上是公司叫他走人。”

“嗨,所以说呀,好好学习,加强自己的市场竞争力还是非常重要的。”

“第二,就是梅星的工作全部转给我做了,这样我的工作量大大提高,一个人干了两个人的活。”

“哈哈,你不是被老板夸得很开心的吗?现在还开心吗?”

“反正就像你说的,趁着年轻,多做点吧。”

“是的,多做点没坏处的。小菜,你在公司表现格外出色,要继续好好努力哦。”

“我怎么现在听着这话那么别扭,大鸟,你少来,我可不是职场菜鸟了,这种虚情假意的夸奖我可不再上当了。”

“真诚地夸奖你,你反而不信了。真是好心被当作了驴肝肺哦。嗨,做好人难,做好男人更难,做真心的好男人更是难上加难呀。”大鸟深情感慨地说道。

“真心好男人?呕!呕!呕!”小菜大作呕吐状。

两人的脸都笑开了花。


每天学一点,不贪多。

下一篇我们接着读“第28章 男人和女人——访问者模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论