更多优质内容
请关注公众号

PHP设计模式篇(六) Trait特性——类方法的组合模式-张柏沛IT博客

正文内容

PHP设计模式篇(六) Trait特性——类方法的组合模式

栏目:PHP 系列:PHP设计模式 发布时间:2022-11-12 10:33 浏览量:55

一、什么是Trait

我们知道PHP类是不能多继承的,Trait就是类似于多继承的一种代码复用机制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。

实际上,这里说Trait是多继承不太恰当,虽然它能达到多继承的效果,复用多个类的方法,但是无法描述多继承中的对象归属关系(即is-a关系)。更确切的来说,trait提供的是一种方法的组合功能,类似于接口类,能反映类与方法的包含关系(has-a关系),只是接口类无法实现方法,而trait可以。

接下来我们通过一个场景来看看trait如何使用,能带来什么好处。

 

请看如下场景:如果有若干个类需要使用单例模式,为了复用实例化对象相关的方法代码,我们能想到的做法是在父类实现单例功能,子类继承并实现具体的其他功能。

然而事情没有那么简单,假如现在业务上有3个需求,我们需要写3个方法(简称为方法1~3)来分别满足这3个需求。并且上述的子类A需要使用方法1和3,子类B使用方法1和2,子类C使用方法2和3,而且这三个方法业务上毫无关联(也就是分属3个不同业务的需求),因此将三个方法放到父类中实现虽然满足复用性,却在逻辑上不合理,此时就需要Trait解决我们的痛点。

我们可以将 方法1~3 分别放到 Trait1~3,然后子类1使用Trait1和Trait3,子类2使用 Trait1 和 Trait2。也就是说一个业务类可以随意的组合使用哪几个 Trait,从而让这个业务类具备哪些几个功能。

 

下面看个例子:

trait Singleton
{
    private static $instance;

    public static function getInstance() {
        if (!(self::$instance instanceof self)) {
            self::$instance = new self;
        }
        return self::$instance;
    }
}

class DbReader extends ArrayObject
{
    use Singleton;
}

class  FileReader
{
    use Singleton;
}

 

Trait 与抽象类类似,可定义抽象方法和正常方法,但自身无法实例化。通过在一个类中 use 一个Trait,该Trait内的方法和属性就会被水平的注入该类中。

所谓的水平的注入,意思是如果一个Trait定义了某些属性或方法,那么use这个Trait的类A也可以使用这些属性/方法,即便这些属性/方法是私有的,因为Trait中的所有属性和方法都被注入到了类A,这些属性和方法就变成了类A的属性和方法。而对于继承而言,子类是不能使用父类的私有属性/方法的。因此这一点也体现了Trait特性只能说表面类似于多继承,实际上不算是多继承,而是一种组合模式。

 

二、Trait特性

为了方便说明,下面内容中我们将 use Trait 语句所在的类称为使用类

0、Trait自身无法实例化,而且Trait的静态方法或属性不能直接由Trait自身调用,只能由其使用类调用。

1、私有属性/方法水平注入,被注入trait的类(即使用类)可访问trait的私有成员,反之trait也可以访问被注入trait的类的私有成员,当然后者的情况很少,因为一般来说trait本身是不知道它的实现类中有些什么属性。

例如:

<?php
trait Message
{
    function alert() {
        echo $this->message;
    }
}

class Messenger
{
    use Message;
    private $message = "This is a message";
}

 

2、一个类可以use多个trait,一个trait可以也可以use多个trait组合成一个新trait。

<?php
trait Hello
{
    function sayHello() {
        echo "Hello";
    }
}

trait World
{
    function sayWorld() {
        echo "World";
    }
}

trait HelloWorld
{
    use Hello, World;
}

class MyWorld
{
    use HelloWorld;
}

$world = new MyWorld();
echo $world->sayHello() . " " . $world->sayWorld(); //Hello World

 

3、对于方法而言:trait 的方法覆盖从父类继承的方法(不难理解,因为trait和被注入trait的类是同级的),当前类定义的方法覆盖 trait 的方法。

例如:

<?php
trait Hello
{
    function sayHello() {
        return "Hello";
    }

    function sayWorld() {
        return "Trait World";
    }

    function sayHelloWorld() {
        echo $this->sayHello() . " " . $this->sayWorld();
    }

    function sayBaseWorld() {
        echo $this->sayHello() . " " . parent::sayWorld();
    }
}

class Base
{
    function sayWorld(){
        return "Base World";
    }
}

class HelloWorld extends Base
{
    use Hello;
    function sayWorld() {
        return "World";
    }
}

$h =  new HelloWorld();
$h->sayHelloWorld(); // Hello World
$h->sayBaseWorld(); // Hello Base World

 

对于非静态属性而言:如果一个 trait 定义了一个属性,那么一个使用类不能定义一个具有相同名称的属性,除非它具有相同的可见性和初始值,否则会发出致命错误。

<?php
trait PropertiesTrait {
    public $same = true;
    public $different = false;
}

class PropertiesExample {
    use PropertiesTrait;
    public $same = true;
    public $different = true; // Fatal error
}
?>

 

对于静态属性而言:对于trait中的某个静态属性,多个使用类的该静态属性是相互独立的(而对于继承而言,父类中的静态属性在所有子类中是共享的,一个子类对其修改,另一个子类访问到的是修改后的值)。

<?php
trait Singleton
{
    protected static $name;

    public static function setName($name){
      self::$name = $name;
    }

    public static function getName(){
      return self::$name;
    }
}

class DbReader
{
    use Singleton;
}

class  FileReader
{
    use Singleton;
}

DbReader::setName('zbp');
FileReader::setName('abc');
var_dump(DbReader::getName());  // zbp
var_dump(FileReader::getName());  // abc
var_dump(Singleton::getName());   // 报Deprecated并返回null

 

4、多个 traits 使用了相同的方法名将抛出一个致命错误,需要insteadof关键字解决冲突。

<?php
trait Game
{
    function play() {
        echo "Playing a game";
    }
}

trait Music
{
    function play() {
        echo "Playing music";
    }
}

class Player
{
    use Game, Music {
        Music::play insteadof Game;
    }
}

$player = new Player();
$player->play(); //玩音乐

$player = new Player();
$player->play();

 

另一种方式是保留两个方法,为其中给一个方法起别名。

<?php
class Player
{
    use Game, Music {
        Game::play as gamePlay;
        Music::play insteadof Game;
    }
}

$player = new Player();
$player->play(); //玩音乐
$player->gamePlay(); //玩游戏

 

5、trait可以定义抽象方法来强制要求使用类必须实现这些抽象方法。

6、PHP 的 Trait 可以拥有构造器(__construct(),如果使用类也定义了构造方法,则实用类的构造方法会覆盖Trait的构造方法,且无法通过parent::__construct的方式保留),但是必须将其声明为 public,如果声明 protected 或 private 将会报错。在 Trait 中使用构造器时应当谨慎,因为经常会导致使用类出现意料之外的冲突。

7、可以使用as关键字改变trait中的方法的可见性(可以在使用类中将trait的方法的权限改大或者改小)。

<?php
trait HelloWorld {
    public function sayHello() {
        echo 'Hello World!';
    }
}

class MyClass1 {
    use HelloWorld { sayHello as protected; }
}

class MyClass2 {
    use HelloWorld { sayHello as private myPrivateHello; }
}
?>

 




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > PHP设计模式篇(六) Trait特性——类方法的组合模式

热门推荐
推荐新闻