Published on

PHP面向对象精髓解析:继承、多态与后期静态绑定实战指南

Authors
  • avatar
    Name
    Liant
    Twitter

基础篇-面向对象

零、前言

我们都知道,面向对象的特征有三个:继承、封装、多态,但是在工作真正用到的地方很少,对于这三个概念,大部分同学都是没有理解到的,今天我们就再回头学习一下面向对象。涉及到的内容主要有下面几点:

  • 继承、协变与逆变、多态、接口编程
  • 接口、抽象类
  • 后期静态绑定
  • 性状
  • 匿名类

以上这些内容都是php中面向对象的基础,他们环环相扣,互为补充,都是为了解决项目设计中的问题而慢慢演变出来的, 我们学习的最终目的是掌握并且运用,即使自己不会应用到项目中,至少我们要能看懂别人的代码,下面是我截取的一些代码片段,如果你自己不知道代码背后是如何实现的,那后面的内容就能帮助你更好的理解这些代码:

// 返回的对象是什么
public static function instantiate($row) {
    return new static();
}
// 接口为什么继承
interface ActiveRecordInterface extends StaticInstanceInterface{
    
}
// 链式调用怎么实现
public static function batchInsertGroup($data, $limit = 100) {
  Yii::$app->db->createCommand()
  ->batchInsert(static::tableName(), array_keys(reset($grop)), $grop)
  ->execute();
}

并且,在面向对象的基础上,又衍生出了很多编程规范,比如S.O.L.I.D 原则:

  • S:单一原则,一个类或一个方法,一个函数只做一件事情,比如:导出数据可以导出txt、xls、doc各种类型,应该封装到不同的方法,而不是在一个方法里面用if来做判断

  • O:开闭原则,一个类应该对扩展开放,对修改关闭,比如:用户支付的时候,可以选择支付宝、微信、京东等支付渠道,应该不同的支付渠道单独放到一个类里面,而不是把这三个支付渠道放到一个类里面,如果未来要新增支付通道,还需要修改这个类,对以前的支付通道产影响。

  • L:里式替换,简单的说,就是父类的类型必须可以被子类替换,父类可以是抽象类、接口、类,这个规则主要是让我们的继承,尽可能合理

  • I:接口隔离,使用接口来实现解耦

  • D:依赖倒置,调用者不应该依赖被调用者,比如:一个服务类里面调用Db类操作mysql,如果未来我想操作oracle数据库,那所以使用Db类的地方我都要去改成oracle类,那就不符合依赖倒置,正确的做法是新增一个中间代理类,调用者手动传入Db实例。

在上面的设计原则基础上,又衍生出了各种设计模式,比如:单例、工厂、桥接、代理、观察者等等,他们往往是同时遵循多种设计原则,所以掌握面向对象是比较重要的。 这里,我们不会讲面向对象里面的譬如类、属性、方法等基础的概念,我们假定所有人都已经熟悉了这些概念,并且已经熟练掌握php,而是直接讲具体如何理解和使用面向对象的概念。

一、继承、协变与逆变、多态、接口编程

面向对象中最重要的概念是:一切都是围绕类型展开的

  • 继承
class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function sayName() {
        var_dump($this->name);
    }
}

class Dog extends Animal {

}

(new Dog("doly"))->sayName();
string(4) "doly"

可以看到,子类继承的数据有:属性、构造函数、普通方法,这些大家都耳熟能详,也不是我们关注的内容。我们重点关注的是:子类继承父类以后,最大的一个变化就是子类多了一种新的类型

class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function sayName() {
        var_dump($this->name);
    }
}

class Dog extends Animal {

}


var_dump((new Dog("doly")) instanceof Animal);
var_dump((new Dog("doly")) instanceof Dog);
bool(true)
bool(true)

从结果中可以看到,Dog这个对象不仅是Dog类的实例,也是Animal类的实例,就是说,它同时有两种类型:Dog和Animal,那么利用这个特性,我们可以衍生出下面两个概念:逆变与协变

  • 逆变、协变

协变和逆变都是用来描述类型与类型之间的关系,其中协变表示子类型关系可以“向上转型”,逆变表示子类型关系可以“向下转型”,协变和逆变可以帮助我们编写更灵活、通用的代码。

通俗的讲,继承关系中,如果子类可以转换成父类(向上转型,范围扩大),这个过程称为协变。如果父类转换成子类,这个过程称为逆变(向下转型,范围缩小)。

class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function sayName() {
        var_dump($this->name);
    }
}

class Dog extends Animal {
    public function dogEat() {
        var_dump($this->name . " eat ");
    }
}


function main(Animal $animal) {
    // 参数是Animal类型,Animal类本来就具有sayName方法,这里可以直接调用,可以理解为协变。
    $animal->sayName();
    // 参数是Animal类型,Animal类是没有dogEat这个方法的,但是这里还是可以调用,是因为发送了逆变,把一个宽泛的类型Animal转换成了一个具象的类型Dog
    // php中的逆变是隐式发生的,在强类型语言中需要自己手动转换,伪代码:(Dog)($animal) 或 $animal as Dog
    $animal->dogEat();
}

// 调用入口
main(new Dog("doly"));
string(4) "doly"
string(9) "doly eat "

通过上面的例子,如果两个类有继承关系,那么子类会因为继承而多出一种类型(也适用于接口、抽象类),可以在多个类型之间转换,即协变与逆变,基于这个特性,又可以衍生出一个概念:多态

  • 多态

多态顾名思义,就是指一个对象可以利用逆变与逆变呈现出多种状态,多态遵循依赖倒置,接口隔离,单一职责

多态可以使代码更灵活,比如经常听到的:面向接口编程。实现多态有多种方式,可以使用接口、抽象类、继承,我们这里先熟悉继承实现多态,其他两种方式是类似的。

class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function eat() {
        var_dump($this->name . " eat ");
    }
}

class Dog extends Animal {

}

class Cat extends Animal {

}


function main(Animal $animal) {
    // 参数限定了Animal类型,但外部可以传入cat和dog来控制具体是哪一种类型的eat方法,一种类型呈现出了不同的行为状态,这就是多态。
    $animal->eat();
}

// 调用入口
main(new Dog("doly"));
main(new Cat("kitty"));
string(9) "doly eat "
string(10) "kitty eat "
  • 接口编程

接口编程是多态的一种应用形式,就是用接口来解耦。

经常听到别人说解耦,我们先知道什么是解耦:将不同的功能封装到不同的类,当实现类功能发生变化,只需要修改实现类,而不需要修改调用者,达到最小修改,这就是解耦。

interface Animal {
    public function eat();
}

class Dog implements Animal {

    public function eat() {
        var_dump("dog eat ");
    }

}

class Cat implements Animal {

    public function eat() {
        var_dump("cat eat ");
    }

}

// 使用强耦合,如果后期要增加Dog类,就必须修改main方法
function mainBefore() {
    $cat = new Cat();
    $cat->eat();

    $dog = new Dog();
    $dog->eat();
}

// 使用接口编程,main方法不需要做任何修改,只需要新增类,实现Animal接口,就可以实现扩展main方法,这就是接口编程
function mainAfter(Animal $animal) {
    $animal->eat();
}

二、抽象类、接口

抽象类、接口可以让代码结构更合理,更灵活,和继承一样,只要一个类继承了抽象类或者实现了接口,这个类的对象就会多出一种类型

  • 抽象类——Abstract Class

抽象类用来对代码进行整理,它的最显著的特点就是:求同存异,对相同的代码行为共享,对不同的代码行为开放。

abstract class Animal {

    // Animal类型的对象都有eat这个共同的行为
    public function eat() {
        var_dump("animlal eat ");
    }

    // 但是个别对象的run行为可能不同
    abstract function run();
}

class Dog extends Animal {

    function run() {
        var_dump("dog run ");
    }
}

class Bird extends  Animal{

    function run() {
        var_dump("bird fly ");
    }
    
}

// Animal对象eat方法都是一样的,但是run方法,可以在各自的类里面进行扩展,求同存异。
function main(Animal $animal){
    $animal->eat();
    $animal->run();
}
main(new Dog());
main(new Bird());
string(12) "animlal eat "
string(8) "dog run "
string(12) "animlal eat "
string(9) "bird fly "
  • 接口——Interface

接口主要是用来对对象类型进行约束,利用多态,实现面向接口编程来解耦代码


interface Animal {

    public function eat();

}

interface Human extends Animal {
    public function study();
}

class People implements Human {

    public function eat() {
        var_dump("people eat");
    }

    public function study() {
        var_dump("people study");
    }
}

class Dog implements Animal {

    public function eat() {
        var_dump("dog eat");
    }
}

// 多态调用
function main(Animal $animal) {
    $animal->eat();
    $animal->study();
}

main(new People());
main(new Dog());
string(10) "people eat"
string(12) "people study"
string(7) "dog eat"

三、后期静态绑定——Static Bind

后期静态绑定是php才有的一个特性,主要关注static关键字在不同场景下的行为,也是比较晦涩一个特性

我们经常看到下面这种代码写法,如果对后期静态绑定不熟悉,是很容易出错的:

class Animal {

    public function name() {
        return static::class;
    }

    public function instance() {
        return new static();
    }
    
}

php中调用方法有两种类型:转发调用与直接调用(也称非转发调用),不同的调用方式,static关键字表现出来的行为不一样

  • 转发调用

    没有具体的对象和类,即以self::,parent::,static:: 以及 forward_static_call(),几种方式进行调用时,称为转发调用,中间有一个解析的过程。

  • 直接调用

    直接使用对象或者具体的类进行方法调用,如User::findOne()或$user->findOne(),明确指定了对象和类,称为直接调用。

后期静态绑定的调用原理:每一次调用,存储了在外层“直接调用”的类名。意思是当我们使用一个转发调用的时候,static指的实际调用的类是上一层栈存储的直接调用者的类名

class Animal {
    public static function foo() {
        echo __CLASS__ . "\n";
        // 转发调用,外层存储的类是Animal
        static::who();
    }

    public static function who() {
        echo __CLASS__ . "\n";
    }
}


class Human extends Animal {
    // 当前调用栈存储的外层调用类是People
    public static function test() {
        // 直接调用,存储的类是Animal
        Animal::foo();

        // 转发调用,外层存储的类是People
        parent::foo();

        // 转发调用,外层存储的类是People
        self::foo();
    }

    public static function who() {
        echo __CLASS__ . "\n";
    }

}


class People extends Human {
    public static function who() {
        echo __CLASS__ . "\n";
    }
}

// A A A P A P
// 直接调用,存储的类是People
People::test();
Animal
Animal
Animal
People
Animal
People

四、性状——Trait

性状的特性和抽象类类似,可以把公共的代码都放入一个类中进行共用。但是抽象类有一个问题,如果我只需要其中的一部分功能,子类也需要继承整个类,会让创建出来的对象示例膨胀,而性状就是解决这个问题的。

trait Singleton {
    private static $instance = null;
    public         $times    = 0;

    private function __construct() {
        $this->times += 1;
    }

    private function __clone() {
    }

    /**
     * @return static|null
     */
    public static function getInstance(): ?Singleton {
        self::$instance || self::$instance = new self();
        return self::$instance;
    }

    public function getTimes(){
        var_dump($this->times);
    }
}

class Database {
    use Singleton;
}

Database::getInstance()->getTimes();
Database::getInstance()->getTimes();
int(1)
int(1)

五、使用规范

接口、抽象类、性状这三个特性都有相似的地方,那么我们在什么场景下使用他们呢,这里有一些参考:

  • 如果代码有大量公共的逻辑,只有少部分代码是需要在子类里面重写的,使用抽象类

    比如,用户中心发送短信的渠道,不管是阿里云、腾讯、还是253,都有验证手机号、写redis、发送验证码这三步,那可以都放到抽象类里面,然后不同平台的配置参数可能是不一样的,可以放到抽象方法里面返回。

  • 如果代码需要解耦,保证可扩展,而不修改原来调用者的代码,使用接口来进行约束

    比如,用户中心发送验证码,有语音验证码,有文本验证码,那可以把这两个方法分别定义成两个接口,VoiceInterface和TextInterface,如果阿里云同时支持语音和文本,那它就应该同时实现这两个接口 而253只支持文本验证码,那就只需要实现TextInterface即可。在调用的地方,在未来新增短信渠道的时候,只需要实现这两个接口,传入就行了,不需要改其他代码。

  • 如果是工具性质的方法,比如实现单例、发送网络请求,操作redis,这种很独立的功能,可以使用性状。

六、匿名类

匿名类也可以成为临时类,当我们需要一个对象,但是不想单独创建一个类的时候,可以使用匿名类

class Animal {

    public function name() {
        return static::class;
    }

    public function instance() {
        return new static();
    }
}

$dog = new class extends Animal{
    public function name() {
        var_dump("dog");
    }
};
$dog->name();
string(3) "dog"

七、综合应用


trait Singleton {
    private static $instance = null;

    private function __construct() {

    }

    private function __clone() {
    }

    /**
     * @return static|null
     */
    public static function getInstance() {
        self::$instance || self::$instance = new self();
        return self::$instance;
    }
}

interface IDatabase {
    public function query();
}

class Mysql implements IDatabase {
    use Singleton;

    public function query() {
        var_dump("mysql query");
    }
}

class Oracle implements IDatabase {
    use Singleton;

    public function query() {
        var_dump("Oracle query");
    }
}

class DatabaseService {
    private ?IDatabase $database = null;

    public function setQuery(IDatabase $database): DatabaseService {
        $this->database = $database;
        return $this;
    }

    public function query() {
        $this->database->query();
    }
}

$service = new DatabaseService();
$service->setQuery(Mysql::getInstance())->query();
$service->setQuery(Oracle::getInstance())->query();

string(11) "mysql query"
string(11) "oracle query"