[译]鲜为人知的PHP OO模型 “功能(Features)”

原文地址:http://www.sitepoint.com/lesser-known-features-of-phps-oo-model/

(刚开始翻译,其中诸多地方翻译的不是很好,不幸被大神看到请使劲拍~~)

如今大多数的PHP应用都使用面向对象模式开发,而且PHP的开发者对OOP概念都有不错的理解。本文将拓展你的认知,并告诉你一些技巧和潜在的误区。

接口的继承以及 Traits

让我们从熟悉的领域“继承”开始。PHP接口允许我们定义一个约束,任何实现这个接口的对象必须在他自身实现这个接口(中的抽象方法)。但是你知道接口可以继承其他接口并且父或子接口都能被类实现吗?

考虑下面这段代码,定义一个接口,另一个接口继承他,并且用一个类实现这个子接口:

<?php
interface Reversible
{
    function reverse($target);
}
 
interface Recursible extends Reversible
{
    function recurse($target);
}
 
class Tricks implements Recursible
{
    public function recurse($target) {
        // something cool happens here
    }
 
    public function reverse($target) {
        // something backward happens here
    }
}

我定义了一个叫做 Reversible 的接口,并且用另一个叫做 Recursible 的接口继承他。当我用 Tricks 类去实现 Recursible 接口的时候,Recursible

这是一个有用的技术,当采用接口的时候,有一个类使用接口的抽象方法,而另一个类还需要一些额外的抽象方法限制的时候,你就可以写一个复合接口而不必去实现两个不同的接口。

Traits中有一个类似的模式。如果你还没有机会使用Traits,像这些类被定义了完整的方法并且可以用在你的应用的任何地方,而且不需要从同一个公共位置继承。我们经常说:移动公用的代码到公共的父类当中去要比在各个类中到处复制粘帖要好的多,但是有时类之间是不相关的,互相继承会有不妥。Traits是一个非常好的功能因为它允许我们复用代码即使对象之间没有相似之处也可以像继承一样使用其中的方法。

让我们来看一个trait的简单例子。(PS:之前的例子的命名继续使用)

<?php
trait Reversible
{
    public function reverse($target) {
        return array_reverse($target);
    }
}

traits 的语法看上去非常像 class, 而且traits的确可以包含属性和方法(包括抽象方法)。然后可以应用在一个class类中,使用 use 关键字引入 trait。 也可以应用在另外的trait,像我们看到的:

<?php
trait RecursivelyReversible
{
    use Reversible;
 
    public function reverseRecursively($target) {
        foreach($target as $key => $item) {
            if(is_array($item)) {
                $target[$key] = $this->reverseRecursively($item);
            }
        }
        return $this->reverse($target);
    }
}

现在我们有了一个trait并且调用了另一个trait同时为它自己添加了一种方法 。这个方法调用了第一个trait中的一个方法。这时候,我们应用这个trait到一个类中,因此这个traits包含的方法可以被这个类调用(像他自己的一样),我想在这里说明一下,这个类中是没有包含任何其他属性或者方法的,除了这个traits。

<?php
class Mirror
{
    use RecursivelyReversible;
}
 
$array = [1, "green", "blue", ["cat", "sat", "mat", [0,1,2]]];
$reflect = new Mirror();
print_r($reflect->reverseRecursively($array));

 如果你运行这段代码,你会发现不仅仅只有顶层的数组元素被反转了,PHP也钻如数组内部将所有的子元素也反转了(因为嵌套的traits也被正常调用了)。

如果属性是私有的会怎样?

因此你会认为私有的属性只有当前对象可以使用?并不是这样的!其实私有的限制仅仅是对于类的名字,也就是说相同类的实例化对象可以互相访问他们当中的私有属性和方法。为了说明这一点,我已经写了一个类包含一个私有属性和一个public方法,接收一个相同的类实例化后的对象作为这个方法的参数:

<?php
class Storage
{
    private $things = [];
 
    public function add($item) {
        $this->things[] = $item;
    }
 
    public function evaluate(Storage $container) {
        return $container->things;
    }
}
 
$bucket = new Storage();
$bucket->add("phone");
$bucket->add("biscuits");
$bucket->add("handcream");
 
$basket = new Storage();
print_r($basket->evaluate($bucket));

 你可能认为 $basket 没有权限访问 $bucket 的私有数据,但实际上以上代码运行良好!如果要问这算是一个功能还是一个bug(原文为:疑难杂症(gotcha)我认为有类似bug的意思,或许没那么严重),就像是问你种下去的是一个花还是杂草一样,这完全取决于你的想法和观点。

抽象类是什么样的?

一个抽象类通常情况下被认为是一个不完整的类;我们只定义了方法的一部分并且使用 abstract关键字来声明让这个类不能直接被实例化。

<?php
class Incomplete
{
    abstract public function notFinished();
}

 如果你尝试实例化 Incomplete 这个类,你将看到如下的错误信息:

PHP Fatal error: Class Incomplete contains 1 abstract method
and must therefore be declared abstract or implement the
remaining methods (Incomplete::notFinished) in /home/lorna/
sitepoint/oop-features/incomplete.php on line 5

这个信息已经很明白了,但是我们再来考虑另外一个类:

<?php
abstract class PerfectlyGood
{
    public function doCoolStuff() {
        // cool stuff
        return true;
    }
}

 这是一个完整可用的类除了他被 abstract 关键字声明了。实时上,你能用 abstract 关键字声明任何一个类只要你想这样做。其它的类可以继承他,但是他自己也不能被实例化了。这一般用于一个工具库的设计者希望开发者来扩展这些类而不是直接使用他们。正是因为这样 Zend Framework 才拥有丰富的传统抽象类。

类型提示不自动加载

我们用类型提示来确保传入一个方法的参数符合一定的限制,必须是一个类名或接口名(或者相关的)。然而PHP不会调用autoload 自动加载,如果这个类或者接口还没有被声明的话。我们将看到一个确实类声明的错误提示。

<?php
namespace MyNamespace;
 
class MyException extends Exception
{
}
 
class MyClass
{
    public function doSomething() {
        throw new MyException("you fool!");
    }
}
 
try {
    $myclass = new MyClass();
    $myclass->doSomething();
    echo "that went well";
}
catch (Exception $e) {
    echo "uh oh... " . $e->getMessage();
}

 在catch 子句中的类名实际上是一个类型提示,但是我们并没有指明这个 Exception 类的顶级命名空间是谁。PHP会认为我们指的是 MyNamespaceException 但是这个类并不存在。这个类缺失并不会引起错误,但是我们的异常错过了catch子句(注:应该指的的try中的代码抛出异常,但是catch子句并没有正常执行)。你可能想到我们会看到缺失类 MyNamespaceException 的提示信息,但是实际上我们得到了一个丑陋的“Uncaugh Exception”错误:

Fatal error: Uncaught exception ‘MyNameSpaceMyException’
with message ‘you fool!’ in /home/lorna/sitepoint/
oop-features/namespaced_typehints.php:11

如果你自信想想这个行为应该是有道理的,如果这个类型提示没有被加载,那么我们传入的参数是不能被匹配的。在我故意写这个错误的时候我还思考过这个问题,他只是个错字(这是直译,没太理解,求大神教导)!与其让catch 子句 抛出异常还不如直接抛出异常(我想这里的意思是:try 中的代码是异常的,应该跳到catch处理异常,但是实际上catch也存在异常,所以就直接抛出了try中的异常而不会跳到catch),这跟我的预期是一致的。

Finally

最近发布的PHP5.5版本中的新功能 Finally 子句是值得了解的。如果你解除过其他语言的异常处理,你可能对这个结构已经有所了解。一个 try 块 可以有多个 catch 块,从PHP5.5版本后我们还可以增加一个 Finally 块。

这里有前面代码的示例,加入了 finally :

<?php
namespace MyNameSpace;
 
class MyException extends Exception
{
}
 
class MyClass
{
    public function doSomething() {
        throw new MyException("you fool!");
    }
}
 
try {
    $myclass = new MyClass();
    $myclass->doSomething();
    echo "that went well";
}
catch (Exception $e) {
    echo "uh oh ... " . $e->getMessage();
}
finally {
    echo "move along, nothing to see here";
}

finally 子句将一直被执行,无论代码走到了try块 还是进入了任何的 catch 块中,有了更多未捕获异常的处理方式。在这个示例中,在 finally 块输出之前其实已经出现了未捕获的异常错误。但是在 try/catch/finally 这部分代码结束之前,他还不是一个未捕获的错误(所以不会阻止finally)。

结论:

当然这里列举的一些例子有的比较牵强,但愿你以后不会碰到他们,但是我认为这些极端的列子是值得我们了解的,这对我们进行系统结构等设计时会有很多帮助。了解这些元素肯定能帮助我们正确的完成工作和快速的处理bug,况且我喜欢更方便的分享我的想法!

在PHP中你还发现了什么坑爹的地方?在评论中一起分享吧!