1面向对象思想的核心概念1 PHP核心技术与最佳实践

正文开始

1.1  --  1.2.2

这是一本致力于为希望成为中高级PHP程序员的读者提供高效而有针对性指导的经典著作。本书系统归纳和深刻解读了PHP开发中的编程思想、底层原理、核心技术、开发技巧、编码规范和最佳实践。

    全书分为5个部分:第一部分(1~2章)从不同的角度阐述了面向对象软件设计思想的核心概念、技术和原则,分析了面向对象的特性、设计模式的理念,指出了如何设计低耦合、高可扩展性的软件,等等;第二部分(3~6章)详细讲解了PHP中正则表达式的规范和使用技巧,PHP网络编程的原理、方法、技巧和一些重要的操作,PDO、数据库应用优化,数据库设计和MySQL的高级应用,PHP扩展引擎的原理与实践;第三部分(第7章)拨云见日,围绕PHP扩展开发进行了细致而深入的探讨,解析了PHP的底层实现和Zend虚拟机API,并用PHP扩展开发的实例带领读者走进PHP的底层世界,旨在让读者对PHP性能优化、底层原理进行深入的理解。第四部分(8~11章)重点讨论了缓存的设计、Memcached的原理与实践、NoSQL数据库Redis源码分析与应用实践、高性能PHP网站的架构和设计等内容;第五部分(12~14章)详细讲解了PHP代码的调试和测试、Hash算法和数据库的实现,以及PHP的编码规范,旨在帮助读者提高开发效率,养成良好编程习惯。


1.1 面向对象的“形”与“本”

    类是对象的抽象组织,对象是类的具体存在。

    2200年前的战国时期,赵国平原君的食客公孙龙在骑着白马进城时,被守城官以马不能入城拦下,公孙龙即兴演讲,口述“白马非马”一论,守城官无法反驳,于是公孙龙就骑着他的白马(不是马的)进城去了。这就是历史上最经典的一次对面向对象思维的阐述。

    公孙龙的“白马非马”论如下:

    “白马非马”,可乎?曰:“可。”曰“何哉?”曰:“马者,所以命形也;白者,所以命色也。命色者非命形也。故曰:‘白马非马’。”曰:“有白马不可谓无马也。不可谓无马者,非马也?有白马为有马,白之,非马何也?”曰:“求马,黄、黑马皆可致;求白马,黄、黑马不可致。使白马乃马也,是所求一也。所求一者,白者不异马也。所求不异,如黄、黑马有可有不可,何也?可与不可,其相非明。故黄、黑马一也,而可以应有马,而不可以应有白马,是白马之非马,审矣!”

    公孙龙乃战国时期的“名家”,名家的中心论题是所谓“名”(概念)和“实”(存在)的逻辑关系问题。名者,抽象也,类也。实者,具体也,对象也。从这个角度讲,公孙龙是我国早期的最著名的面向对象思维的思想家。

    “白马非马”这一论断的关键就在于“非”字,公孙龙一再强调白马与马的特征,通过把白马和马视为两个不同的类,用“非”这一关系,成功地把“白马”与“马”的关系由从属关系转移到“白马”这个对象与“马”这个对象的相等关系上,显然,二者不等,故“白马非马”。而我们正常的思维是,马是一个类,白马是马这个类的一个对象,二者属于从属关系。说“白马非马”,就是割裂马与白马之间的从属关系,偷换概念,故为诡辩也。

    白马非马这个典故,我们可以称之为诡辩。但我们把这个问题抽象出来,实际上讨论的就是类与类之间的界定、类的定义等一系列问题,类应该抽象到什么程度,其中即涉及了类与对象的本质问题,也涉及了类的设计过程中的一些原则。
1.1.1 对象的“形”

        要回答类与对象本质这个问题,我想可以先从“形”的角度来回答。本节以PHP为例,来探讨对象的“形”与“本”的问题。

        类是我们对一组对象的描述。

        在PHP里,每个类的定义都以关键字class开头,后面跟着类名,紧接着一对花括号,里面包含有类成员和方法的定义。如下面代码所示:

        class person{

        public $name;

        public $gender;

        public function say(){

        echo $this->name,"is ",$this->gender;

        }

        }

        在这里,我们定义了一个person类。代表了抽象出来的人这个概念,它含有姓名和性别这两个属性,还具有一个开口说话的方法,这个方法会告诉外界这个人的性别和姓名。我们接下来就可以产生这个类的实例:

        $student=new person();

        $student->name='Tom';

        $student->gender='male';

        $student->say();

        $teacher=new person();

        $teacher->name='Kate';

        $teacher->gender='female';

        $teacher->say();

        这段代码则实例化了person类,产生了一个student对象和teacher对象的实例。实际上也就是从抽象到具体的过程。现实世界中,仅仅说“人”是没有意义的,中国人把它叫“人”,美国人把它叫person或者human,如果高兴,把它叫“God”或者“板凳”都无所谓。但是只要你把“人”这个概念加上各种属性和方法,比如说有两条腿、直立行走、会说话,则无论是中国人,还是美国人,甚至外星人都是能理解你所描述的事物。所以,一个类的设计需要能充分展示其最重要的属性和方法,并且能与其他事物相区分。只有类本身有意义,从抽象到具体的实例化才会有意义。

        根据上面的实例代码,可以有下面的一些理解:

        类定义了一系列的属性和方法,并提供了实际的操作细节,这些方法可以用来对属性进行加工。

        对象含有类属性的具体值,这就是类的实例化。正是由于属性的不同,才能区分不同的对象。在上面例子里,由于student和teacher的性别和姓名不一样,才得以区分开二人。

        类与对象的关系类似一种服务与被服务、加工与被加工的关系,具体而言,就如同原材料与流水线的关系。只需要在对象上调用类中所存在的方法,就可以对类的属性进行加工,并且展示其功能。

        类是属性和方法的集合,那么在PHP里,对象是什么呢?比较普遍的说法就是“对象由属性和方法组成”。对象是由属性组成,这很好理解,一个对象的属性是它区别于另一个对象的关键所在。由于PHP的对象是用数组来模拟的,因此我们把对象转为数组,就能看到这个对象所拥有的属性了。

        继续使用上面代码,可以打印student对象:

        print_r((array)$student);

        var_dump($student);

        到这里,可以很直观地认识到,对象就是一堆数据。既然如此,可以把一个对象存储起来,以便需要时用。这就是对象的序列化。

        所谓序列化,就是把保存在内存中的各种对象状态(属性)保存起来,并且在需要时可以还原出来。下面的代码实现了把内存中的对象当前状态保存到一个文件中:

        $str=serialize($student);

        echo $str;

        file_put_contents('store.txt', $str);

        输出序列化后的结果:

        O:6:"person":2:{s:4:"name";s:3:"Tom";s:6:"gender";s:4:"mail";}

        在需要时,反序列化取出这个对象:

        $str = file_get_contents('store.txt');

        $student = unserialize($str);

        $student->say();

        注意 在序列化和反序列化时都需要包含类的对象的定义,不然有可能出现在反序列化对象时,找不到该对象的类的定义,而返回不正确的结果。

        可以看到,对象序列化后,存储的只是对象的属性。类是由属性和方法组成的,而对象则是属性的集合,由同一个类生成的不同对象,拥有各自不同的属性,但共享了类的代码空间中方法区域的代码。


1.1.2 对象的“本”

    我们需要更深入地了解这种机制,看对象的“本”。对象是什么?对象在PHP中也是变量的一种,所以先看PHP源码中对变量的定义:

    #zend/zend.h

    typedef uni on _zvalue_value {

    long lval;/*long value */

    double dval;/*double value */

    struct {

    char *val;

    int len;

    } str;

    HashTable *ht; /*hash table value */

    zend_object_value obj;

    } zvalue_value;

    zvalue_value,就是PHP底层的变量类型,zend_object_value obj就是变量中的一个结构。接着看对象的底层实现。

    在PHP5中,对象在底层的实现是采取“属性数组+方法数组”来实现的。可以简单地理解为PHP对象在底层的存储如图1-1所示。


     

    对象在PHP中是使用一种zend_object_value结构体来存储的。对象在ZEND(PHP底层引擎,类似Java的JVM)中的定义如下:

    #zend/zend.h

    typedef struct _zend_object {

    zend_class_entry *ce;  // 这里就是类入口

    HashTable *properties;// 属性组成的HashTable

    HashTable *guards; /*protects from get/set ...recursion */

    } zend_object;

    ce是存储该对象的类结构,在对象初始化时保存了类的入口,相当于类指针的作用。properties是一个HashTable,用来存放对象属性。guards用来阻止递归调用。

    类的标准方法在zend/zend_object_handlers.h文件中定义,具体实现则是在zend/zend_object_handlers.c文件中。关于PHP变量的存储结构的底层实现,将在第7章中进行更深入的介绍。

    通过对上述源代码的简单阅读,可以更清晰地认识到对象也是一种很普通的变量,不同的是其携带了对象的属性和类的入口。

1.1.3 对象与数组

    对象是什么,我们不好理解,也不容易回答,但是我们知道数组是什么。数组的概念比较简单。可以拿数组和对象对比来帮助我们理解对象。对象转化为数组,数组也能转换成对象。数组是由键值对数据组成的,数组的键值对和对象的属性/属性值对十分相似。对象序列化后和数组序列化后的结果是惊人的相似。如下面的代码所示:

    $student_arr=array('name'=>'Tom','gender'=>'male');

    echo "\n";

    echo serialize($student_arr);

    输出为:

    a:2:{s:4:"name";s:3:"Tom";s:6:"gender";s:4:"male";}

    可以很清楚地看出,对象和数组在内容上一模一样!

    而对象和数组的区别在于:对象还有个指针,指向了它所属的类。在对student对象序列化时,我们看到了“person”这几个字符,这个标识符就标志了这个对象归属于person类,故在取出这个对象后,可以立即对其执行所包含的方法。如果对象中还包含对象呢?我们来看下一节的内容。

1.1.4 对象与类

    在前面代码中定义了一个类,并创建了这个类的对象,把前面产生的对象作为这个新对象的一个属性,完整代码如代码清单1-1所示。

    代码清单1-1 object.php

    <?php

    class person{

    public $name;

    public $gender;

    public function say(){

    echo $this->name," \tis ",$this->gender,"\r\n";

    }

    }

    class family{

    public $people;

    public $location;

    public function construct($p,$loc){

    $this->people=$p;

    $this->location=$loc;

    }

    }

    $student=new person();

    $student->name='Tom';

    $student->gender='male';

    $student->say();

    $tom=new family($student,'peking');

    echo serialize($student);

    $student_arr=array('name'=>'Tom','gender'=>'male');

    echo "\n";

    echo serialize($student_arr);

    print_r($tom);

    echo "\n";

    echo serialize($tom);

    输出结果如下:

    Tom is male

    O:6:"person":2:{s:4:"name";s:3:"Tom";s:6:"gender";s:4:"male";}

    a:2:{s:4:"name";s:3:"Tom";s:6:"gender";s:4:"male";}

    family Object

    (

    [people] => person Object

    (

    [name] => Tom

    [gender] => male

    )

     

    [location] => peking

    )

    O:6:"family":2:{s:6:"people";O:6:"person":2:{s:4:"name";s:3:"Tom";s:6:"gender";s:4:"male";}s:8:"location";s:6:"peking";}

    可以看出,序列化后的对象会附带所属的类名,这个类名保证此对象能够在执行类的方法(也是自己所能执行的方法)时,可以正确地找到方法所在的代码空间(即对象所拥有的方法存储在类里)。另外,当一个对象的实例变量引用其他对象时,序列化该对象时也会对引用对象进行序列化。

    基于如上的分析,可以总结出对象和类的概念以及二者之间的关系:

    类是定义一系列属性和操作的模板,而对象则把属性进行具体化,然后交给类处理。

    对象就是数据,对象本身不包含方法。但是对象有一个“指针”指向一个类,这个类里可以有方法。

    方法描述不同属性所导致的不同表现。

    类和对象是不可分割的,有对象就必定有一个类和其对应,否则这个对象也就成了没有亲人的孩子(但有一个特殊情况存在,就是由标量进行强制类型转换的object,没有一个类和它对应。此时,PHP中一个称为“孤儿”的stdClass类就会收留这个对象)。

    理解了以上四个概念,结合现实世界从实现和存储理解对象和类,这样就不会把二者看成一个抽象、神秘的东西,也就能写出符合现实世界的类了。

    如果需要一个类,要从客观世界抽象出一套规律,就得总结这类事物的共性,并且让它可以与其他类进行区分。而这个区分的依据就是属性和方法。区分的办法就是实例化出一个对象,是骡子是马,拉出来遛遛。

    现在,你是否对“白马非马”这个典故有了新的认识?

1.2 魔术方法的应用

    魔术方法是以两个下画线“”开头、具有特殊作用的一些方法,可以看做PHP的“语法糖”。

    语法糖指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。语法糖往往给程序员提供了更实用的编码方式或者一些技巧性的用法,有益于更好的编码风格,使代码更易读。不过其并没有给语言添加什么新东西。PHP里的引用、SPL等都属于语法糖。

    实际上,在1.1节代码中就涉及魔术方法的使用。family类中的construct方法就是一个标准魔术方法。这个魔术方法又称构造方法。具有构造方法的类会在每次创建对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。因此,这个方法往往用于类进行初始化时执行一些初始化操作,如给属性赋值、连接数据库等。

    以代码清单1-1所示代码为例,family中的construct方法主要做的事情就是在创建对象的同时对属性赋值。也可以这么使用:

    $tom=new family($student,'peking');

    $tom->people->say();

    这样做就不需要在创建对象后再去赋值了。有构造方法就有对应的析构方法,即destruct方法,析构方法会在某个对象的所有引用都被删除,或者当对象被显式销毁时执行。这两个方法是常见也是最有用的魔术方法。

1.2.1 set和get方法

    set和get是两个比较重要的魔术方法,如代码清单1-2所示。

    代码清单1-2 magic.php
      
    <?php

    class Account{

    private $user=1;

    private $pwd=2;

    }

    $a=new Account();

    echo $a->user;

    $a->name=5;

    echo $a->name;

    echo $a->big;

    运行这段代码会怎样呢?结果报错如下:

    Fatal error: Cannot access private property Account::$user in G:\bak\temp\tempcode\sg.php on line 7

    所报错误大致是说,不能访问Account对象的私有属性user。在代码清单1-2的类定义里增加以下代码,其中使用了set魔术方法。

    public function set($name, $value) {

    echo "Setting $name to $value \r\n";

    $this->$name= $value;

    }

     public function get($name) {

    if(!isset($this->$name)){

    echo '未设置';

    $this->$name="正在为你设置默认值";

    }

    return $this->$name;

    }

    再次运行,看到正常输出,没有报错。在类里以两个下画线开头的方法都属于魔术方法(除非是你自定义的),它们是PHP中的内置方法,有特殊含义。手册里把这两个方法归到重载。

    PHP的重载和Java等语言的重载不同。Java里,重载指一个类中可以定义参数列表不同但名字相同的多个方法。比如,Java也有构造函数,Java允许有多个构造函数,只要保证方法签名不一样就行;而PHP则在一个类中只允许有一个构造函数。

    PHP提供的“重载”指动态地“创建”类属性和方法。因此,set和get方法被归到重载里。

    这里可以直观看到,若类中定义了set和get这一对魔术方法,那么当给对象属性赋值或者取值时,即使这个属性不存在,也不会报错,一定程度上增强了程序的健壮性。

    我们注意到,在account类里,user属性的访问权限是私有的,私有属性意味着这个属性是类的“私有财产”,只能在类内部对其进行操作。如果没有set这个魔术方法,直接在类的外部对属性进行赋值操作是会报错的,只能通过在类中定义一个public的方法,然后在类外调用这个公开的方法进行属性读写操作。

    现在有了这两个魔术方法,是不是对私有属性的操作变得更方便了呢?实际上,并没有什么奇怪的,因为这两个方法本身就是public的。它们和在对外的public方法中操作private属性的原理一样。只不过这对魔术方法使其操作更简单,不需要显式地调用一个public的方法,因为这对魔术方法在操作类变量时是自动调用的。当然,也可以把类属性定义成public的,这样就可以随意在类的外部进行读写。不过,如果只是为了方便,类属性在任意时候都定义成public权限显然是不合适的,也不符合面向对象的设计思想。

1.2.2 call和callStatic方法

如何防止调用不存在的方法而出错?一样的道理,使用call魔术重载方法。

call方法原型如下:

mixed call ( string $name , array $arguments )

当调用一个不可访问的方法(如未定义,或者不可见)时,call()会被调用。其中$name参数是要调用的方法名称。$arguments参数是一个数组,包含着要传递给方法的参数,如下所示:

public function call($name, $arguments) {

switch(count($arguments)){

case 2:

echo $arguments[0]*$arguments[1],PHP_EOL;

break;

case 3:

echo array_sum($arguments),PHP_EOL;

break;

default:

echo '参数不对',PHP_EOL;

break;

}

}

$a->make(5);

$a->make(5,6);

以上代码模拟了类似其他语言中的根据参数类型进行重载。跟call方法配套的魔术方法是callStatic。当然,使用魔术方法“防止调用不存在的方法而报错”,并不是魔术方法的本意。实际上,魔术方法使方法的动态创建变为可能,这在MVC等框架设计中是很有用的语法。假设一个控制器调用了不存在的方法,那么只要定义了call魔术方法,就能友好地处理这种情况。

试着理解代码清单1-3所示代码。这段代码通过使用callStatic这一魔术方法进行方法的动态创建和延迟绑定,实现一个简单的ORM模型。

代码清单1-3 simpleOrm.php

<?php

abstract class ActiveRecord {

protected static $table;

protected $fieldvalues;

public $sele ct;

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 = "sel ect *from "

.static::$table

." where $field='$args[0]'";

return self::createDomain($query);

}

private static function createDomain($query) {

$klass = get_called_class();

$domain = new $klass();

$domain->fieldvalues = array();

$domain->sele ct = $query;

foreach($klass::$fields as $field => $type) {

$domain->fieldvalues[$field] = 'TODO: set from sql result';

}

return $domain;

}

}

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);

再举个类似的例子。PHP里有很多字符串函数,假如要先过滤字符串首尾的空格,再求出字符串的长度,一般会这么写:

strlen(trim($str));

如果要实现JS里的链式操作,比如像下面这样,应该怎么实现?

$str->trim()->strlen()

很简单,先实现一个String类,对这个类的对象调用方法进行处理时,触发call魔术方法,接着执行call_user_func即可。



正文结束

PHP接口(interface)和抽象类(abstract) 第2章 面向对象的设计原则