PHP 面向?qū)ο笃豪^承、封裝與多態(tài)
2023-06-03 加入收藏
1、繼承
所謂繼承,指的是子類可以通過繼承的方式訪問父類的屬性和方法(protected
或者 public
方式定義),在 PHP 中,繼承通過 extends
關(guān)鍵字實現(xiàn),我們以上篇教程編寫的 Car
類為例,編寫一個實現(xiàn)該類的子類 Benz
(仍然在 class.php
中定義):
class Benz extends Car
{
public function __construct($seats = 5, $doors = 4, $engine = 1)
{
$this->brand = '奔馳';
// $this->setBrand('奔馳'); // 也可以通過該方法設(shè)置
parent::__construct($this->brand, $seats, $doors, $engine);
}
}
這里 extends Car
的含義就是 Benz
繼承自 Car
,是它的子類,相對的,Car
是 Benz
的父類。
在子類 Benz
的構(gòu)造函數(shù)中,我們將品牌設(shè)置為「奔馳」,然后通過 parent::__construct
調(diào)用父類的構(gòu)造函數(shù)進(jìn)行初始化(調(diào)用父類的同名方法需要通過 parent::
進(jìn)行調(diào)用,否則 PHP 會不知道調(diào)用父類還是子類的方法),這樣,初始化 Benz
對象時,就無須傳入品牌參數(shù)了。
可以看到,在子類中可以通過 $this
對象直接訪問父類定義的屬性和方法,前提是該屬性或方法的可見性是 protected
或者 public
級別,如果試圖訪問 private
聲明的屬性或方法,PhpStorm 會警告:

運(yùn)行代碼也會報錯。
另外,我們也可以通過子類對象訪問父類方法(在子類函數(shù)體中訪問父類方法,通過 $this
即可):
$benz = new Benz();
$benz->drive();
上述代碼的執(zhí)行結(jié)果如下:

可以看到子類可以繼承父類所有通過 protected
和 public
聲明的屬性和方法,并且在調(diào)用過程中自動將 $this
指針引用指向子類對象,對于 public
屬性和方法,和父類一樣,直接可以在類外部通過 ->
操作符調(diào)用。
當(dāng)然,你也可以在子類中新增一些獨(dú)有的屬性和方法:
class Benz extends Car
{
private $customProp = "自定義屬性";
...
public function customMethod()
{
echo "Call custom prop \$customProp: " . $this->customProp . PHP_EOL;
echo "This is a custom method in Benz Class" . PHP_EOL;
}
}
PHP 遵循單繼承機(jī)制,即一個子類只能繼承自一個父類。
2、封裝
概念解釋
封裝一方面指的是調(diào)用者無需關(guān)心對象方法實現(xiàn)細(xì)節(jié),比如我們要開車,就調(diào)用 $car->drive()
方法即可,不用編寫具體的實現(xiàn)邏輯,也不用去關(guān)心(調(diào)用了那些屬性、那些方法、不管是私有的還是公開的、當(dāng)前類的還是其他類的,統(tǒng)統(tǒng)不用關(guān)心),就像我們在真實世界中開車一樣,只需要按照流程來操作就好了,不用關(guān)心汽車引擎內(nèi)部是如何工作的。
另一方面是通過訪問控制限定屬性和方法的可見性,比如 public
修飾的屬性和方法所有地方可見,不管是當(dāng)前類、子類還是類之外,protected
修飾的屬性和方法在當(dāng)前類和子類中可見,而 private
修飾的屬性和方法僅在當(dāng)前類可見,你可以根據(jù)自己的業(yè)務(wù)需要合理的設(shè)置屬性和方法的可見性。
反射
不過,饒是如此,依然可以通過反射的方式將 protected
或者 private
級別的屬性和方法變成類以外可以訪問,比如我們將 Benz
類中的 customMethod
方法設(shè)置為私有的:
class Benz extends Car
{
private $customProp = '自定義屬性';
...
private function customMethod()
{
echo "Call custom prop \$customProp: " . $this->customProp . PHP_EOL;
echo "This is a custom method in Benz Class" . PHP_EOL;
}
}
在類外直接調(diào)用會報錯:

我們通過反射來調(diào)用這個方法,可以這么做:
// 通過反射調(diào)用非 public 方法
$method = new ReflectionMethod(Benz::class, 'customMethod');
$method->setAccessible(true);
$benz = new Benz();
$method->invoke($benz);
打印結(jié)果和調(diào)用聲明為 public
的 customMethod
方法完全一樣,如果將 private
修改為 protected
效果也一樣,通過反射,我們可以在運(yùn)行時以逆向工程的方式對 PHP 類進(jìn)行實例化,并對類中的屬性和方法進(jìn)行動態(tài)調(diào)用,不管這些屬性和方法是否對外公開,所以這是一個黑科技,更多反射的細(xì)節(jié)可以參考 PHP 官方文檔:https://www.php.net/manual/zh/book.reflection.php。
3、多態(tài)
方法重寫
所謂多態(tài),指的是在 PHP 繼承體系中,子類可以重寫父類的同名方法,這樣,在子類對象中調(diào)用該方法,就會自動轉(zhuǎn)發(fā)到子類方法調(diào)用,還是以 Car
和 Benz
為例,我們在子類中重寫父類的 drive
方法(所謂重寫,英文是 override,即在子類中編寫和父類同名方法,來覆蓋父類的實現(xiàn)):
class Benz extends Car
{
...
// 重寫父類實現(xiàn)
public function drive()
{
echo $this->getBrand() . '汽車的啟動流程:' . PHP_EOL;
parent::drive(); // TODO: Change the autogenerated stub
}
}
我們在子類的 drive
方法中,先打印了一段提示文本,然后和構(gòu)造函數(shù)一樣,通過 parent::drive
調(diào)用父類的同名方法,因為所有的汽車啟動流程基本都是一樣的。
接下來,我們通過子類對象調(diào)用 drive
方法:
$benz = new Benz();
$benz->drive();
打印結(jié)果如下:

包含了第一行提示文本,所以,調(diào)用的是子類的方法而不是父類的。
類型轉(zhuǎn)化
PHP 不像 Java 那樣支持同一個類中定義多個同名方法(參數(shù)數(shù)量或類型不同,這種叫做方法重載),另外,由于子類一定包含了父類的公開方法,所以當(dāng)類作為參數(shù)類型聲明時,如果聲明類型為父類,則可以傳入子類對象,反過來,如果聲明類型為子類,則不能傳入父類對象。
比如我們定義一個測試汽車類啟動功能的測試類和方法如下,并編寫一段測試代碼:
class TestCarDrive
{
public function testDrive(Car $car)
{
$car->drive();
}
public function testBenzDrive(Benz $benz)
{
$benz->drive();
}
}
// 初始化類對象
$bmw = new Car('寶馬');
$benz = new Benz();
$test = new TestCarDrive();
// 測試子類轉(zhuǎn)父類
$test->testDrive($benz);
// 測試父類轉(zhuǎn)子類
$test->testBenzDrive($bmw);
上述代碼第一個測試 $test->testDrive
可以正常運(yùn)行,第二個會報錯:
