继承与多态:extends关键字、方法重写、parent关键字、final关键字、抽象类与接口、多态的实现

好,咱们今天聊聊面向对象里最核心的两个概念——继承和多态。说实话,我见过不少新手把这两个东西背得滚瓜烂熟,但一到写代码就乱套。其实没那么玄乎,你把它想成「父子关系」和「变身能力」就对了。

extends关键字:子承父业

继承用 extends 关键字实现。子类可以拥有父类的所有非私有属性和方法。我习惯把父类叫做「基类」,子类叫做「派生类」。

<?php
class Animal {
    public $name;
    
    public function eat() {
        echo "{$this->name} 正在吃东西\n";
    }
}

class Dog extends Animal {
    public function bark() {
        echo "{$this->name} 汪汪叫\n";
    }
}

$dog = new Dog();
$dog->name = '旺财';
$dog->eat();  // 继承来的方法
$dog->bark(); // 自己的方法

你看,Dog 类一个字都没写 eat() 方法,但它能用。这就是继承最直接的好处——代码复用。

我的习惯:继承层次别超过三层。超过三层,代码基本就成一团乱麻了。我在项目中见过七层继承的代码,改一个父类方法,下面六个子类全崩,那叫一个酸爽。

方法重写:青出于蓝

子类可以重新定义父类的方法,这叫重写(Override)。说白了,就是儿子觉得老爸的做法不够好,自己来一套。

<?php
class Animal {
    public function makeSound() {
        echo "动物发出声音\n";
    }
}

class Cat extends Animal {
    public function makeSound() {
        echo "喵喵喵\n";
    }
}

$cat = new Cat();
$cat->makeSound(); // 输出:喵喵喵

这里要注意,重写的方法签名必须一致。PHP 7.4 以后,返回类型也得一致,否则会报错。嗯,这个坑我踩过——有一次我重写方法时忘了加返回类型声明,结果线上跑得好好的,升级 PHP 版本后直接崩了。

避坑指南:我曾经在重写方法时,不小心把参数类型从 int 改成了 string。PHP 不会报错,但逻辑全乱了。所以重写时,参数类型和个数一定要和父类保持一致。

parent关键字:不忘本

有时候子类重写了方法,但还想调用父类的原始逻辑。这时候就用 parent 关键字。我把它叫做「回娘家」语法。

<?php
class ParentClass {
    public function greet() {
        echo "你好,我是父类\n";
    }
}

class ChildClass extends ParentClass {
    public function greet() {
        parent::greet(); // 先调用父类方法
        echo "你好,我是子类\n";
    }
}

$child = new ChildClass();
$child->greet();
// 输出:
// 你好,我是父类
// 你好,我是子类

你想想看,这种模式在框架里特别常见。比如 Laravel 的控制器,你重写 index() 方法时,经常先调一下 parent::index() 来执行一些公共逻辑。

final关键字:到此为止

final 关键字有两个用途:禁止类被继承,或者禁止方法被重写。说白了就是「我说了算,你们别改了」。

<?php
// 禁止类被继承
final class FinalClass {
    public function doSomething() {
        echo "这个类不能被继承\n";
    }
}

// 禁止方法被重写
class BaseClass {
    final public function importantMethod() {
        echo "这个方法不能被重写\n";
    }
}

我的经验:我一般只在两种场景用 final:一是工具类,比如数学计算类,没必要继承;二是核心业务方法,比如支付流程里的验签方法,改了会出大事。其他情况,我建议少用 final,给未来的自己留点余地。

抽象类与接口:契约精神

抽象类和接口,说白了就是「定规矩」的。它们只定义方法签名,不写具体实现。子类必须按照这个规矩来写代码。

抽象类

抽象类用 abstract 关键字定义。它可以有抽象方法(没有方法体),也可以有普通方法。我习惯把抽象类叫做「半成品」——它搭好了骨架,但留了一些细节让子类去填充。

<?php
abstract class Database {
    protected $connection;
    
    abstract public function connect($host, $user, $password);
    abstract public function query($sql);
    
    public function disconnect() {
        $this->connection = null;
        echo "断开连接\n";
    }
}

class MySQL extends Database {
    public function connect($host, $user, $password) {
        $this->connection = "连接到 MySQL: {$host}";
        echo $this->connection . "\n";
    }
    
    public function query($sql) {
        echo "执行 MySQL 查询: {$sql}\n";
    }
}

接口

接口用 interface 关键字定义。它只能定义方法签名,不能有任何实现。一个类可以实现多个接口,但只能继承一个类。

<?php
interface Logger {
    public function log($message);
    public function error($message);
}

interface Cache {
    public function set($key, $value);
    public function get($key);
}

class FileLogger implements Logger, Cache {
    public function log($message) {
        echo "记录日志: {$message}\n";
    }
    
    public function error($message) {
        echo "记录错误: {$message}\n";
    }
    
    public function set($key, $value) {
        echo "缓存设置: {$key} = {$value}\n";
    }
    
    public function get($key) {
        echo "获取缓存: {$key}\n";
        return null;
    }
}

我的选择标准:抽象类和接口怎么选?我个人习惯是:如果「是什么」的关系,用抽象类(比如 Car extends Vehicle);如果「能做什么」的关系,用接口(比如 Car implements Drivable)。说白了,抽象类描述本质,接口描述能力。

多态的实现:一个接口,多种形态

多态,说白了就是「同样的方法调用,不同的对象有不同的表现」。PHP 的多态主要靠接口和抽象类来实现。

<?php
interface Payment {
    public function pay($amount);
}

class Alipay implements Payment {
    public function pay($amount) {
        echo "使用支付宝支付 {$amount} 元\n";
    }
}

class WechatPay implements Payment {
    public function pay($amount) {
        echo "使用微信支付 {$amount} 元\n";
    }
}

class Order {
    public function checkout(Payment $payment, $amount) {
        $payment->pay($amount);
    }
}

// 使用多态
$order = new Order();
$order->checkout(new Alipay(), 100);   // 支付宝支付
$order->checkout(new WechatPay(), 200); // 微信支付

你看,checkout() 方法根本不知道具体是哪个支付方式,它只认 Payment 接口。这就是多态的魅力——代码写一次,能应对各种变化。

实战心得:我在做电商项目时,支付方式从最初的支付宝、微信,后来加了银联、PayPal。如果不用多态,每个支付方式都要写一堆 if-else。用了接口多态后,新增支付方式只需要新建一个类实现 Payment 接口,其他地方一行代码都不用改。这就是面向对象设计的威力。

总结一下

继承和多态,是面向对象设计的基石。记住几个要点:

  • 继承:用 extends,子类复用父类代码,但别滥用,三层以内最合适
  • 重写:子类改父类方法,注意签名一致
  • parent:子类想调用父类方法时用,别忘本
  • final:禁止继承或重写,核心方法才用
  • 抽象类 vs 接口:抽象类描述「是什么」,接口描述「能做什么」
  • 多态:面向接口编程,让代码拥抱变化

嗯,这一章就到这儿。下一章咱们聊聊「封装与访问控制」,看看怎么把不该暴露的东西藏起来。到时候见。