本章包括:
- 解释器(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";
}
}
这里面的调用比较复杂,嵌套了好几层的相互调用,所以大家在阅读的过程中,需要多花点时间画个图,好好梳理一遍其中的调用逻辑,就能有比较好的理解。
最后,完善一下类图: