类和对象:Trait

在前面几节的学习中,我们清楚了类和对象的概念、结构,面向对象的基本思想和特殊的三个类以及接口的定义与使用,这些特性在许多编程语言中都是普遍存在的,接下来的这节则是 PHP 专为单继承语言(类只能继承一个类)设计的代码复用机制:Trait

我们已经知道,PHP 是一门单继承语言,单继承意味着一个类只能继承一个类,这在有时候是受限的:在不同结构层次的互不关联的类中无法复用方法。在PHP 5.4的时候,解决了这个问题——Trait面世了。

定义一个Trait使用关键字trait即可,语法如下:

trait Traitable {
    // 代码
}

Trait的用法

Trait与类很像,它不能像普通类一样实例化,但它拥有类的许多特点。下面我们来看一下Trait可以做什么:

<?php

// Trait

trait tBase
{
    // Trait 可定义属性
    public $val;

    public function getThis()
    {
        // Trait 所定义的方法必须包含实现
        return $this;
    }

    protected function perameter()
    {
        // Trait 对于方法的可见性没有限制
    }

    // Trait 支持定义抽象方法(使用Trait的类同样必须实现抽象方法)
    abstract public function abstractFunc();

    static public function staticFunc()
    {
        // Trait 可定义静态方法
    }
}

trait tSub
{
    use tBase;
}

class Base
{
    // 使用 Trait
    use tSub;

    // 实现 Trait 中的抽象方法
    public function abstractFunc()
    {
        echo 'Trait中定义的抽象方法必须在使用了Trait的类中实现',"\n";
    }
}

class Other
{
    use tSub;
    // 实现 Trait 中的抽象方法
    public function abstractFunc()
    {
        echo 'Trait中定义的抽象方法必须在使用了Trait的类中实现',"\n";
    }
}

// 测试:Trait 中获取的 $this 会指向使用该Trait的类
$base = new Base();
var_dump($base);
/* 输出:
object(Base)#1 (1) {
  ["val"]=>
  NULL
}
*/

$base->abstractFunc();// 输出:Trait中定义的抽象方法必须在使用了Trait的类中实现

$other = new Other();
var_dump($base->getThis());
/* 输出:
object(Base)#1 (1) {
  ["val"]=>
  NULL
}
*/

var_dump($other->getThis());
/* 输出:
object(Other)#2 (1) {
  ["val"]=>
  NULL
}
*/

可以看出Trait与类结构别无二致!

Trait除了不能像普通类一样被实例化,还有一点和类不同的是,Trait可以随意组合class可以使用TraitTrait也可以使用Trait,只要使用关键字use声明。

“可随意组合”意味着Trait可以在各种类中重复使用而不用考虑继承问题,Trait就像一块砖,哪儿有用往哪儿搬

也意味着我们的类可以拆分出一部分描述其需求和功能的代码,作为具有某特点的Trait。但这并不是说发现两个类中两段代码一样就提取到一起,一切从功能或需求出发

Trait的特征性

Trait翻译过来就是特征、特性,在有些书里也把它叫做性状。我们暂且将它看做“特征”好了。

特征可以理解为在一群类似的对象中,某一个对象拥有不一样的特点。这样一想笔者第一反应是丑小鸭与白天鹅的故事。当然这里不会讲童话故事,我们只是把这个故事作为案例:

在“丑小鸭与白天鹅”的故事里,小白鹅与小黄鸭都属于禽类,有着一身的羽毛,会说话(并不),但是小白鹅会飞,小黄鸭不行。

这样一分析,运用前面所学的知识,我们大概可以设计出三个类:禽类作为基类,有着禽类的基本特征:羽毛(这里只列出一部分),会叫。小白鹅和小黄鸭分别作为禽类的子类,小白鹅类实现“会飞”的方法,小黄鸭类没有。

这样并没有错。

但是如果再添加一个不一样的对象呢?老鹰。这三者都是禽类,但是小白鹅和老鹰都会飞,小黄鸭和小白鹅都会游泳。按照上面的思考方式,我们可能会做两种方案:

  • 禽类作为基类,小白鹅、小黄鸭和老鹰分别是它的子类,小白鹅和老鹰实现“会飞”的方法,小白鹅和小黄鸭实现“会游”的方法
    • 听上去没毛病,但这里造成了代码重复使用
  • 禽类作为基类,“会飞的禽类”是它的子类,小白鹅和老鹰继承“会飞的禽类”,小黄鸭则继承禽类,独自实现“会游”的方法
    • 听上去也没毛病,也把重复代码去掉了。但这里多了一个基类

在可区分的特性越来越多时,PHP 的单继承性需要让我们设计许多基类,层层继承,最后得到一个拥有各种特性的类,想想上面的例子,如果再加一个“会迁徙”的大雁呢?

这时候我们就可以用到 PHP 的Trait

同一类对象可能拥有多种性征,而多类对象可能拥有相同性征,这种情况就适用于Trait

将每个特征(会飞的、会游的、会迁徙的)都提取出来,然后在类中使用它们!上面的例子使用代码表达就是:

<?php

// 禽类

class Bird
{
    public $feather;// 羽毛
    public function howl()
    {
        // 啼叫
    }
}

// Trait
trait Fly
{
    public function fly()
    {
        // 飞行
        echo '我会飞',"\n";
    }
}

trait Swimming
{
    public function swimming()
    {
        // 凫水
        echo '我会游泳',"\n";
    }
}

trait Migrate
{
    public function migrate()
    {
        // 迁徙
        echo '我会迁徙',"\n";
    }
}

// 继承基类的子类,并使用相应的Trait
class Duck { use Swimming; }
class Swan { use Fly, Swimming; }
class Eagle { use Fly; }
class Goose { use Fly, Migrate; }

// 测试:
$duck = new Duck();
$duck->swimming();
// $duck->fly();// 未定义,不能使用
$goose = new Goose();
$goose->fly();
$goose->migrate();

这样一看是不是很清晰?子类各自具备的特征一目了然。

如果按照一层一层继承的方法,很容易迷失在臃肿的结构中,而且不方便解耦。比如说想要去除某一个特性,而这个特性被嵌套在深深的类继承中,“牵一发而动全身”说的就是这种情况。

而使用Trait的话,只需要删掉这个特性的使用即可。

合理使用Trait

我们已经知道Trait的优势,那是不是遇到重复代码就提取为Trait呢?

当然不是,Trait不仅仅是可复用代码段的集合,更是某个特性的属性和方法的一段描述。通常Trait被命名为“xxable”,表示“可做某某事”(上述示例中三个Trait都没有able后缀的版本故而没有遵循这个命名规则),用以体现Trait的用途。

示例代码并没有涉及太多属性,但经常一些特征是伴随着独特的属性一起出现的,例如上面的“迁徙”方法,可以添加两个属性:迁徙的时间和迁徙的地点。这两个属性仅跟“迁徙”这个方法相关,与“大雁”类没有太大关联,那么在“拆分”代码时可以把属性和方法一起“打包”到Trait


本节阐述了 Trait 的基本使用和特点,并分析了它的适用情况。现在进行总结:

  • 使用上
    • Trait与类结构相似,只是不能被实例化
    • 使用关键字trait定义
    • Trait可定义属性、抽象方法、静态方法和普通方法
    • Trait和类都可以使用关键字use使用Trait
  • 优势
    • 相比于类,Trait可随意组合
    • Trait逻辑清晰,可读性高
    • Trait耦合性低,可复用性高

截至本节,关于类和对象的所有主要概念都已结束!最后一节为扩展说明,讲述了类和对象中的魔术方法