天天看点

掌握 PHP Trait 的概念和用法

1. Trait 是什么

Trait [treɪt] 翻译过来是 "特性"、"特点" 、"特质",是一种在 PHP 中复用代码的形式。

我们看下官方的解释:

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。

Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

上述说明可以提取出几个关键词:代码复用、单继承、减少复杂性。

说到单继承,不得不提到另外一个特性:多态。多态和继承是软件开发中常用的代码复用方式,但是继承的方式虽然也能解决问题,但其思路违背了面向对象的原则,显得很粗暴;多态方式也可行,但不符合软件开发中的 DRY ( Don't repeat yourself ) 原则,增加了维护成本。

此时此刻,Trait 以一种全新的继承方式出现了,它既解决了前文叙述的两种继承方式的弊端,也相对优雅的实现了代码的复用。

简单说一下 Trait 在底层的运行原理:PHP 解释器在编译代码时会把 Trait 部分代码复制粘贴到类的定义体中,但是不会处理这个操作引入的不兼容问题。(是不是很厉害)

2. 定义和使用 Trait

接下来我还会沿用第二节课中的例子,但这次不是买手机,而是自己做手机。假设我们是一家比较特殊的手机厂商,为什么特殊呢,因为我们什么品牌的手机都做(就当是山寨工厂吧),废话不多说,我来说一下制作的规则:

假设我们掌握了三种手机的制作工艺,他们分别是:小米 Note3、三星 Galaxy S8 和 iPhone X,他们各有各的特点(相对于其他两种品牌独一无二)还有相同的特点(面部识别),为方便大家理解,我画了一张图(看不清图片的同学请点击图片查看大图):

掌握 PHP Trait 的概念和用法

简单解释一下:众所周知,这三部手机都有面部识别的功能(上图左侧部分),这属于相同点,其实他们的相同点不止这些(比如都可以打电话、发短信等),而了解他们的同学也都知道,其中任何一部手机都有相对于其他手机没有的功能(上图右侧部分)。

下面我将用代码的形式讲解,当把这些手机抽象为 PHP 类的概念时,如何利用 PHP Trait 更好更优雅的实现这个图中的功能。

首先,我需要把『面部识别』这三个手机都有的功能抽象为一个 Trait,请看代码示例:

// 小米 Note3 三星 S8 iPhone X
// 共同拥有的面部识别功能
trait Faceable {
    protected $face_id = 0;
    // 就当我是获取面部信息的功能
    public function getFace()
    {
        //...
        return $this->face_id;
    }
    // 就当我是设置面部信息的功能
    public function setFace(string $face_id)
    {
        //...
        $this->face_id = $face_id;
    }
}      

定义一个 Trait 是不是很简单,除特殊关键字以外,内部构造其实和 PHP 普通的类没啥区别。这里需要注意的是,Trait 的命名规范最好是以able 结尾,这样方便我们自己识别和理解。其次,建议每个文件只定义一种性状,这是良好的实践。

接下来就是定义这三部手机的类了,需要注意的是,我在实现这三部手机中特有的功能外,同时引用了我刚才定义的面部识别 Trait。

// 小米 Note3
class MiNote3 {
    // 引入面部识别 Trait
    use Faceable;
    // 独有的 MIUI
    protected $miui;
    // 初始化MIUI
    public function __construct($miui)
    {
        $this->miui = $miui;
        $this->bootUI();
    }

    private function bootUI()
    {
        return $this->miui;
    }

    //...
}      

注:小米的 MIUI 可以说是安卓阵营的佼佼者,用来当做其独特的特性不为过。

// 三星 GalaxyS8
class SamsangS8 {
    // 引入面部识别 Trait
    use Faceable;
    // 独有的 Bixby
    protected $bixby;
    
    public function __construct()
    {
        $this->sayHello();
    }

    private function sayHello()
    {
        echo "Hi I am Bixby!";
    }

    //...
}      

注:Bixby 是三星近期发布的语音助手,虽然还在内测中,我有幸提前体验了一把,感觉各个方面都碾压苹果的 Siri 和其他语音助手。所以把这个 Bixby 的功能作为一个三星S8独有的特性,抽象出来一个类。

// iPhoneX
class iPhoneX {
    // 引入面部识别 Trait
    use Faceable;
    // 独有的 TrueDepth
    protected $true_depth;

    public function __construct()
    {
        $this->openCamera();
    }

    private function openCamera()
    {
        return $this->true_depth;
    }
    //...
}      

注:iPhoneX 最好玩的特点莫过于前置的深感摄像头了,可以把表情动作赋给 emoji,这个功能就可以作为这个手机独有的特色抽象出来一个类。

通过以上示例,我们就可以把『面部识别』这三部手机都拥有的特性很轻松的组装到这三部手机中,使他们都具有完整性。用代码来说呢,就是每个类都使用了一个公共的 PHP Trait,在不改变自身的前提下,拥有了额外的属性。

3. Trait 的好处

有的人看完上述示例可能会有这样的疑问:与其像代码里这么写,我不如新建一个名为 Mobile 的类,在这个类里完成面部识别的功能,然后让三个手机的类直接继承 Mobile 类不就行了吗?比如:

// 手机公共类
class Mobile {
    protected $message;
    //...
    public function __construct($message)
    {
        $this->message = $message;
    }

    public function getMessage()
    {
        return $this->message;
    }

    // 实现了开关机 闹钟 音乐播放等功能...
    # code...
}      

然后这样引用

// 小米Note3
class MiNote3 extends Mobile {
     // ...
}

// GalaxyS8
class SamsangS8 extends Mobile {
    // ...
}      

答案是可以的,但是你有没有想过,除了『面部识别』,是不是还有其他的特性我没有列出来?比如三星和苹果都有的超大分辨率,小米没有;小米和三星都是安卓系统,而苹果不是。这样的关系,你如果都写在一个类里,由于 PHP 只有单继承的原因,会使你的 Mobile 类显得异常臃肿,难以解读。

// 小米Note3
class MiNote3 {
    use Faceable,Androidable;
    // ...
}

// GalaxyS8
class SamsangS8 {
    use Faceable,Androidable,HDisplayable;
    // ...
}

// iPhoneX
class iPhoneX {
    use Faceable,HDisplayable;
    // ...
}