月度归档:2011年10月

代理模式(Proxy)和PHP的反射功能

代理模式(Proxy)和PHP的反射功能

本文包括以下内容:

  • 代理模式概述
  • 代理模式常规示例
  • 使用PHP的反射功能实现多代理

正文

模式意图 :为其他对象提供一种代理以控制对这个对象的访问[GOF95]

代理模式是对象的结构模式,代理模式给某一个对象提供一个代理对象,并由此代理对象控制对原代理对象的引用。代理模式不应该让用户感觉到代理的存在,所以代理对象和原对象的对外的调用接口是一致的。

代理模式一般包括三个角色:

  • 抽象主题角色(Subject):它的作用是统一接口。此角色定义了真实主题角色和代理主题角色共用的接口,这样就可以在使用真实主题角色的地方使用代理主题角色。
  • 真实主题角色(RealSubject):隐藏在代理角色后面的真实对象。
  • 代理主题角色(ProxySubject):它的作用是代理真实主题,在其内部保留了对真实主题角色的引用。它与真实主题角色都继承自抽象主题角色,保持接口的统一。它可以控制对真实主题的存取,并可能负责创建和删除真实对象。代理角色并不是简单的转发,通常在将调用传递给真实对象之前或之后执行某些操作,当然你也可以只是简单的转发。 与适配器模式相比:适配器模式是为了改变对象的接口,而代理模式并不能改变所代理对象的接口。

从以上三个角色我们可以得出一个简单的示例:

/**
 * 代理模式简单示例 2011-10-30 sz
 * @author phppan.p#gmail.com  http://www.phppan.com
 * @package design pattern
 */

/**
 * 抽象主题角色
 */
abstract class Subject {
    abstract public function action();
}

/**
 * 真实主题角色
 */
class RealSubject extends Subject {

    public function __construct() {
    }

    public function action() {
        echo "action method in RealSubject<br />\r\n";
    }

}

/**
 * 代理主题角色
 */
class ProxySubject extends Subject {

    private $_real_subject = NULL;

    public function __construct() {
    }

    public function action() {
        $this->_beforeAction();

        if (is_null($this->_real_subject)) {
            $this->_real_subject = new RealSubject();
        }

        $this->_real_subject->action();

        $this->_afterAction();
    }

    /**
     * 请求前的操作
     */
    private function _beforeAction() {
        echo "Before action in ProxySubject<br />\r\n";
    }

    /**
     * 请求后的操作
     */
    private function _afterAction() {
        echo "After action in ProxySubject<br />\r\n";
    }

}

/**
 * 客户端
 */
class Client {

    public static function main() {
        $subject = new ProxySubject();
        $subject->action();
    }
}

Client::main();
?>

以上的示例适用于代理模式为一个对象提供一个代理对象。当为多个对象提供代理对象时,是否我们需要创建多个代理对象呢? 如此按这种实现确实是,但是我们也可以将一个代理对象给多个对象用,或者可以作为一个代理工厂,此时一个对象会保留多个对象的引用, 并且在执行操作时需要判断所代理的是哪个对象,那么此时这种判断如何进行呢? 以多个instanceof判断对象的归属?但是这样,代理对象也必须知道真实对象的方法, 如果我们现在的需求是实现一个代理对象和真实对象的松耦合,但是在代理对象中,对于每个真实对象都有一个前置操作和后转操作, 也许我们可以使用反射,那么反射是什么呢?

反射一般来说是指在程序执行过程中获取程序相关的信息或者修改程序信息。 如获取类、方法、函数等的详细信息,或删除类方法定义。ruby的反射功能实现了类、对象、常量、变更等的获取和修改,但是在PHP中只有获取功能而无修改。 在PHP中我们可以使用PHP5以后的Reflection扩展, 此扩展是PHP的核心扩展,在PHP安装时就已经自动加载,它的作用是分析PHP程序, 导出或提取出关于类、方法、函数、属性、参数等的详细信息,包括注释。 在这里我们经常可以此扩展来实现对PHP程序内部关于类、方法等的信息检测,并做作出处理。

<?PHP
/**
 * 使用反射实现代理工厂 2011-10-30 sz
 * @author phppan.p#gmail.com  http://www.phppan.com
 * @package design pattern
 */

/**
 * 真实主题角色 A
 */
final class RealSubjectA {

    public function __construct() {
    }

    public function actionA() {
        echo "actionA method in RealSubject A <br />\r\n";
    }

}

/**
 * 真实主题角色 B
 */
final class RealSubjectB {

    public function __construct() {
    }

    public function actionB() {
        echo "actionB method in RealSubject B <br />\r\n";
    }

}

/**
 * 代理主题角色
 */
final class ProxySubject {

    private $_real_subjects = NULL;

    public function __construct() {
        $this->_real_subjects = array();
    }

    /**
     * 动态添加真实主题
     * @param type $subject
     */
    public function addSubject($subject) {
        $this->_real_subjects[] = $subject;
    }

    public function __call($name, $args) {
        foreach ($this->_real_subjects as $real_subject) {

            /* 使用反射获取类及方法相关信息  */
            $reflection = new ReflectionClass($real_subject);

            /* 如果不存在此方法,下一元素 */
            if (!$reflection->hasMethod($name)) {
                continue;
            }

            $method = $reflection->getMethod($name);

            /* 判断方法是否为公用方法并且是否不为抽象方法 */
            if ($method && $method->isPublic() && !$method->isAbstract()) {
                $this->_beforeAction();

                $method->invoke($real_subject, $args);

                $this->_afterAction();

                break;
            }
        }
    }

    /**
     * 请求前的操作
     */
    private function _beforeAction() {
        echo "Before action in ProxySubject<br />\r\n";
    }

    /**
     * 请求后的操作
     */
    private function _afterAction() {
        echo "After action in ProxySubject<br />\r\n";
    }

}

/**
 * 客户端
 */
class Client {

    public static function main() {
        $subject = new ProxySubject();

        $subject->addSubject(new RealSubjectA());
        $subject->addSubject(new RealSubjectB());

        $subject->actionA();
        $subject->actionB();
    }

}

Client::main();
?>

以上示例使用反射机制实现了对不同真实对象的代理,但是还存在一些问题:

  • 对象的显式添加,不能对用户透明
  • 真实对象的重复问题,如果存在多个相同的对象添加到对象列表中,如何处理?Flyweigh?

使用Bison和re2c构建词法分析和语法分析器

使用说明: 本文需要读者对C语言有一定的基础,对于re2c和bison有一些了解,最好也熟悉linux命令

我们在前面介绍了PHP的语法分析器-Bison入门PHP的词法解析器:re2c,那么如何将re2c与bison集成在一起的呢? 我们以一个从PHP源码中剥离出来的示例来说明整个过程。这个示例的功能与词法分析器的示例类似,作用都是识别输入参数中的字符串类型。 本示例是在其基础上添加了语法解析过程。 首先我们看这个示例的语法文件:demo.y

%{
#include <stdio.h>
#include "demo_scanner.h"
extern int yylex(znode *zendlval);
void yyerror(char const *);

#define YYSTYPE znode   //关键点一,znode定义在demo_scanner.h   
%}

%pure_parser    //  关键点二

%token T_BEGIN
%token T_NUMBER
%token T_LOWER_CHAR
%token T_UPPER_CHAR
%token T_EXIT
%token T_UNKNOWN
%token T_INPUT_ERROR
%token T_END
%token T_WHITESPACE

%%

begin: T_BEGIN {printf("begin:\ntoken=%d\n", $1.op_type);}
     | begin variable {
        printf("token=%d ", $2.op_type);
        if ($2.constant.value.str.len > 0) {
            printf("text=%s", $2.constant.value.str.val);
        }
        printf("\n");
}

variable: T_NUMBER {$$ = $1;}
|T_LOWER_CHAR {$$ = $1;}
|T_UPPER_CHAR {$$ = $1;}
|T_EXIT {$$ = $1;}
|T_UNKNOWN {$$ = $1;}
|T_INPUT_ERROR {$$ = $1;}
|T_END {$$ = $1;}
|T_WHITESPACE {$$ = $1;}

%%

void yyerror(char const *s) {
    printf("%s\n", s);
}

这个语法文件有两个关键点:

1、znode是复制PHP源码中的znode,只是这里我们只保留了两个字段,其结构如下:

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
} zvalue_value;

typedef struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    int type;    /* active type */
}zval;

typedef struct _znode {
    int op_type;
    zval constant;
}znode;

这里我们同样也复制了PHP的zval结构,但是我们也只取了关于整型,浮点型和字符串型的结构。 op_type用于记录操作的类型,constant记录分析过程获取的数据。 一般来说,在一个简单的程序中,对所有的语言结构的语义值使用同一个数据类型就足够用了。比如在前一小节的逆波兰记号计算器示例就只有double类型。 而且Bison默认是对于所有语义值使用int类型。如果要指明其它的类型,可以像我们示例一样将YYSTYPE定义成一个宏:

#define YYSTYPE znode

2、%pure_parser 在Bison中声明%pure_parse表明你要产生一个可重入(reentrant)的分析器。默认情况下Bison调用的词法分析函数名为yylex,并且其参数为void,如果定义了YYLEX_PARAM,则使用YYLEX_PARAM为参数, 这种情况我们可以在Bison生成的.c文件中发现其是使用#ifdef实现。

如果声明了%pure_parser,通信变量yylval和yylloc则变为yyparse函数中的局部变量,变量yynerrs也变为在yyparse中的局部变量,而yyparse自己的调用方式并没有改变。比如在我们的示例中我们声明了可重入,并且使用zval类型的变更作为yylex函数的第一个参数,则在生成的.c文件中,我们可以看到yylval的类型变成

一个可重入(reentrant)程序是在执行过程中不变更的程序;换句话说,它全部由纯(pure)(只读)代码构成。 当可异步执行的时候,可重入特性非常重要。例如,从一个句柄调用不可重入程序可能是不安全的。 在带有多线程控制的系统中,一个非可重入程序必须只能被互锁(interlocks)调用。

通过声明可重入函数和使用znode参数,我们可以记录分析过程中获取的值和词法分析过程产生的token。 在yyparse调用过程中会调用yylex函数,在本示例中的yylex函数是借助re2c生成的。 在demo_scanner.l文件中定义了词法的规则。大部分规则是借用了上一小节的示例, 在此基础上我们增加了新的yylex函数,并且将zendlval作为通信变量,把词法分析过程中的字符串和token传递回来。 而与此相关的增加的操作为:

SCNG(yy_text) = YYCURSOR;   //  记录当前字符串所在位置
/*!re2c
  <!*> {yyleng = YYCURSOR - SCNG(yy_text);} //  记录字符串长度

main函数发生了一些改变:

int main(int argc, char* argv[])
{
    BEGIN(INITIAL); //  全局初始化,需要放在scan调用之前
    scanner_globals.yy_cursor = argv[1];    //将输入的第一个参数作为要解析的字符串

    yyparse();
    return 0;
}

在新的main函数中,我们新增加了yyparse函数的调用,此函数在执行过程中会自动调用yylex函数。

如果需要运行这个程序,则需要执行下面的命令:

re2c -o demo_scanner.c -c -t demo_scanner_def.h demo_scanner.l
bison -d demo.y
gcc -o t demo.tab.c demo_scanner.c
chmod +x t
./t "<?php tipi2011"

相关代码下载请移步TIPI项目

PHP的语法分析器-Bison入门

Bison是一种通用目的的分析器生成器。它将LALR(1)上下文无关文法的描述转化成分析该文法的C程序。 使用它可以生成解释器,编译器,协议实现等多种程序。 Bison向上兼容Yacc,所有书写正确的Yacc语法都应该可以不加修改地在Bison下工作。 它不但与Yacc兼容还具有许多Yacc不具备的特性。

Bison分析器文件是定义了名为yyparse并且实现了某个语法的函数的C代码。 这个函数并不是一个可以完成所有的语法分析任务的C程序。 除此这外我们还必须提供额外的一些函数: 如词法分析器、分析器报告错误时调用的错误报告函数等等。 我们知道一个完整的C程序必须以名为main的函数开头,如果我们要生成一个可执行文件,并且要运行语法解析器, 那么我们就需要有main函数,并且在某个地方直接或间接调用yyparse,否则语法分析器永远都不会运行。

先看下bison的示例:逆波兰记号计算器

%{
#define YYSTYPE double
#include <stdio.h>
#include <math.h>
#include <ctype.h>
int yylex (void);
void yyerror (char const *);
%}

%token NUM

%%
input:    /* empty */
     | input line
    ;

line:     '\n'
    | exp '\n'      { printf ("\t%.10g\n", $1); }
;

exp:      NUM           { $$ = $1;           }
   | exp exp '+'   { $$ = $1 + $2;      }
    | exp exp '-'   { $$ = $1 - $2;      }
    | exp exp '*'   { $$ = $1 * $2;      }
    | exp exp '/'   { $$ = $1 / $2;      }
     /* Exponentiation */
    | exp exp '^'   { $$ = pow($1, $2); }
    /* Unary minus    */
    | exp 'n'       { $$ = -$1;          }
;
%%

#include <ctype.h>

int yylex (void) {
       int c;

/* Skip white space.  */
       while ((c = getchar ()) == ' ' || c == '\t') ;

/* Process numbers.  */
       if (c == '.' || isdigit (c)) {
       ungetc (c, stdin);
       scanf ("%lf", &yylval);
       return NUM;
     }

       /* Return end-of-input.  */
       if (c == EOF) return 0;

       /* Return a single char.  */
       return c;
}

void yyerror (char const *s) {
    fprintf (stderr, "%s\n", s);
}

int main (void) {
    return yyparse ();
}

我们先看下运行的效果:

bison demo.y
gcc -o test -lm test.tab.c
chmod +x test
./test

NOTE gcc命令需要添加-lm参数。因为头文件仅对接口进行描述,但头文件不是负责进行符号解析的实体。此时需要告诉编译器应该使用哪个函数库来完成对符号的解析。  GCC的命令参数中,-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,这里我们在-l后面接的是m,即数学库,他的库名是m,他的库文件名是libm.so。

这是一个逆波兰记号计算器的示例,在命令行中输入 3 7 + 回车,输出10

一般来说,使用Bison设计语言的流程,从语法描述到编写一个编译器或者解释器,有三个步骤:

  • 以Bison可识别的格式正式地描述语法。对每一个语法规则,描述当这个规则被识别时相应的执行动作,动作由C语句序列。即我们在示例中看到的%%和%%这间的内容。
  • 描述编写一个词法分析器处理输入并将记号传递给语法分析器(即yylex函数一定要存在)。词法分析器既可是手工编写的C代码, 也可以由lex产生,后面我们会讨论如何将re2c与bison结合使用。上面的示例中是直接手工编写C代码实现一个命令行读取内容的词法分析器。
  • 编写一个调用Bison产生的分析器的控制函数,在示例中是main函数直接调用。编写错误报告函数(即yyerror函数)。

将这些源代码转换成可执行程序,需要按以下步骤进行:

  • 按语法运行Bison产生分析器。对应示例中的命令,bison demo.y
  • 同其它源代码一样编译Bison输出的代码,链接目标文件以产生最终的产品。即对应示例中的命令 gcc -o test -lm test.tab.c