`

[转]PHP V5.3 用延后静态绑定搞活面向对象编程

    博客分类:
  • PHP
阅读更多
面向对象编程(OOP)可让开发人员通过使用数据抽象、封装、模块化、多态性和继承减少和简化代码 — 在对 OOP 有着深刻的理解的前提下。对 OOP 特性的了解还让 PHP 编码者得以利用设计模式 — 一些众所周知的用来解决常见问题的算法。PHP 自 V3.0 就已经提供了 OOP 功能,但直到 V5.3 到来时,PHP 的 OOP 实现内的怪异之处还是会阻止一些常见设计模式的使用。随着 PHP V5.3 的延后静态绑定(LSB)特性的出现,这些怪异之处均已彻底消失。

本文向您介绍了在 PHP V5.3 出现之前,存在问题的一些设计模式,解释了这些模式为何不能工作。然后展示了 PHP V5.3 的 LSB 特性,并给出了单例和活动记录设计模式。

重新回顾 OOP

如果您过去曾接触过 PHP OOP,那么很可能会出于以下原因而决定不使用它:

    读过诸多宣称 PHP OOP 有问题的博文中的一条。
    曾尝试实现一个简单的设计模式,但没有成功。

而对于 PHP V5.3,有关 OOP 的博文都是正面的,并且 PHP OOP 的问题在很大程度上已得到解决。是时候重回 PHP OOP 了。通过本文,您将看到在 V5.3 出现之前曾存在问题的一些设计模式:单例、生成器、工厂方法和活动记录。

单例、生成器和工厂方法设计模式被视为是 创建型 的模式,因它们可协助对象的构建。单例模式可能是最常用的 OOP 设计模式之一了 ;它限制了一个类的对象实例数只能为 1。比如数据库连接池就是单例设计模式的一个例子:我们一般不想让应用程序具有连接池类的多个资源密集型实例。

在需要分离复杂对象的构建和表示时,就需要用到生成器设计模式,您可以使用相同的构造过程来创建多个对象。生成器模式的实现可以很复杂,但一旦生成器可用,它就可以简化生成器所创建对象的构造和使用。具有输出 HTML、XML 或 PDF 能力的转变器就是需要使用生成器的一个例子。

而工厂方法模式,顾名思义,定义的是一个用来大量产出对象的方法的实现。您可以在应用程序需要创建其类型依赖于子类的实现的对象时,使用工厂方法模式。

活动记录模式则可用来在域类内包装关系数据库持久性方法。一个活动记录的每个实例都关系到数据库内的特定行。这个类包含了要插入、删除和更新数据库内的一行或多个行的方法。活动记录设计模式是由 Martin Fowler 在 Patterns of Enterprise Application Architecture 内定义的,并因在 Ruby on Rails 内的使用而日益流行。

回页首

前-LSB 的创建型设计模式实现问题

上述提到的所有这四个设计模式均使用了静态的属性和方法。例如,看一下清单 1 内所示的这个连接池单例。

清单 1. 一个简单的单例


<?php
class ConnPool {
    private static $onlyOne;
    private static $count = 0;
    private function __construct() {
// real-world db conn stuff here...
    }

    public static function getInstance() {
        if (!is_object(self::$onlyOne)) {
     $klass = __CLASS__;
            self::$onlyOne = new $klass();
            self::$count++;
        }
        return self::$onlyOne;
    }
    public static function getInstanceCount() {return self::$count;}


$db = ConnPool::getInstance();
assert (1 == $db->getInstanceCount());
$db2 = ConnPool::getInstance();
assert (1 == $db2->getInstanceCount());
?>


请注意这个静态的 $onlyOne 变量。该变量被设计用来保存连接池对象的一个实例。$onlyOne 之前的静态修饰符将此变量关系到类本身。$onlyOne 变量是一个类属性,因为其作用域是这个类。而 $onlyOne 属性只有一个实例。当一个属性不具有静态修饰符时,就称其是一个对象属性,因为该属性对类的每个实例都是惟一的。

注意到 ConnPool 的构造函数方法(called __construct)是空的。在一个生产实现中,可以使用该方法来创建数据库连接池的间隔。

静态 getInstance 方法包含单例的模板代码。只有在静态的 $onlyOne 变量为空时,它才会创建一个 $onlyOne 实例。请注意它是如何使用 __CLASS__ 变量来获得类的类型并随即创建该类的一个实例的。

使用 getInstanceCount 方法只是为了证明只创建了连接池的一个实例。清单 1 底部的四行代码则证明无论请求 ConnPool 池类的一个实例多少次,它都会返回相同的对象。

所以, 到目前为止,此单例一切正常 — 直到您决定想要以面向对象的继承树的形式对这个连接池进行子类处理来支持多个数据库。清单 2 显示了这个继承树(为了清晰起见,删除了实例计数器和构造函数代码)。

清单 2. 在没有 LSB 时对单例进行的一次失败尝试


<?php
class ConnPool {
    private static $onlyOne;
    protected static $klass = __CLASS__;

    public static function getInstance() {
        if (!is_object(self::$onlyOne)) {
            self::$onlyOne = new self::$klass();
        }
        return self::$onlyOne;
    }


class ConnPoolAS400 extends ConnPool {
    protected static $klass = __CLASS__;

$db = ConnPoolAS400::getInstance();
assert ('ConnPoolAS400' == get_class($db)); // fails
?>


为了支持多个类型的单例类,ConnPool 类添加了一个 $klass 静态变量并假设它会在子类中被覆盖。 ConnPoolAS400 子类扩展了 ConnPool 类并提供了 $klass 属性自己的版本。 我们的预期是当 ConnPoolAS400 类的实例创建时,$klass 属性会保存 ConnPoolAS400。但是当执行这些代码时,它不会按预期的那样运行。当 PHP 实用函数 get_class 返回 ConnPool 而不是 ConnPoolAS400 时,代码底部的声明会失败。 问题是 ConnPool 类的 getInstance 方法使用的是它自己的 $klass 属性版而非 ConnPoolAS400 的覆盖版。

清单 2 内的代码存在的问题是 self 关键字绑定到了在编译时引用的属性或方法。self 关键字指向的是包含类,且不会意识到子类。基本上,编译器会用所包含类的名称替换 self 关键字。这就类似于如下这行代码:

self::$onlyOne = new self::$klass();


被编译器替代为:

ConnPool::$onlyOne = new ConnPool::$klass();


而这就是所谓的提前绑定。而您所需要的是 延后绑定。

回页首

有了 LSB 的单例继承

您可以通过使用 PHP V5.3 的 LSB 功能修复这个单例功能。可以用 static 替换 self 指定符 self::$onlyOne = new self::$klass();:

self::$onlyOne = new static::$klass(); 


代码的重新运行的结果是一个成功的声明。

static 关键字会在可能的最近时刻强迫 PHP 绑定到代码实现。没有 LSB, self::$klass 会引用所找到的第一块代码:父类的版本。

PHP V5.3 内一个名为 get_called_class 的新功能稍稍简化了单例的代码。清单 3 用 get_called_class 函数替换了静态 $klass 属性的使用。

清单 3. 用 get_called_class 简化的单例


<?php
class ConnPool {
private static $instance;
public function get_instance() {
if (!is_object(self::$instance)) {
$klass = get_called_class();
self::$instance = new $klass();
}
return self::$instance;
}


class ConnPoolAS400 extends ConnPool {} 
$db = ConnPoolAS400::get_instance();
assert ('ConnPoolAS400' == get_class($db));
?>


清单 3 内的单例显然更为准确,但更为重要的一点是它使用了 PHP 的 LSB 来引用适当的覆盖静态类。尽管单例实现使用的是子类的类名,其他的模式(比如稍后介绍的活动记录模式)需要引用其他的静态属性。此外,LSB 可同时使用静态函数 和静态属性。静态函数与静态属性一样,作用域也是类而非该类的对象实例。清单 4 显示了使用方法而非属性来指定适当类的单例。

清单 4. 在方法上使用了 LSB 的一个单例


<?php
class ConnPool {
private static $onlyOne;
protected static function getClass() {
return __CLASS__;
}

public function get_instance() {
if (!is_object(self::$onlyOne)) {
$klass = static::getClass();
self::$onlyOne = new $klass();
}
return self::$onlyOne;
}


class ConnPoolAS400 extends ConnPool {
protected static function getClass() {
return __CLASS__;
}

$db = ConnPoolAS400::get_instance();
assert ('ConnPoolAS400' == get_class($db));
?>


在此代码中,静态 getClass 实现在 ConnPool 内定义并在 ConnPool400 内覆盖。 ConnPool 的 get_instance 方法的如下代码行会在运行时调用适当的方法:

$klass = static::getClass();


回页首

活动记录

让我们先来看看活动记录设计模式的一个简单的部分实现。清单 5 显示了一个名为 ActiveRecord 的抽象类以及两个子类:Customer 和 Sales。子类是域类,因为它们向存在于应用程序域内的实用工具提供了包装程序。

清单 5. 活动记录设计模式的简单实现


<?php
abstract class ActiveRecord {
  protected static $table;
  protected $fieldvalues;
  public $select; // used for illustration only

  static function findById($id) {
    $query = "select * from "
        .static::$table
        ." where id=$id";
    return self::createDomain($query);
  }
  function __get($fieldname) {
    return $this->fieldvalues[$fieldname];
  }
  static function __callStatic($method, $args) {
    $field = preg_replace('/^findBy(\w*)$/', '${1}', $method);
    $query = "select * from "
        .static::$table
        ." where $field='$args[0]'";
    return self::createDomain($query);
  }
  // TODO: code a __set method
  private static function createDomain($query) {
    $klass = get_called_class();
    $domain = new $klass();
    $domain->fieldvalues = array();
    $domain->select = $query;
    foreach($klass::$fields as $field => $type) {
      $domain->fieldvalues[$field] = 'TODO: set from sql result';
    }
    return $domain;
  }
  // TODO: code static create, update, delete methods
}
class Customer extends ActiveRecord {
  protected static $table = 'custdb';
  protected static $fields = array(
    'id' => 'int',
    'email' => 'varchar',
    'lastname' => 'varchar'
    );
}
class Sales extends ActiveRecord {
  protected static $table = 'salesdb';
  protected static $fields = array(
    'id' => 'int',
    'item' => 'varchar',
    'qty' => 'int'
    );
}

assert ("select * from custdb where id=123" ==
        Customer::findById(123)->select);
assert ("TODO: set from sql result" ==
    Customer::findById(123)->email);
assert ("select * from salesdb where id=321" ==
        Sales::findById(321)->select);
assert ("select * from custdb where Lastname='Denoncourt'" ==
    Customer::findByLastname('Denoncourt')->select);
?>


ActiveRecord 类使用 abstract 修饰符来确保代码不会实例化一个 ActiveRecord 对象。如果用 new ActiveRecord(); 尝试创建一个 ActiveRecord,将会收到一个错误,称 “PHP Fatal error: Cannot instantiate abstract class ActiveRecord”。这是一件好事,因为在没有子类时,ActiveRecord 类不会做任何有价值的事情。

ActiveRecord 类定义一个静态的 $table 变量,它会相继被 Customer 和 Sales 子类覆盖来指定 SQL 表名 custdb 和 salesdb。

ActiveRecord 的静态 findById 函数是活动记录设计模式的实现内常见的一个方法的例子。findById 负责基于所传递的惟一标识符来检索数据库内的适当行,然后再构建并返回代表业务实体的域对象。 findById 方法使用 static 关键字来启用对子类的表名的延后绑定引用。此方法构建一个 SQL select,然后会将域的创建延迟到 createDomain 方法。

createDomain 方法使用子类的名称(通过 PHP V5.3 的 get_called_class 函数)来实例化适当的类。createDomain 方法之后会创建一个数组来保存数据库列的名称和值的一个映射。为了让这个示例尽量简单,ActiveRecord 并不会实际运行 SQL 代码。同时为了让本文的代码能够充分展示并测试 SQL select 的构造,ActiveRecord 具有一个在 createDomain 内设置的 $select 属性。foreach 语句,不是设置来自 SQL 结果集的域属性值,而是将字符串 TODO: set from sql result 填塞到字段值数组的每个元素。这个方法会返回新构建的域,而这个域又会由 findById 方法返回。代码底部四个声明中的第一个声明会验证适当的 SQL 语句是否被创建。

回页首

动态属性和 __get

您可能会注意到的 Customer 和 Sales 域名类的一个奇怪的事情是它们并没有任何的域属性。您可能会期望看到如下这些行,作为 Customer 类的属性:

$id;
$email;
$name;


这样一来,您就可以使用以下代码访问这些域属性:

$custObj->id;
$custObj->email;
$custObj->lastname;


但这些属性是可用的;它们保存在 $fieldvalues 数组内。并且其上的代码也能正常工作。为了提供对域属性的无缝访问以便上述引用的语法能正常工作,ActiveRecord 实现了奇妙的 __get 方法。代码底部的第二个声明显示了 Customer 对象是如何从 findById 方法被检索到的,以及 email 属性是如何被访问的。Customer 没有 email 属性,但是由于定义了 __get 方法,PHP 调用了 __get 方法,并以一个参数传递了所请求的属性名。之后,__get 方法只需简单地从 $fieldvalues 数组拉出属性值。

注意: 生产代码可以处理对数组内不存在的属性的请求。

回页首

动态 finder 方法和 __callStatic

活动记录设计模式实现内经常提供的一个很棒的特性是动态 finder 方法。没有动态方法,域类的代码编写将必须考虑到类的每个数据库查询用户可能会要求检索域的一个或多个实例(并随后编写一个类似于 findById 的方法)。

我们来重点看看 清单 5 底部的最后一个声明:它运行一个名为 findByLastname 的方法。但该方法不具备任何实现。此方法可能不会存在,但是由于 Customer 类的父类具有 PHP V5.3 的新 __callStatic 方法的一个实现,所以不仅不会抛出任何错误,而且 findByLastname 调用还会实际执行并会做一些有价值的事情。

__callStatic 方法接受两个参数:被调用的方法名以及一个参数数组。当调用 findByLastname 时,PHP 看到此名称不存在并会运行 ActiveRecord 的 __callStatic 方法,在第一个参数内传递 findByLastname,第二个参数内传递 Denoncourt 作为一个数组。 __callStatic 的 ActiveRecord 实现去掉 findBy 前缀后跟的字符串并将其用作 SQL where 子句内的字段名。 __callStatic 方法然后会使用 Denoncourt 实参作为对比值。有了动态调用,还可以使用如下这行代码:

Customer::findByEmail('dondenoncourt@gmail.com');


注意您还可以增进 __callStatic 方法来支持操作符,如下所示:

Sales::findAllByQtyGreaterThan(100);


显然,活动记录的生产实现将远比此更为复杂并会处理除 findBy 外的一些方法前缀。这些方法前缀可能会包含 findAllBy 来返回行的数组,使用 countBy 来返回与条件相匹配的那些行。

注意: 现在已经出现了一些利用 PHP V5.3 新特性的活动记录框架 — Dirivante 和 php.activerecord —(参考资料)
分享到:
评论

相关推荐

    php5.3后静态绑定用法详解

    php5.3后引入了后静态绑定功能,这一功能主要用于解决在继承范围内引用静态调用类的问题。在静态方法调用时,类名是在::运算符左侧部分明确指定的;而在非静态方法调用时,类名则是所属对象的类名。所谓"转发调用",...

    蓝牙核V5.3 Core_v5.3

    蓝牙核心规范V5.3 蓝牙核心规范V5.3是最新的蓝牙协议版本,发布于2021年7月13日。该规范定义了创建互操作蓝牙设备所需的技术。下面是蓝牙核心规范V5.3的详细知识点: 一、蓝牙技术概述 蓝牙技术是一种无线个人...

    STEP 7 V5.3编程手册.pdf

    ### STEP 7 V5.3 编程手册关键知识点概览 #### 一、产品介绍与软件安装(第1章) - **产品简介**:首先介绍了STEP 7 V5.3这一版本的主要特点及其适用范围。它是一款由西门子公司开发的专业编程软件,主要用于...

    step7v5.3编程手册

    ### STEP7 V5.3 编程手册知识点详解 #### 一、STEP7 V5.3 软件概述 **重要性:** STEP7 V5.5是SIMATIC S7系列PLC的主要编程软件之一,广泛应用于工业自动化领域。 **知识点:** - **安装与配置:** - 安装过程需要...

    STEP 7 V5.3 编程手册.rar

    《STEP 7 V5.3 编程手册》是西门子SIMATIC自动化系统中用于编程的权威参考资料,尤其对于使用STEP 7 V5.3软件进行S7系列PLC编程的工程师而言,该手册是不可或缺的学习和工作宝典。这份压缩包包含的PDF文档可能详尽地...

    SIMATIC STEP 7 V5.3使用入门.PDFSIMATIC STEP 7 V5.3使用入门.PDF

    SIMATIC STEP 7 V5.3使用入门.PDFSIMATIC STEP 7 V5.3使用入门.PDFSIMATIC STEP 7 V5.3使用入门.PDF

    STEP 7V5.3 SP2中文版.rar

    STEP 7V5.3 SP2中文版rar,STEP 7V5.3 SP2中文版

    最新STEP 7 V5.3 使用入门

    ### 最新STEP 7 V5.3 使用入门详解 #### 一、STEP 7 V5.3概述 **标题和描述解析:** 标题与描述强调了文档的主题是关于西门子(Siemens)的STEP 7 V5.3版本的入门指南。STEP 7是Siemens为SIMATIC S7系列PLC(可...

    蓝牙core-v5.3中英文对照版本

    【蓝牙Core_v5.3】是蓝牙技术联盟(Bluetooth SIG)发布的核心规范,该规范详细定义了构建可互操作蓝牙设备所需的技术标准。版本v5.3是在2021年7月13日更新的,是蓝牙技术的最新版本之一,主要关注提升性能和增强...

    西门子SIMATIC 使用 STEP 7 V5.3 编程手册[课件].swf

    西门子SIMATIC 使用 STEP 7 V5.3 编程手册[课件]swf,该手册完整概述了如何使用 STEP 7 编程。它设计用于在安装和调试该软件时为您提供支持。它解释了如何创建程序并描述了用户程序的组件。 该手册旨在用于供使用 ...

    S7-Plcsim V5.3

    本文将详细探讨S7-PLCSIM V5.3这一仿真软件及其在与Step7 V5.3配合使用时,如何进行离线调试PLC程序以及与WinCC6.0的集成应用。 首先,S7-PLCSIM V5.3是西门子提供的一款强大的PLC仿真软件,它允许用户在不连接实际...

    桃源网盘v5.3桃源网盘v5.3

    桃源网盘v5.3桃源网盘v5.3桃源网盘v5.3桃源网盘v5.3

    WinISO V5.3(正版注册码)

    WinISO V5.3(正版注册码)WinISO V5.3(正版注册码)WinISO V5.3(正版注册码)WinISO V5.3(正版注册码)WinISO V5.3(正版注册码)WinISO V5.3(正版注册码)WinISO V5.3(正版注册码)

    S7-SCL编程手册 v5.3(S7-SCL v5.3)

    《S7-SCL编程手册 v5.3》是专为S7-300/400 PLC编程设计的一份详尽指南,旨在提供S7-SCL编程的全面概述。S7-SCL是一种基于文本的编程语言,适用于西门子的SIMATIC S7系列PLC。这份390页的手册不仅涵盖了S7-SCL的安装...

    Core_v5.3 蓝牙5.3核心规范(中文版和英文版)

    **蓝牙5.3核心规范详解** 蓝牙5.3是蓝牙技术联盟(Bluetooth Special Interest Group, SIG)发布的...对于想要深入研究蓝牙技术的人来说,中文版和英文版的《Core_v5.3 蓝牙5.3核心规范》文档是不可或缺的参考资料。

    PHP 5.3入门经典(所有的源码)

    2. **晚期静态绑定(Late Static Bindings)**:在继承链中调用静态方法时,早期版本的PHP会绑定到定义该方法的类,而PHP 5.3引入的晚期静态绑定允许在运行时绑定到实际调用静态方法的类,这对于多层继承的静态方法...

    Raize.v5.3

    2. **设计时支持**:Raize.v5.3 提供了设计时界面,允许你在 Delphi 的对象浏览器中查看和选择控件,然后直接拖放到表单上进行布局和设置属性。 3. **API 和事件**:了解每个控件的 API 和事件是至关重要的。API ...

    S7-Plcsim_V5.3_SP2

    在使用S7-Plcsim_V5.3_SP2时,用户需要熟悉西门子的TIA Portal环境,这是西门子统一的工程组态和编程平台。同时,了解S7-300 PLC的硬件结构和编程原理也至关重要,这包括了解各种I/O模块的功能以及如何在模拟环境中...

    S7-300 V5.3软件+KEY(04)

    S7-300 V5.3软件+KEY ZZZZZZZZZZZZZZZZZZZZZZZZZZ

    (中文版+英文版)Core-v5.3 蓝牙5.3核心规范

    提供的两个PDF文件——`Core_v5.3.pdf`和`BT_Core_v5.3-中文.pdf`,分别是英文版和中文版的蓝牙5.3核心规范文档,是深入理解蓝牙技术的基础资料。读者可以通过这些文档,详细了解蓝牙5.3的各个方面,包括协议栈结构...

Global site tag (gtag.js) - Google Analytics