【深入PHP 面向对象】读书笔记(十一) - 执行及描述任务(一) - 解释器模式

本章包括:

  • 解释器(Interpreter)模式:构造一个可以用于创建脚本化应用的迷你语言解释器。
  • 策略(Strategy)模式:在系统中定义算法并以它们自己的类型封装。
  • 观察者(Observer)模式:创建依赖关系,当有系统事件发生时通知观察者对象。
  • 访问者(Visitor)模式:在对象树的所有节点上应用操作。
  • 命令(Command)模式:创建可悲保存和传送的命令对象。

11.1 解释器模式

PHP 是由 C 语言编写的,同样,我也也可以用 PHP 定义和运行我们自己发明的编程语言。当然,我们自己创建的语言运行起来会比较慢,功能也比较有限。

11.1.1 问题

假设有一个问答程序,制作者设计问题并为回答者提交的答案制定评分规则,比如这样一个问题:《设计模式》有几个作者?

正确答案为「4」或者「four」,我们可以创建一个 Web 界面来允许命题人使用正则表达式来标记正确答案:

^4|four$

但是,大多数命题人(非编程人员)并不了解正则表达式。因此,我们可以实现一个对用户更加友好的机制来标记回应:

$input equals "4" or $input equals "four"

这里我们设计了一种支持变量、equals 操作符和布尔逻辑(or 和 and)操作符的语言。这种语言称之为 MarkLogic。

11.1.2 实现

这里写图片描述

表11-1列出了 EBNF(Extended Backus-Naur Form,扩展巴克斯范式)名称。它由一系列语句(也称部件)组成,每一条语句又由一个名称及一个描述组成。描述中包含对其他部件和终端(即本身不包含对其他部件的引用的元素)。

下面是使用 EBNF 的语法:

/* 语法  名称 ::= 描述 */
/* 第一条语句
   expr由operand和零个或多个orExpr或andExpr组成
   operand在第二条语句中解释 orExpr在第三条语句中解释 andExpr在第四条语句中解释
   expr = operand + 大于等于(orExpr/andExpr)
 */
expr ::= operand (orExpr|andExpr)*
/* 第二条语句
   operand可以是带括号的表达式,可以是字符串,也可以是eqExpr实例
   expr在第一条语句中解释 eqExpr在第五条语句中解释
   operand = (表达式) / 字符串 / eqExpr
 */
operand ::= ('('expr')'|<stringLiteral>|variable)(eqExpr)*
/* 第三条语句
   orExpr是一个由字符串'or'开头的表达式
   operand = 'or' + (表达式) / 字符串 / eqExpr
 */
orExpr ::= 'or' operand
/* 第四条语句
   andExpr是一个由字符串'and'开头的表达式
   operand = 'and' + (表达式) / 字符串 / eqExpr
 */
andExpr ::= 'and' operand
/* 第五条语句
   eqExpr是一个由字符串'equals'开头的表达式
   operand = 'equals' + (表达式) / 字符串 / eqExpr
 */
eqExpr ::= 'equals' operand
/* 第六条语句
   variable是一个由美元符'$'开头,加上一个单词的表达式
   operand = $ + 单词
 */
variable ::= '$'<word>

语法中的关系可以用类图来描述:

这里写图片描述

如图11-1,BooleanAndExpression 和它的兄弟类(BooleanOrExpression、EqualsExpression类)都继承自 OperatorExpression(操作符表达式类),这些类都是对其他 Expression 对象执行操作的。VariableExpression(变量表达式类)和LiteralExpression(字符表达式类)直接对值进行操作。

接下来我们来看整个解释器的代码实现过程,代码部分比较复杂,建议大家通读完后,回过头来好好梳理一遍,按照自己的习惯梳理出一份关系图来:
这里写图片描述

首先来看类图当中,最顶端的 InterpreterContext 类:

/* InterpreterContext文本解释类 
   用于存储Expression表达式对象的结果,包括:
      variable变量的变量名和变量值
      literal字符串常值
      equal相等表达式结果true/false
      or逻辑或表达式结果true/false
      and逻辑与表达式结果true/false
   并将其保存在$expressionstore数组中
      variable变量 保存为 $expressionstore[变量名]=变量值
      literal字符串 保存为 $expressionstore[key]=字符串常值
      equal相等 保存为 $expressionstore[key]=true/false
      or逻辑或 保存为 $expressionstore[key]=true/false
      and逻辑与 保存为 $expressionstore[key]=true/false
   其中,key是一个静态变量,从0开始自增长

   提供两个方法:
      replace()实现将表达式对象结果赋值到$expressionstore数组
      lookup()客户端能根据数组键名来查找键值,其中键名保存在具体的表达式对象中
 */
class InterpreterContext {
    private $expressionstore = array();

    /* 将value值写入特定key的expressionstore数组中
       以一个Expression对象的key(作为数组元素的键)
       以一个任意类型的值(作为数组元素的值)
     */
    function replace(Expression $exp, $value) {
        $this->expressionstore[$exp->getKey()] = $value;
    }

    /* 根据特定的key,获取expressionstore数组中存储的数据 */
    function lookup(Expression $exp) {
        return $this->expressionstore[$exp->getKey()];
    }
}

这个类简单的说,是用来解析并存储表达式的结果的。(具体的内容在注释中。)

然后我们来看类图左半部分上边的两个表达式类(Expression表达式类和 LiteralExpression字符串表达式类):

/* Expression表达式类 
   存储了一个$key和获取这个$key的方法,$key是获取存储在InterpreterContext对象中的表达式结果的钥匙
   同时定义一个抽象interpret()方法,用于子类将表达式结果存储到InterpreterContext对象中
 */
abstract class Expression {
    private static $keycount=0;
    private $key;
    /* interpret()方法将InterpreterContext对象用作共享的数据存储 */
    abstract function interpret(InterpreterContext $context);

    /* getKey()方法会被InterpreterContext::lookup()和InterpreterContext::replace()方法使用,用于获取索引        
     */
    function getKey() {
        /* 使用静态计数器的值来生成、存储和返回一个标识 */
        if (!isset($this->key)) {
            self::$keycount++;
            $this->key = self::$keycount;
        }
        return $this->key;
    }
}

/* LiterExpression字符串表达式类
   保存一个value值并通过interpret()方法,将该值写入到InterpreterContext对象中
 */
class LiterExpression extends Expression {
    private $value;

    function __construct($value) {
        $this->value = $value;
    }

    function interpret(InterpreterContext $context) {
        $context->replace($this, $this->value);
    }
}

至此,我们就构建起了一个最小表达式解释系统,做一个简单的调用:

$context = new InterpreterContext();
$literal = new LiterExpression('four');
$literal->interpret($context);
echo $context->lookup($literal); // four

通过实例化一个 InterpreterContext 和 LiteralExpression 对象,再把 InterpreterContext 对象传给 LiteralExpression::interpret(),通过 interpret() 方法在 InterpreterContext 对象中保存键值对。最后通过调用 InterpreterContext 对象的 lookup() 来获取到 value 值。

具体调用过程可以参照下图:

这里写图片描述

接下来我们再来看 VariableExpression 变量表达式类。

/* VariableExpression变量表达式类
   用于创建一组变量名和变量值 
   因为这个类的key是变量名,所以覆写了getKey(),把变量名作为key
   其他的功能和作用与LiteralExpression一样  
 */
class VariableExpression extends Expression {
    /* $name 变量名   $val 变量值*/
    private $name;
    private $val;

    function __construct($name, $val=null) {
        $this->name = $name;
        $this->val = $val;
    }

    /* 如果$val存在,则将它存入到$context中 */
    function interpret(InterpreterContext $context) {
        if (!is_null($this->val)) {
            $context->replace($this, $this->val);
            $this->val = null;
        }
    }

    function setValue($value) {
        $this->val = $value;
    }

    /* 覆盖getKey()方法,使与变量值对应的是变量名,而不是静态ID */ 
    function getKey() {
        return $this->name;
    }
}

所以这个类的创建和传值的用法会比较灵活:

/* 将一组名为input,值为four的变量$var对象
   存入到$context对象的数组$expressionstore中
   即$context对象中存在一个$expressionstore['input']='four'
 */
$context = new InterpreterContext();
$var = new VariableExpression('input', 'four');
$var->interpret($context);
/* 相当于调用 echo $expressionstore['input'] 输出four */
echo $context->lookup($var); //four

/* 新的变量只有变量名input,所以不会覆盖$expressionstore['input']='four' */
$newvar = new VariableExpression('input');
$newvar->interpret($context);
/* 相当于调用 echo $expressionstore['input'] 输出four *
echo $context->lookup($newvar); // four

/* 变量$var重新赋值,覆盖为$expressionstore['input']='five' */
$var->setValue('five');
$var->interpret($context);
/* 相当于调用 echo $expressionstore['input'] 输出five */
echo $context->lookup($var); //five
echo $context->lookup($newvar); // five

接下来我们看类图的右半部分,首先是一个抽象类 OperatorExpression(操作表达式类),这个类用于操作两个表达式结果的运算,两个表达式存储在$l_op$r_op表达式对象中,具体运算过程通过子类实现的doInterpret()方法处理。 interpret()需要首先处理存储在$l_op$r_op中的表达式对象,获取到结果,再把结果交给doInterpret()方法处理。

/* OperatorExpression操作表达式类
   用于操作两个表达式结果的运算,两个表达式存储在$l_op和$r_op表达式对象中
   具体运算过程通过子类实现的doInterpret()方法处理
   interpret()需要首先处理存储在$l_op和$r_op中的表达式对象,获取到结果,再把结果交给doInterpret()方法处理
 */
abstract class OperatorExpression extends Expression {
    /* 两个Expression对象类型的变量 */
    protected $l_op;
    protected $r_op;

    function __construct(Expression $l_op, Expression $r_op) {
        $this->l_op = $l_op;
        $this->r_op = $r_op;
    }

    function interpret(InterpreterContext $context) {
        /* 把$l_op和$r_op中的值存到$context对象的数组$expressionstore中 */
        $this->l_op->interpret($context);
        $this->r_op->interpret($context);
        /* 获取到这些值 */
        $result_l = $context->lookup($this->l_op);
        $result_r = $context->lookup($this->r_op);
        /* 调用doInterpret(),由子类来决定怎么处理这些操作结果 */
        $this->doInterpret($context, $result_l, $result_r);
    }

    protected abstract function doInterpret(InterpreterContext $context, $result_l, $result_r);
}

最后我们来看 EqualsExpression相等表达式类、BooleanOrExpression 逻辑或表达式类和 BooleanAndExpression 逻辑与表达式类。EqualsExpression相等表达式类用于检查两个表达式对象是否相等;BooleanOrExpression 逻辑或表达式类用于检测是否满足「或」关系;BooleanAndExpression 逻辑与表达式类用于检测是否满足「与」关系。并把这些运算结果通过 true/false 存储到 InterpreterContext 对象中。

/* EqualExpression(相等表达式类) */
class EqualExpression extends OperatorExpression {
    /* 检查由interpret()方法传递来的两个操作对象的结果是否相等
       并把结果true(相等)或者false(不相等)保存在InterpreterContext对象中
     */
    protected function doInterpret(InterpreterContext $context, $result_l, $result_r) {
        $context->replace($this, $result_l==$result_r);
    }
}

/* BooleanOrExpression布尔或操作类 用于检测是否满足「或」关系 */
class BooleanOrExpression extends OperatorExpression {
    /* 使用逻辑或操作 并把结果保存到$context中 */
    protected function doInterpret(InterpreterContext $context, $result_l, $result_r) {
        $context->replace($this, $result_l||$result_r);
    }
}

/* BooleanAndExpression布尔与操作类 用于检测是否满足「与」关系*/
class BooleanAndExpression extends OperatorExpression {
    /* 使用逻辑与操作 并把结果保存到$context中 */
    protected function doInterpret(InterpreterContext $context, $result_l, $result_r) {
        $context->replace($this, $result_l&&$result_r);
    }
}

至此,我们的语言就有足够的能力来处理之前的迷你语言片段:

$input equals "4" or $input equals "four"

将上面的逻辑存储到 $statement,如下:

$context = new InterpreterContext();
// 先实例一个没有赋值的'input'变量
$input = new VariableExpression('input');
$statement = new BooleanOrExpression(
    new EqualExpression($input, new LiterExpression('four'));
    new EqualExpression($input, new LiterExpression('4'));
);

对于用户输入的"four",'4','32'内容,将它们依次存入到预设好的变量$input中,将这个$input变量依次与字符串表达式new LiterExpression('four')new LiterExpression('4')做是否相等的判断,最后将两个判断结果做逻辑或的判断,即我们需要的结论。

$answer_arr = array("four",'4','32');
foreach ($answer_arr as $val) {
    $input->setValue($val);
    $statement->interpret($context);
    if($context->lookup($statement)) {
        echo "true";
    } else {
        echo "false";
    }
}

这里面的调用比较复杂,嵌套了好几层的相互调用,所以大家在阅读的过程中,需要多花点时间画个图,好好梳理一遍其中的调用逻辑,就能有比较好的理解。

这里写图片描述

最后,完善一下类图:

这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值