Home >Backend Development >PHP Tutorial >开始了解 PHP V5 中的对象_PHP

开始了解 PHP V5 中的对象_PHP

WBOY
WBOYOriginal
2016-06-01 12:30:06834browse

  本文描述 PHP V5 中对象和类的基础知识,从最基本的概念一直讲到继承,主要针对经验丰富的面向对象程序员和尚未接触过对象的读者。

  作为 PHP 程序员,您肯定知道变量和函数。但类和对象可能就是另一回事。不定义单个类,就可以创建完美的系统。但即使您决定在自己的代码中不使用面向对象的编程,您仍可能需要了解面向对象的编程。例如,如果使用第三方库,比如通过 PHP Extension and Application Repository (PEAR) 可以使用的库,您将发现自己在实例化对象和调用方法。

  什么是类和对象?

  简单地说,类 是一个由变量和方法组成的独立块或束。这些组件通常结合实现单个责任或一组责任。在本文中,您将创建一个类,该类收集了用于查询和填充由项和值组成的词典的方法。

  类可以直接用作组织数据和功能的简单方法,就像一组函数和变量一样。但使用类可以忽略它的存在。类可用于在内存中生成多个实例。这样的实例叫做对象。每个对象可以访问一组相同的函数(在面向对象上下文中叫做方法)和变量(叫做特性或实例变量),但每个变量的实际值在每个对象中是不同的。

  考虑角色扮演游戏中的一个单元——比如坦克。类可能为坦克设置一组变量:防御和进攻能力,范围,健康状况,等等。该类也可能定义一组函数,其中包括 move() 和 attack()。当系统包含一个坦克类时,该类可用于生成数十个或数百个坦克对象,每个对象都潜在地具有自己的健康状况或范围特征。因此,类是用于生成对象的蓝图或模板。

  理解类和对象最简单的方法可能就是创建一些类和对象。

  第一个类

  可以用 class 关键字创建类。最简单的情况是,类由关键字类、名称和代码块组成:

class Dictionary {
}

  类名可以包含字母、数字和下划线字符的任何组合,但不能以数字打头。

  上例中的 Dictionary 类尽管用处有限,但完全合法。那么如何使用该类来创建一些对象呢?

$obj1 = new Dictionary();
$obj2 = new Dictionary();
$obj3 = new Dictionary();

  至少在形式上,实例化对象与调用函数相似。对于函数调用,必须提供圆括号。与函数一样,一些类需要您为其传递参数。您还必须使用 new 关键字。这就告诉 PHP 引擎您希望实例化一个新对象。然后,返回的对象可以存储在一个变量中以供将来使用。

  属性

  在类的主体中,可以声明叫做属性的特殊变量。在 PHP V4 中,属性必须用关键字 var 调用。这仍是合法的语法,但主要是为了向后兼容。在 PHP V5 中,属性必须声明为 public、private 或 protected。可以在 关键字:在此我们是否可以有一点隐私?中阅读有关这些限定词的内容。但现在在例子中将所有属性声明为 public。清单 1 显示一个声明了两个属性的类。

  清单 1. 声明两个属性的类

class Dictionary {
public $translations = array();
public $type ="En";
}

  正如所看到的,可以同时声明属性并为其赋值。可以用 print_r() 函数快速浏览一下对象的状态。清单 2 显示 Dictionary 对象现在具有更多成员。

  清单 2. Dictionary 对象一览

$en = new Dictionary();
print_r( $en );

  如果运行该脚本,将看到如下对象的输出:

Dictionary Object
(
[translations] => Array
(
)
[type] => En
)

  可以使用对象操作符 -> 访问公共对象属性。所以 $en->type 表示由 $en 引用的 Dictionary 对象的 $type 属性。如果可以访问属性,就意味着可以设置和获得其值。清单 3 中的代码创建 Dictionary 类的两个实例 —— 换言之,它实例化两个 Dictionary 对象。它更改一个对象的 $type 属性,并添加两个对象的翻译:

  清单 3. 创建 Dictionary 类的两个实例

$en = new Dictionary();
$en->translations['TREE'] = "tree";
$fr = new Dictionary();
$fr->type = "Fr";
$fr->translations['TREE'] = "arbre";
foreach ( array( $en, $fr ) as $dict ) {
print "type: {$dict->type} ";
print "TREE: {$dict->translations['TREE']}\n";
}

  该脚本输出如下

type: En TREE: tree
type: Fr TREE: arbre

  所以 Dictionary 类现在比较有用了。单个对象可以存储不同的键值组合,还有一个标志,该标志告诉客户端有关这种 Dictionary 的详细信息。

  尽管 Dictionary 类当前与关联数组的包装器相差无几,但这里有一些了解对象功能的线索。目前,我们已经可以很好地表示我们的示例数据了,如清单 4 所示。

  清单 4. 示例数据

$en = array(
'translations'=>array( 'TREE' => 'tree' ),
'type'=>'En'
);
$fr = array(
'translations'=>array( 'TREE' => 'arbre' ),
'type'=>'Fr'
);

  虽然该数据结构完成了与 Dictionary 类相同的目的,但它没有提供结构的保证。如果传递 Dictionary 对象,我们知道它具有 $translations 属性。但如果是一个关联数据,则没有这样的保证。这个事实使得类似 $fr['translations']['TREE']; 的查询有些碰运气,除非进行查询的代码确定数组的起源。这是对象的重点:对象的类型是其特征的保证。

  虽然用对象存储数据有优点,但是您可能没有一点感觉。对象可以是东西,但关键在于它们还可以做事情。

  方法

  简单地说,方法是在类中声明的函数。它们通常(但不总是)通过对象实例使用对象操作符来调用的。清单 5 向 Dictionary 类中添加一个方法,并调用该方法。

  清单 5. 向 Dictionary 类中添加方法

class Dictionary {
public $translations = array();
public $type ="En";
function summarize() {
$ret = "Dictionary type: {$this->type}\n";
$ret .= "Terms: ".count( $this->translations )."\n";
return $ret;
}
}
$en = new Dictionary();
$en->translations['TREE'] = "tree";
print $en->summarize();

  它提供如下输出:

Dictionary type: En
Terms: 1

  正如所看到的,声明 summarize() 方法与声明任何函数的方式一样,只不过它是在类中声明。summarize() 方法是通过 Dictionary 实例使用对象操作符调用的。summarize() 函数访问属性来提供对象状态的简述。

  注意对于本文来说的一个新特性的用法。$this 伪变量提供了一种用于对象引用自己的属性和方法的机制。在对象外部,可以使用句柄来访问它的元素(在本例子中是 $en)。在对象内部,则无此句柄,所以必须求助于 $this。如果觉得 $this 有些迷惑,则在代码中遇到它时,试着在头脑中用当前实例 替换它。

  类通常使用通用建模语言 (Universal Modeling Language,UML) 表示在图表中。UML 的详细信息超出了本文的范围,但这种图表不过是一种可视化类关系的好方法。图 1 显示用 UML 表示的 Dictionary 类。类名位于顶层,属性在中间,方法在底层。

开始了解 PHP V5 中的对象_PHP

图 1. 使用 UML 显示 Dictionary 类

  构造函数

  PHP 引擎识别许多“魔术”方法。如果定义了方法,则 PHP 引擎将在相应的情况发生时自动调用这些方法。最常实现的方法是构造函数方法。PHP 引擎在实例化对象时调用构造函数。对象的所有基本设置代码都放在构造函数中。在 PHP V4 中,通过声明与类同名的方法来创建构造函数。在 V5 中,应声明叫做 __construct() 的方法。清单 6 显示需要 DictionaryIO 对象的构造函数。

  清单 6. 需要 DictionaryIO 对象的构造函数

class Dictionary {
public $translations = array();
public $type;
public $dictio;
function __construct( $type, DictionaryIO $dictio ) {
$this->type = $type;
$this->dictio=$dictio;
}
//...

  要实例化 Dictionary 对象,需要将类型字符串和 DictionaryIO 对象传递给它的构造函数。构造函数使用这些参数来设置自有属性。下列代码显示可以如何实例化 Dictionary 对象: $en = new Dictionary( "En", new DictionaryIO() );
  Dictionary 类现在比以前更安全。所有 Dictionary 对象都已经用必需的参数初始化过了。

  当然,还无法阻止一些人随后更改 $type 属性或将 $dictio 设置为空。可喜的是,PHP V5 可以帮助您实现这一功能。

  关键字:在此我们是否可以有一点隐私?

  前面已经看到与属性声明相关的 public 关键字。该关键字表示属性的可见度。事实上,属性的可见度可以设置为 public、private 和 protected。声明为 public 的属性可以在类外部写入和读取,声明为 private 的属性只在对象或类上下文中可见。声明为 protected 的属性只能在当前类及其子类的上下文中可见。(在 继承 部分将会看到这些内容起作用。)可以使用 private 属性来真正锁定类。如果将属性声明为 private 并试图从类范围外部访问它(如清单 7 所示),PHP 引擎将抛出致命错误。

  清单 7. 试图从类范围外部访问属性

class Dictionary {
private $translations = array();
private $dictio;
private $type;
function __construct( $type, DictionaryIO $dictio ) {
$this->type = $type;
$this->dictio = $dictio;
}
// ...
}
$en = new Dictionary( "En", new DictionaryIO() );
$en->dictio = null;

  输出如下: Fatal error: Cannot access private property
Dictionary::$dictio in...
  一般来说,应将大多数属性声明为 private,然后根据需要提供获得和设置这些属性的方法。这样就可以控制类的接口,使一些数据只读,在将参数分配给属性之前对参数进行清理或过滤,并提供与对象交互的一套明确的规则。

  修改方法可见度的方法与修改属性可见度的方法一样,即在方法声明中添加 public、private 或 protected。如果类需要使用一些外部世界无需知道的家务管理方法,则可以将其声明为 private。在清单 8 中,get() 方法为 Dictionary 类的用户提供了提取翻译的接口。该类还需要跟踪所有查询,因此提供了 private 方法 logQuery()。

  清单 8. get() 方法为 Dictionary 类的用户提供了接口

function get( $term ) {
$value = $this->translations[$term];
$this->logQuery( $term, $value, "get" );
return $value;
}
private function logQuery( $term, $value, $kind ) {
// write log information
}

  将 logQuery() 声明为 private 简化了公共接口,而且防止了类不适当地调用 logQuery()。与属性一样,尝试从包含类外部调用私有方法将导致致命错误。

  在类上下文操作

  到目前为止,所看到的方法和属性都在对象上下文中进行操作。也就是说,必须使用对象实例,通过 $this 伪变量或标准变量中存储的对象引用来访问方法和属性。有时候,可能发现通过类而不是对象实例来访问属性和方法更有用。这种类成员叫做静态 成员。

  要声明静态属性,将关键字 static 放在可见度修饰符后面,直接位于属性变量前面。

  下例显示单个静态属性:$iodir,存放用于保存和读取 Dictionary 数据的默认目录的路径。因为该数据对于所有对象是相同的,所以让它可用于所有实例是有意义的。

  清单 9. 单个静态 $iodir 属性

class Dictionary {
public static $iodir=".";
// ...
}

  可以使用范围解析操作符来访问静态属性,该操作符由双冒号 (::) 组成。范围解析操作符应位于类名和希望访问的静态属性之间。

print Dictionary::$iodir . "\n";
Dictionary::$iodir = "/tmp";

  正如所看到的,访问该属性无需实例化 Dictionary 对象。

  声明和访问静态方法的语法与此相似。再次,应将 static 关键字放在可见度修饰符后。清单 10 显示了两个静态方法,它们访问声明为 private 的 $iodir 属性。

  清单 10. 访问 $iodir 属性的两个静态方法

class Dictionary {
private static $iodir=".";
// ...
public static function setSaveDirectory( $dir ) {
if ( ! is_dir( $dir ) ||
! is_writable( $dir ) ) {
return false;
}
self::$iodir = $dir;
}
public static function getSaveDirectory( ) {
return self::$iodir;
}
// ...
}

  用户不再能访问 $iodir 属性目录了。通过创建特殊方法来访问属性,可以确保所提供的任何值是健全的。在本例中,方法在进行分配前检查给定字符串指向可写入的目录。

  注意,两个方法都使用关键字 self 和访问解析操作符来引用 $iodir 属性。不能在静态方法中使用 $this,因为 $this 是对当前对象实例的引用,但静态方法是通过类而不是通过对象调用的。如果 PHP 引擎在静态方法中看到 $this,它将抛出致命错误和一条提示消息。

  要从类外部调用静态方法,可使用类名加上范围解析符和方法名。

Dictionary::setSaveDirectory("/tmp");
print Dictionary::getSaveDirectory();

  需要使用静态方法有两个重要原因。首先,实用程序操作可能不需要对象实例来做它的工作。通过声明为静态,为客户机代码节省了创建对象的工作量。第二,静态方法是全局可用的。这意味着可以设置一个所有对象实例都可以访问的值,而且使得静态方法成为共享系统上关键数据的好办法。

  尽管静态属性通常被声明为 private 来防止别人干预,但有一种方法可以创建只读静态范围的属性,即声明常量。与全局属性一样,类常量一旦定义就不可更改。它用于状态标志和进程生命周期中不发生更改的其他东西,比如 pi 或非洲的所有国家。

  用 const 关键字声明类常量。例如,因为 Dictionary 对象的实际实现背后几乎肯定有一个数据库,所以还可以假设项和翻译有最大长度。清单 11 将其设置为类常量。

  清单 11. 将 MAXLENGTH 设置为类常量

class Dictionary {
const MAXLENGTH = 250;
// ...
}
print Dictionary::MAXLENGTH;

  类常量始终为 public,所以不能使用可见度关键字。这并是问题,因为任何更改其值的尝试都将导致解析错误。还要注意,与常规属性不同,类常量不以美元符号开始。   继承

  如果熟悉面向对象编程,您将知道我一直把最好的留到最后。类及其生成的动态对象之间的关系使得系统更灵活。例如,每个 Dictionary 对象封装不同的翻译数据集合,但是这些不同实体的模型定义在单个 Dictionary 类中。

  但有时候需要记下类级别的差异。是否记得 DictionaryIO 类?扼要重述一下,它从 Dictionary 对象中获取数据,将其写入文件系统,从一个文件中获取数据,将其合并回到 Dictionary 对象中。清单 12 显示使用序列化来保存和加载 Dictionary 数据的快速实现。

  清单 12. 使用序列化的快速实现

class Dictionary {
// ...
function asArray() {
return $this->translations;
}
function getType() {
return $this->type;
}
function export() {
$this->dictio->export( $this );
}
function import() {
$this->dictio->import( $this );
}
}
class DictionaryIO {
function path( Dictionary $dictionary, $ext ) {
$path = Dictionary::getSaveDirectory();
$path .= DIRECTORY_SEPARATOR;
$path .= $dictionary->getType().".$ext";
return $path;
}
function export( Dictionary $dictionary ) {
$translations = $dictionary->asArray();
file_put_contents( $this->path(
$dictionary, 'serial'),
serialize( $translations ) );
}
function import( Dictionary $dictionary ) {
$path = $this->path( $dictionary, 'serial' );
if ( ! is_file( $path ) ) return false;
$translations = unserialize(
file_get_contents( $path ) );
foreach ( $translations as $term => $trans ) {
$dictionary->set( $term, $trans );
}
}
}
$dict = new Dictionary( "En", new DictionaryIO() );
$dict->set( "TREE", "tree" );
$dict->export();

  本例引入两个简单的 Dictionary 方法,具体来说,asArray() 返回 $translations 数组的副本。DictionaryIO 实现具有简约的优点。因为在示例代码中通常省略了错误检查,即便如此,这仍是将数据保存到文件中的快速简单的方法。

  一旦部署了这种库之后,则需要立即支持它的保存格式。让格式过时会冒犯那些可能以这种方式存储备份的用户的愿望。但要求改变了,而且还可能收到输出格式不方便用户编辑的抱怨。这些用户希望将导出文件以 XML 格式发送给第三方。

  现在面临一个问题。如何在 DictionaryIO 接口中支持两种格式?

  一个解决方案是在 export() 和 import() 方法中使用条件语句,测试类型标志,如清单 13 所示。

  清单 13. 在 export() 和 import() 方法中使用条件语句

function export( Dictionary $dictionary ) {
if ( $this->type == DictionaryIO::SERIAL ) {
// write serialized data
} else if ( $this->type == DictionaryIO::XML ) {
// write xml data
}
}
function import( Dictionary $dictionary ) {
if ( $this->type == DictionaryIO::SERIAL ) {
// read serialized data
} else if ( $this->type == DictionaryIO::XML ) {
// read xml data
}
}

  这种结构是坏“代码味道”的一个例子,原因在于它依赖于复制。在一个地方进行更改(比如,添加新类型测试)需要在其他地方进行一组相应的更改(将其他类型测试带入行中),代码很快就会变得易错难读。

  继承提供了更优雅的解决方案。可以创建一个新类 XmlDictionaryIO,该类继承由 DictionaryIO 设置的接口,但覆盖其中一些功能。

  使用 extends 关键字创建子类。如下是 XmlDictionaryIO 类的最小实现:

XmlDictionaryIO extends DictionaryIO {
}
  XmlDictionaryIO 现在的功能与 DictionaryIO 完全相同。因为它从 DictionaryIO 继承了所有的公共(和保护)属性,所以可以将应用于 DictionaryIO 对象的相同操作应用于 XmlDictionaryIO 对象。这种关系扩展到对象类型。XmlDictionaryIO 对象显然是 XmlDictionaryIO 类的实例,但它也是 DictionaryIO 的实例 —— 同样地,以一般化的顺序,一个人同时是人类、哺乳动物和动物。可以使用 instanceof 操作符来测试这一点,如果对象是指定类的成员,则返回 true,如清单 14 所示。   清单 14. 使用 instanceof 操作符测试继承

$dictio = new XmlDictionaryIO();
if ( $dictio instanceof XmlDictionaryIO ) {
print "object is an instance of XmlDictionaryIO\n";
}
if ( $dictio instanceof DictionaryIO ) {
print "object is an instance of DictionaryIO\n";
}

  输出如下:

object is an instance of XmlDictionaryIO
object is an instance of DictionaryIO

  正如 instanceof 接受 $dictio 是 DictionaryIO 对象,所以方法也将接受这些对象作为参数。这意味着 XmlDictionaryIO 对象可以被传递给 Dictionary 类的构造函数,即使 DictionaryIO 是由构造函数的签名指定的类型。

  清单 15 是快而脏的 XmlDictionaryIO 实现,使用 DOM 来完成 XML 功能。

  清单 15. XmlDictionaryIO 实现

class XmlDictionaryIO extends DictionaryIO {
function export( Dictionary $dictionary ) {
$translations = $dictionary->asArray();
$doc = new DOMDocument("1.0");
$dic_el = $doc->createElement( "dictionary" );
$doc->appendChild( $dic_el );
foreach ( $translations as $key => $val ) {
$term_el = $doc->createElement( "term" );
$dic_el->appendChild( $term_el );
$key_el = $doc->createElement("key", $key );
$val_el = $doc->createElement(
"value", $val );
$term_el->appendChild( $key_el );
$term_el->appendChild( $val_el );
}
file_put_contents( $this->path(
$dictionary, 'xml'),
$doc->saveXML() );
}
function import( Dictionary $dictionary ) {
$path = $this->path( $dictionary, 'xml');
if ( ! is_file( $path ) ) return false;
$doc = DOMDocument::loadXML(
file_get_contents( $path ) );
$termlist = $doc
->getElementsByTagName( "term" );
foreach ( $termlist as $term ) {
$key = $term->getElementsByTagName( "key" )
->item( 0 )->nodeValue;
$val = $term
->getElementsByTagName( "value" )
->item( 0 )->nodeValue;
$dictionary->set( $key, $val );
}
}
}

  有关获得并生成 XML 的详细信息是当然要介绍的。有许多方法能完成这一操作,其中包括完美的 SimpleXML 扩展。简言之,import() 方法以 XML 文档为参数,并使用它来填充 Dictionary 对象。export() 方法从 Dictionary 对象中取得数据,并将其写入 XML 文件中。(在现实世界中,可能会使用叫做 XLIFF 的基于 XML 的格式,该格式适用于导入到第三方翻译工具中。)

  注意,import() 和 export() 都调用实用程序方法 path(),该方法不存在于 XmlDictionaryIO 类中。但没有关系,因为 path() 在 DictionaryIO 中实现。当 XmlDictionaryIO 实现一个方法时,则当调用该方法时,会为 XmlDictionaryIO 对象调用该实现。当没有任何实现存在时,调用失败返回给父类。

  图 2 显示了 DictionaryIO 和 XmlDictionaryIO 类之间的继承关系。封闭的箭头表示继承,从子类指向父类。

开始了解 PHP V5 中的对象_PHP

图 2. 继承关系

  结束语

  由于篇幅有限,因此不可能全部介绍。进一步研究有两个方向:广度和深度。广度指的是超出本文范围的那些特性,比如抽象类、接口、迭代器接口、反射、异常和对象复制。深度指的是设计问题。尽管理解 PHP 中可用于面向对象编程的工具范围很重要,但考虑如何最佳使用这些特性同样重要。幸运的是,专门讲述面向对象上下文中设计模式的可用参考资料很多(参阅“参考资料”)

  如果仍在使用 PHP V4 编程,我希望您查找足够新的特性来证明迁移到 V5 及其面向对象核心特性是正确的。不久您就会惊讶地发现自己无法离开它们了。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn