月度归档:2011年08月

以数组形式访问对象的成员变量

在Yii框架中我们可以直接以数组的方式访问对象的成员变量,查看其源码得这些类都实现了ArrayAccess接口。 如果你想让一个类的实例可以以数组的方式访问,实现ArrayAccess接口就可以了。如下示例

class Foo implements ArrayAccess {

    private $_container = array();

    public function __construct() {
    }

    public function offsetSet($offset, $value) {
        $this->_container[$offset] = $value;
    }

    public function offsetExists($offset) {
        return isset($this->_container[$offset]);
    }

    public function offsetUnset($offset) {
        unset($this->_container[$offset]);
    }

    public function offsetGet($offset) {
        return isset($this->_container[$offset]) ? $this->_container[$offset] : NULL;
    }

}

$foo = new Foo;

$foo['test'] = 100;
echo $foo["test"];

这是官网上的一个例子,修改了一些代码,非常简单,它实现了一个类,这个类实现了ArrayAccess接口。 从而我们可以以数组的方式访问或设置值。

什么原因导致可以以这样的方式访问呢?难道仅仅是因为那个接口吗? 归根结底应该是类中的我们约定好的方法,而这些方法中只是接口的形式表现出来了。 如果我们没有实现这个接口,而仅仅某个类拥有了这些方法呢? 当程序执行时,程序会输出: Fatal error: Cannot use object of type Foo as array…

这表示实现这个接口是必须的,如果实现这个接口,那么就一定需要实现这个接口定义的所有方法。 比如我们要以数组的方式读取一个成员变量,那在PHP内核中是如何实现的呢? 通过表象看本质,这里是以数组的方式读取,那么其实现的位置应该还是在方括号符的实现位置。 以VLD查看其中间代码,我们可以得知数组读取变量的中间代码为:ZEND_FETCH_DIM_R 在此中间代码的执行过程中最终都会调用zend_fetch_dimension_address_read函数来读取值。 在这个函数中,它会依据不同容器类型做不同的处理,这些类型包括:数组,字符串、NULL、对象等。 虽然我们是以数组的方式调用对象的属性,但在放对象属性的窗口还是对象。因此,此处程序走的分支是对象, 在此分支中,对于对象,它会调用对象的read_dimension方法,默认情况下,Zend引擎的read_dimension方法默认实现是 zend_std_read_dimension函数(Zend/zend_object_handlers.c)。我们看zend_std_read_dimension函数的实现,如下:

zval *zend_std_read_dimension(zval *object, zval *offset, int type TSRMLS_DC) /* {{{ */
{
    zend_class_entry *ce = Z_OBJCE_P(object);
    zval *retval;

    /* 判断是否为ArrayAccess的子类 */
    if (instanceof_function_ex(ce, zend_ce_arrayaccess, 1 TSRMLS_CC)) {
        if(offset == NULL) {
            /* [] construct */
            ALLOC_INIT_ZVAL(offset);
        } else {
            SEPARATE_ARG_IF_REF(offset);
        }
        zend_call_method_with_1_params(&object, ce, NULL, "offsetget", &retval, offset);

        zval_ptr_dtor(&offset);

        if (!retval) {
            if (!EG(exception)) {
                zend_error(E_ERROR, "Undefined offset for object of type %s used as array", ce->name);
            }
            return 0;
        }

        /* Undo PZVAL_LOCK() */
        Z_DELREF_P(retval);

        return retval;
    } else {
        zend_error(E_ERROR, "Cannot use object of type %s as array", ce->name);
        return 0;
    }
}

从上面的代码我们可以看出:程序会先判断所给对象的类是否为ArrayAccess的子类,如果不是,则会显示错误,这在前面已经猜测,在此证实了。 如果是其子类,则调用offsetget方法获取值。

虽然我们在使用SPL时会比较简单,但是如果要开发一个SPL有时可能就没这么简单了,特别是那些有语言特性的SPL功能(如ArrayAccess),则在实现时可能就需要调用相关语言实现的代码了, 从上面看SPL与语言结构产生了较为严重的耦合,如果这个SPL要去掉,则需要修改的地方不只一处,是否有其它方案? SPL现在本来就是以扩展的形式存在于PHP中,以扩展的方式加载,却不能以扩展的方式卸载。优雅乎?

PHP的类自动加载机制

在PHP5之前,各个PHP框架如果要实现类的自动加载,一般都是按照某种约定自己实现一个遍历目录,自动加载所有符合约定规则的文件的类或函数。 当然,PHP5之前对面向对象的支持并不是太好,类的使用也没有现在频繁。 在PHP5后,当加载PHP类时,如果类所在文件没有被包含进来,或者类名出错,Zend引擎会自动调用__autoload 函数。此函数需要用户自己实现__autoload函数。 在PHP5.1.2版本后,可以使用spl_autoload_register函数自定义自动加载处理函数。当没有调用此函数,默认情况下会使用SPL自定义的spl_autoload函数。 看下面两个例子:

1、 __autoload示例:

function __autoload($class_name) {
   echo '__autload class:', $class_name, '<br />';
}

new Demo();

以上的代码在最后会输出:__autload class:Demo。
并在此之后报错显示: Fatal error: Class ‘Demo’ not found

2、spl_autoload_register示例:

function classLoader($class_name) {
    echo 'SPL load class:', $class_name, '<br />';
}

spl_autoload_register('classLoader');

new Demo();

以上的代码在最后会输出:SPL load class:Demo。
并在此之后报错显示: Fatal error: Class ‘Demo’ not found

以上的两个示例表明:当类不存在时(即需要的类不在类符号表),Zend引擎会将再调用一次用户定义的函数,如__autoload或spl_autoload_register注册的函数。 如果这两个方法同时存在,那么程序会调用哪一个呢?还是说两个都调用?看下面一个示例,你觉得会输出什么呢?

function __autoload($class_name) {
    echo '__autload class:', $class_name, '<br />';
}

function classLoader($class_name) {
    echo 'SPL load class:', $class_name, '<br />';
}

spl_autoload_register('classLoader');

new Demo();

首先我们看__autload函数。从其命名格式来看,这是一个魔术方法。 虽然__autoload和__set、__tostring等类的魔法方法的常量定义在源码级别是一起的, 可是它并不是专属于某个类的魔法方法。它是所有的类共用的自动加载魔术方法。 它将作为一个全局函数存在。那么Zend引擎是如何在类没有找到时调用这个方法的呢?

不管是使用new关键字创建类的实例,还是使用implement实现接口,或者继承某个类, 所有的这些操作都有可能调用__autoload函数。这几个操作在源码层都有一个共同点,它们在执行的时候都需要获取类的信息(接口在本质上也是一个类)。 它们在最终都会调用 zend_fetch_class (Zend/zend_execute_API.c)函数,这个函数本身没有多少内容,关键是它调用了zend_lookup_class_ex(Zend/zend_execute_API.c)函数, 这个函数就是类的自动加载的真相所在。

在zend_lookup_class_ex函数中,我们看到程序会首先查询类符号表,如果存在类直接返回。如果不存在,就会执行我们所说的自动加载了。 这里针对__autoload函数和spl相关的函数都做了处理,并且以第一参数和第二参数传递给Zend引擎的函数调用函数zend_call_function。

在zend_call_function函数中,它会判断第二参数是否存在函数,如果存在函数则只会调用第二个参数传递的函数(这里指SPL注册的函数)。 如果第二个函数没有值,则执行第一个参数传递过来的函数(这里指用户定义的__autoload函数)。 到这里,我想前面提到的两个方法同时存在的情况应该就有答案了。

关于耦合

在做程序设计时我们经常会听到“高内聚,低耦合”,这是我们追求的目标。 其中内聚是指一个模块内各个元素彼此结合的紧密程度, 高内聚是指一个模块或一个类内部各个元素之间关系紧密,争取用最少的元素和方法实现相应的功能; 耦合是指模块之间互相关联程度的度量, 而低耦合是指一个程序中各个模块之间的联系少和相互依赖程度低,一个模块只需要具体实现一个功能。 总的来说,我们所追求的设计是这样的,每个模块职责单一,一个模块只完成一个相对独立的特定子功能,并且和其他模块之间的关系简单(嗯,保持简单), 在不同的应用场景下复用这些模块,从而组成新的系统或大的模块。

在编写代码的过程中,我们应该尽量实现程序的内聚性。内聚性差的程序通常会让人产生这里什么都有,一片杂乱的景象, 特别是那种将类作为若干不同功能函数集的设计。和内聚性差一样,也会让人产生不好情绪还有高耦合。 耦合就是一些代码或一些模块对另一些代码或模块的依赖。而依赖破坏了当初的分块,严重的模块间耦合会让当初的模块化的设想付之一炬。 虽然这种耦合产生的依赖不是很好,但是我们并不能消除耦合。因为正是这种耦合,各种程序模块才能产生互操作。 所以我们只能在允许的范围内尽量减少不必要的耦合。

我们可以将耦合分为以下三类:

  1. 可以接受的耦合
  2. 不建议使用的耦合
  3. 真的不建议使用的耦合

直接看这三个分类真没啥意义,关键是这些分类中所包含的耦合实例。

可以接受的耦合

可用的耦合是指这种耦合会经常存在,而且有时也必须存在,其害处不大。如数据耦合和标记耦合。 何为 数据耦合,看这样一个例子:

function main() {
    output_name('name');
}

function output_name($name) {
    echo $name;
}
main();

这种以函数间传递参数的方式产生的函数是不可避免的,这对程序没有太大的影响。

标记耦合 是指将一个大的数据结构或类传递给另一个模块或类,而该模块只需要这个数据结构的一小部分,或只需要这个类的一个属性值。 以传递为类为例:

class Foo {
    private $_name;

    public function __construct() {
        $this->_name = __CLASS__;
    }

    public function getName() {
        return $this->_name;
    }
}

function output_name(Foo $foo) {
    echo $foo->getName();
}

output_name(new Foo());

其实我们应该是将getName()的输出直接传递给输出函数。 标记函数在一定程序上阻碍了我们对于程序的理解,因为我们需要去了解这个程序到底调用了类中的哪一个方法或使用了数据结构中的哪一个属性。

不建议使用的耦合

不建议使用的耦合是指那种我们会经常见到的不会影响整体的架构,但是有一些坏味道的代码,如控制耦合,时间耦合

控制耦合 是指一个函数传递的某个参数影响另一个函数中所作的控制决定。有时候这种实现只是为了复用一些代码。 如下示例:

function edit($is_add = 0) {
    if ($is_add) {
        //  一些需要添加的字段
    }else{
        //  一些编辑的字段
    }

    //  公共的字段
    //  保存数据
}

以上的示例只是想复用编辑和添加的操作中一些公共的东西,从而引入是否是添加操作的变量。 如果要重构以上代码,可以将一个函数分为三个函数,添加和修改各是一个,公共代码放一个函数。

时间耦合 是指代码中各个元素之间依赖于时间的关系, 以类的public方法调用为例,如果一个方法A在调用时需要先调用另一个方法B,则这里就是时间耦合。 看一个示例:

class File {

    private $_path;

    public function __construct() {

    }

    public function read() {
        echo 'read file from ', $this->_path, '<br />';
    }

    public function setPath($path) {
        $this->_path = $path;
    }
}

$file = new File();
$file->setPath(__FILE__);
$file->read();

以上是一个示例性质的文件类,在读取一个文件的文件内容之前需要先设置这个文件所在的地址。 这种形式的时间耦合应该尽量避免,如果要重构这样的代码,可以将路径作为一个变量传递给构造函数。

真的不建议使用的耦合

这类耦合一旦出现,虽然不会赤地千里,但会严重影响整体的架构和模块间的关联。如公用耦合

公用耦合 通俗的讲,公共耦合就是使用了全局变量。当两个函数或两个模块访问同一个全局变量时,它们就被公用耦合了。 在PHP中一些全局变量$_GET,$_POST等经常会在一些代码中用到,如果有两段代码都使用了这些变量并且修改了同一个值,但是双方互不相知,此时就会出BUG。

领域耦合 指的是在应用的代码中嵌入了领域或商业知识与规则,简单的讲就是将业务规则硬编码到应用中,为一些特定的业务定制应用。 这在我们的开发中会经常出现,一般会通过配置或特定的领域语言减轻系统某些部分因为耦合带来的问题。

以上的各种耦合的定义都来自《高质量程序设计艺术》一书。