2024 年 PHP Conference Japan

類別抽象

PHP 擁有抽象類別、方法和屬性。 定義為抽象的類別無法被實例化,且任何包含至少一個抽象方法或屬性的類別也必須是抽象的。 定義為抽象的方法僅宣告方法的簽章及其是公開或保護的;它們不能定義實作。定義為抽象的屬性可以宣告`get` 或 `set` 行為的需求,並且可以為其中一個操作提供實作,但不能同時為兩個操作提供實作。

當繼承自抽象類別時,所有在父類別宣告中標記為抽象的方法都必須由子類別定義,並且遵循一般的 繼承簽章相容性 規則。

從 PHP 8.4 開始,抽象類別可以宣告抽象屬性,可以是公開或保護的。受保護的抽象屬性可以由可從受保護或公開範圍讀取/寫入的屬性滿足。

一個抽象屬性可以由一個標準屬性或一個定義了與所需操作相對應的鉤子 (hooks) 的屬性來滿足。

範例 #1 抽象方法範例

<?php

abstract class AbstractClass
{
// 強制繼承的類別定義此方法
abstract protected function getValue();
abstract protected function
prefixValue($prefix);

// 共用方法
public function printOut()
{
print
$this->getValue() . "\n";
}
}

class
ConcreteClass1 extends AbstractClass
{
protected function
getValue()
{
return
"ConcreteClass1";
}

public function
prefixValue($prefix)
{
return
"{$prefix}ConcreteClass1";
}
}

class
ConcreteClass2 extends AbstractClass
{
public function
getValue()
{
return
"ConcreteClass2";
}

public function
prefixValue($prefix)
{
return
"{$prefix}ConcreteClass2";
}
}

$class1 = new ConcreteClass1();
$class1->printOut();
echo
$class1->prefixValue('FOO_'), "\n";

$class2 = new ConcreteClass2();
$class2->printOut();
echo
$class2->prefixValue('FOO_'), "\n";

?>

上述範例將輸出

ConcreteClass1
FOO_ConcreteClass1
ConcreteClass2
FOO_ConcreteClass2

範例 #2 抽象方法範例

<?php

abstract class AbstractClass
{
// 抽象方法只需要定義必要的參數
abstract protected function prefixName($name);
}

class
ConcreteClass extends AbstractClass
{
// 子類別可以定義在父類別簽章中不存在的選用參數
public function prefixName($name, $separator = ".")
{
if (
$name == "Pacman") {
$prefix = "Mr";
} elseif (
$name == "Pacwoman") {
$prefix = "Mrs";
} else {
$prefix = "";
}

return
"{$prefix}{$separator} {$name}";
}
}

$class = new ConcreteClass();
echo
$class->prefixName("Pacman"), "\n";
echo
$class->prefixName("Pacwoman"), "\n";

?>

上述範例將輸出

Mr. Pacman
Mrs. Pacwoman

範例 #3 抽象屬性範例

<?php

abstract class A
{
// 繼承的類別必須具有可公開讀取的屬性
abstract public string $readable {
get;
}

// 繼承的類別必須具有 protected 或 public 可寫入的屬性
abstract protected string $writeable {
set;
}

// 繼承的類別必須具有 protected 或 public 可讀寫的屬性
abstract protected string $both {
get;
set;
}
}

class
C extends A
{
// 這滿足了需求,並且也使其可設定,這是有效的
public string $readable;

// 這將*不*滿足需求,因為它不是公開可讀的
protected string $readable;

// 這完全滿足了需求,因此是足夠的。
// 它只能被寫入,並且只能從 protected 範圍內寫入
protected string $writeable {
set => $value;
}

// 這將可見度從 protected 擴展到 public,這是可以的
public string $both;
}

?>

抽象類別上的抽象屬性可以為任何 hook 提供實作,但必須宣告 getset 其中之一,但不定義它們(如上例所示)。

範例 #4 抽象屬性範例

<?php

abstract class A
{
// 這提供了一個預設(但可覆寫)的 set 實作,
// 並且要求子類別提供 get 實作
abstract public string $foo {
get;

set {
$this->foo = $value;
}
}
}

?>
新增註解

使用者貢獻的註解 16 則註解

750
ironiridis at gmail dot com
16 年前
再一次,用最簡單的術語來說明

介面(Interface)就像是一種協議。它並不指定物件的行為;它指定的是你的程式碼如何指示該物件運作。介面就像英文:定義一個介面,就定義了你的程式碼如何與任何實作該介面的物件進行通訊。

介面始終是一種協議或承諾。當一個類別說「我實作了介面 Y」時,它是在說「我保證擁有與任何具有介面 Y 的物件相同的公開方法」。

另一方面,抽象類別(Abstract Class)就像一個部分建構好的類別。它很像一份有待填寫空白的檔案。它可能使用英文,但這不如文件中某些部分已經寫好的事實來得重要。

抽象類別是另一個物件的基礎。當一個類別說「我繼承了抽象類別 Y」時,它是在說「我使用了在名為 Y 的另一個類別中已經定義的一些方法或屬性」。

那麼,考慮以下 PHP 程式碼
<?php
class X implements Y { } // 意思是「X」同意使用「Y」語言與你的程式碼溝通。

class X extends Y { } // 意思是「X」將會完成部分類別「Y」。
?>

如果你要發佈一個類別供其他人使用,你應該讓你的類別實作特定的介面。介面是一種協議,用於確保你的類別具有一組特定的公開方法。

如果你(或其他人)寫了一個類別,其中已經包含一些你想在新類別中使用的方法,你應該讓你的類別繼承這個抽象類別。

這些概念雖然容易混淆,但卻截然不同。實際上,如果你是你的所有類別的唯一使用者,你就不需要實作介面。
293
mbajoras at gmail dot com
14 年前
以下是一個幫助我理解抽象類別的例子。這只是一種非常簡單的解釋方式(在我看來)。假設我們有以下程式碼

<?php
類別 水果 {
私有
$顏色;

公開 函式
() {
//咀嚼
}

公開 函式
設定顏色($c) {
$this->顏色 = $c;
}
}

類別
蘋果 繼承 水果 {
公開 函式
() {
//咀嚼到果核
}
}

類別
柳橙 繼承 水果 {
公開 函式
() {
//剝皮
//咀嚼
}
}
?>

現在我給你一個蘋果,你吃它。

<?php
$蘋果
= new 蘋果();
$蘋果->();
?>

嚐起來是什麼味道?嚐起來像蘋果。現在我給你一個水果。

<?php
$水果
= new 水果();
$水果->();
?>

那嚐起來像什麼???嗯,這不太合理,所以你不應該這樣做。這可以透過將 水果 類別設定為抽象類別,以及將其中的 吃 方法設定為抽象方法來實現。

<?php
抽象類別 水果 {
私有
$顏色;

抽象 公開 函式
();

公開 函式
設定顏色($c) {
$this->顏色 = $c;
}
}
?>

現在想想一個資料庫類別,MySQL 和 PostgreSQL 繼承它。另外,請注意,抽象類別就像介面,但你可以在抽象類別中定義方法,而在介面中,它們都是抽象的。
5
shewa12kpi at gmail dot com
3 年前
<?php
//這裡是一個抽象類別的良好範例。BaseEmployee 並非實際的員工,它只是一個抽象類別,用於減少程式碼並強制子類別實作抽象方法

abstract class BaseEmployee {

/**
* 員工的共同屬性可以放在抽象類別中
*/
public $firstname,
$lastname;

function
__construct($fn, $ln){
$this->firstname = $fn;
$this->lastname = $ln;
}

public function
getFullName() {

return
"$this->firstname $this->lastname";
}

/**
* 子類別必須定義的抽象方法
*/
abstract protected static function task();
}

class
WebDeveloper extends BaseEmployee {

static function
task()
{
return
' 開發網頁應用程式';
}
}

class
HR extends BaseEmployee {

static function
task()
{
return
' 管理人力資源';
}

}
/**
* 現在實例化並取得資料
*/
$webDeveloper = new WebDeveloper('shaikh','ahmed');
echo
$webDeveloper->getFullName();
echo
$webDeveloper->task();
29
jai at shaped dot ca
7 年前
希望此範例能幫助您了解抽象類別的運作方式、介面的運作方式,以及它們如何協同運作。此範例也可以在 PHP7 上運作/編譯,其他範例是在表單中即時輸入的,可能可以運作,但最後一個範例是實際製作/測試的。

<?php

const = PHP_EOL;

// Define things a product *has* to be able to do (has to implement)
interface productInterface {
public function
doSell();
public function
doBuy();
}

// Define our default abstraction
abstract class defaultProductAbstraction implements productInterface {
private
$_bought = false;
private
$_sold = false;
abstract public function
doMore();
public function
doSell() {
/* the default implementation */
$this->_sold = true;
echo
"defaultProductAbstraction doSell: {$this->_sold}".;
}
public function
doBuy() {
$this->_bought = true;
echo
"defaultProductAbstraction doBuy: {$this->_bought}".;
}
}

class
defaultProductImplementation extends defaultProductAbstraction {
public function
doMore() {
echo
"defaultProductImplementation doMore()".;
}
}

class
myProductImplementation extends defaultProductAbstraction {
public function
doMore() {
echo
"myProductImplementation doMore() does more!".;
}
public function
doBuy() {
echo
"myProductImplementation's doBuy() and also my parent's dubai()".;
parent::doBuy();
}
}

class
myProduct extends defaultProductImplementation {
private
$_bought=true;
public function
__construct() {
var_dump($this->_bought);
}
public function
doBuy () {
/* non-default doBuy implementation */
$this->_bought = true;
echo
"myProduct overrides the defaultProductImplementation's doBuy() here {$this->_bought}".;
}
}

class
myOtherProduct extends myProductImplementation {
public function
doBuy() {
echo
"myOtherProduct overrides myProductImplementations doBuy() here but still calls parent too".;
parent::doBuy();
}
}

echo
"new myProduct()".;
$product = new myProduct();

$product->doBuy();
$product->doSell();
$product->doMore();

echo
."new defaultProductImplementation()".;

$newProduct = new defaultProductImplementation();
$newProduct->doBuy();
$newProduct->doSell();
$newProduct->doMore();

echo
."new myProductImplementation".;
$lastProduct = new myProductImplementation();
$lastProduct->doBuy();
$lastProduct->doSell();
$lastProduct->doMore();

echo
."new myOtherProduct".;
$anotherNewProduct = new myOtherProduct();
$anotherNewProduct->doBuy();
$anotherNewProduct->doSell();
$anotherNewProduct->doMore();
?>

結果如下
<?php
/*
new myProduct()
bool(true)
myProduct 在此覆寫 defaultProductImplementation 的 doBuy() here 1
defaultProductAbstraction doSell: 1
defaultProductImplementation doMore()

new defaultProductImplementation()
defaultProductAbstraction doBuy: 1
defaultProductAbstraction doSell: 1
defaultProductImplementation doMore()

new myProductImplementation
myProductImplementation 的 doBuy() 以及其父類別的 dubai()
defaultProductAbstraction doBuy: 1
defaultProductAbstraction doSell: 1
myProductImplementation doMore() 做更多事!

new myOtherProduct
myOtherProduct 在此覆寫 myProductImplementations 的 doBuy(),但仍會呼叫父類別
myProductImplementation 的 doBuy() 以及其父類別的 dubai()
defaultProductAbstraction doBuy: 1
defaultProductAbstraction doSell: 1
myProductImplementation doMore() 做更多事!

*/
?>
66
a dot tsiaparas at watergate dot gr
13 年前
抽象類別和介面是兩個截然不同的工具,它們就像榔頭和電鑽一樣各有用途。抽象類別可以包含已實作的方法,而介面本身則不包含任何實作。

將所有方法都宣告為抽象的抽象類別並非只是換個名稱的介面。一個類別可以實作多個介面,但不能繼承多個類別(或抽象類別)。

抽象類別與介面的使用取決於具體問題,並且這個選擇是在軟體設計階段做出的,而不是在實作階段。在同一個專案中,您可以同時提供一個介面和一個作為參考的基底類別(可能是一個抽象類別)來實作該介面。為什麼要這樣做呢?

假設我們要建構一個呼叫不同服務的系統,而這些服務又各自有不同的動作。通常,我們可以提供一個名為 execute 的方法,它接受動作名稱作為參數並執行該動作。

我們希望確保類別能夠自行定義執行動作的方式。因此,我們建立一個具有 execute 方法的 IService 介面。然而,在大部分情況下,您會不斷地複製和貼上相同的 execute 程式碼。

我們可以為名為 Service 的類別建立一個參考實作,並實作 execute 方法。這樣,您的其他類別就不用再複製和貼上了!但是如果您想要繼承 MySLLi 呢?您可以實作介面(可能還是需要複製貼上),然後您就再次擁有了另一個服務。抽象類別可以用於包含初始化程式碼,而這些程式碼無法為您將要編寫的每個類別預先定義。

希望這不會太燒腦,並且對某些人有所幫助。謝謝!
Alexios Tsiaparas
12
swashata4u at gmail dot com
6 年前
關於抽象類別和介面,還有另一件事。

有時,我們會為一個「工廠 (Factory)」定義一個介面,並透過一個「抽象類別」簡化該「工廠」的一些常用方法。

在這種情況下,抽象類別會實作該介面,但不需要實作介面的所有方法。

原因很簡單,任何實作介面的類別都需要實作所有方法,或者將自身宣告為抽象類別。

因此,以下程式碼是完全沒問題的。

<?php
interface Element {
/**
* 建構子函式。必須傳入現有的設定,或保持原樣以建立新元素,
* 在這種情況下,將使用預設值。
*
* @param array $config 元素設定。
*/
public function __construct( $config = [] );

/**
* 取得元素的定義。
*
* @return array 包含 'title'、'description' 和 'type' 的陣列
*/
public static function get_definition();

/**
* 取得元素設定變數。
*
* @return array 元素設定的關聯陣列。
*/
public function get_config();

/**
* 設定元素設定變數。
*
* @param array $config 新的設定變數。
*
* @return void
*/
public function set_config( $config );
}

abstract class
Base implements Element {

/**
* 元素設定變數
*
* @var array
*/
protected $config = [];

/**
* 取得元素設定變數。
*
* @return array 元素設定的關聯陣列。
*/
public function get_config() {
return
$this->config;
}

/**
* 建立 eForm 元素實例
*
* @param array $config 元素設定。
*/
public function __construct( $config = [] ) {
$this->set_config( $config );
}
}

class
MyElement extends Base {

public static function
get_definition() {
return [
'type' => 'MyElement',
];
}

public function
set_config( $config ) {
// 在此處執行一些操作
$this->config = $config;
}
}

$element = new MyElement( [
'foo' => 'bar',
] );

print_r( $element->get_config() );
?>

您可以在這裡看到正在執行的測試,並且在 PHP 5.4 以上版本中,輸出是一致的。https://3v4l.org/8NqqW
19
shaman_master at list dot ru
6 年前
您也可以為抽象方法設定回傳/參數類型宣告 (PHP>=7.0)
<?php
declare(strict_types=1);

abstract class
Adapter
{
protected
$name;
abstract public function
getName(): string;
abstract public function
setName(string $value);
}

class
AdapterFoo extends Adapter
{
public function
getName(): string
{
return
$this->name;
}
// 在抽象類別中未定義回傳型別,在此設定
public function setName(string $value): self
{
$this->name = $value;
return
$this;
}
}
?>
8
Eugeny at Kostanay dot KZ
8 年前
這段程式碼片段可以幫助您更了解抽象類別中的屬性
<?php
abstract class anotherAbsClass
{
// 定義並設定靜態屬性
static $stProp = 'qwerty'; // 我們仍然可以直接以靜態方式使用它
// 定義並設定一個受保護的屬性
protected $prProp = 'walrus';
// 為抽象類別的非靜態變數設定任何其他可見性層級是沒有用的。
// 即使在抽象類別的已宣告方法內,我們也無法存取私有屬性,因為我們無法在物件上下文中呼叫該方法。
// 實作一個通用方法
protected function callMe() {
echo
'呼叫中: ' . $this->prProp . PHP_EOL;
}
// 宣告一些抽象方法
abstract protected function abc($arg1, $arg2);
abstract public function
getJunk($arg1, $arg2, $arg3, $junkCollector = true);
// 注意:如果抽象類別已宣告選用值,則不能省略它,否則會發生錯誤
}

class
someChildClass extends anotherAbsClass
{
function
__construct() {
echo
$this->callMe() . PHP_EOL; // 現在我們取得從抽象類別繼承的受保護屬性 $prProp
}
// 必須在下方實作已宣告的函式 abc 和 getJunk
protected function abc($val1, $val) {
// 做一些事情
}
function
getJunk($val1, $val2, $val3, $b = false) { // 選用值是必要的,因為它已在上方宣告
// 做一些事情
}
}

echo
anotherAbsClass::$stProp; // qwerty
$objTest = new someChildClass; // 呼叫中: walrus
?>
69
joelhy
13 年前
文件說明:「不允許建立已定義為抽象的類別的實例。」這僅表示您無法從抽象類別初始化物件。呼叫抽象類別的靜態方法仍然可行。例如
<?php
抽象類別 Foo
{
靜態函式
bar()
{
echo
"test\n";
}
}

Foo::bar();
?>
20
sneakyimp at hotmail dot com
17 年前
好的...關於抽象類別繼承另一個抽象類別的說明文件有點模糊。繼承另一個抽象類別的抽象類別不需要定義父類別的抽象方法。換句話說,這會導致錯誤

<?php
抽象類別 class1 {
抽象公開函式
someFunc();
}
抽象類別
class2 繼承 class1 {
抽象公開函式
someFunc();
}
?>

錯誤:致命錯誤:無法繼承抽象函式 class1::someFunc()(先前在 class2 中宣告為抽象) 於 /home/sneakyimp/public/chump.php 的第 7 行

然而,這樣做則不會

<?php
抽象類別 class1 {
抽象公開函式
someFunc();
}
抽象類別
class2 繼承 class1 {
}
?>

繼承抽象類別的抽象類別可以將實作其父類別抽象方法的責任傳遞給其子類別。
19
bishop
14 年前
順帶一提,抽象類別不一定是基底類別

<?php
類別 Foo {
公開函式
sneeze() { echo 'achoooo'; }
}

抽象類別
Bar 繼承 Foo {
公開抽象函式
hiccup();
}

類別
Baz 繼承 Bar {
公開函式
hiccup() { echo 'hiccup!'; }
}

$baz = new Baz();
$baz->sneeze();
$baz->hiccup();
?>
8
jai at shaped dot ca
7 年前
介面指定了類別必須實作的方法,以便任何使用該類別並期望其遵循該介面的程式碼都能正常運作。

例如:我期望任何 $database 都具有 ->doQuery(),因此我指定給 database 介面的任何類別都應該實作 databaseInterface 介面,這會強制實作 doQuery 方法。

<?php
介面 dbInterface {
公開函式
doQuery();
}

類別
myDB 實作 dbInterface {
公開函式
doQuery() {
/* 實作細節寫在此處 */
}
}

$myDBObj = new myDB()->doQuery();
?>

抽象類別與介面類似,但可以預先定義一些方法。被列為抽象的方法必須像介面一樣被定義。

例如,我希望我的 $person 可以 ->doWalk(),大多數人可以用兩隻腳正常行走,但有些人必須單腳跳 :(

<?php
interface PersonInterface {
/* 每個人都應該行走,或嘗試行走 */
public function doWalk($place);
/* 每個人都應該能夠變老 */
public function doAge();
}

abstract class
AveragePerson implements PersonInterface {
private
$_age = 0;
public function
doAge() {
$this->_age = $this->_age+1;
}
public function
doWalk($place) {
echo
"我要走到 $place".PHP_EOL;
}
/* 每個人的說話方式都不同! */
abstract function talk($say);
}

class
Joe extends AveragePerson {
public function
talk($say) {
echo
"Joe 用澳洲口音說: $say".PHP_EOL;
}
}

class
Bob extends AveragePerson {
public function
talk($say) {
echo
"Bob 用加拿大口音說: $say".PHP_EOL;
}
public function
doWalk($place) {
echo
"Bob只有一條腿,必須跳到 $place".PHP_EOL;
}
}

$people[] = new Bob();
$people[] = new Joe();

foreach (
$people as $person) {
$person->doWalk('那邊');
$person->talk('PHP 萬歲');
}
?>
7
joebert
17 年前
我不同意 jfkallens 對抽象類別和物件介面的最後一個比較。

在抽象類別中,您可以定義某些方法的運作方式,而在物件介面中則不能。

物件介面本質上只是一個函式名稱列表,如果類別實作該介面,則必須定義這些函式名稱。

抽象類別本質上是一個原型,它暗示了繼承類別應該做什麼。
抽象類別也可以被認為是一個提供一些基本功能的基底類別,並且還定義了一個所有繼承類別都將實作的內建物件介面。

所以,物件介面實際上是抽象類別的內建組成部分。
3
Malcolm
8 年前
我在「範例 #2 抽象類別範例」中發現了一個不一致的地方:

如果您移除 $separator 的預設值

<?php
public function prefixName($name, $separator) {
// ...
}
?>

PHP 就會顯示以下致命錯誤訊息:
Fatal error: Declaration of ConcreteClass::prefixName() must be compatible with AbstractClass::prefixName($name) in /index.php on line 23 (致命錯誤:ConcreteClass::prefixName() 的宣告必須與 AbstractClass::prefixName($name) 相容,位於 /index.php 的第 23 行)

奇怪的是,它給出了錯誤的 "ConcreteClass::prefixName()" 宣告... 它缺少兩個參數。因此,我假設這只是一個錯誤,可能在新版本中已經被修正了。(或者只是我的版本特有)我主要記錄這一點是因為它在我編寫的一些衍生自範例 #2 的測試程式碼中(沒有額外參數的預設值)讓我非常抓狂。或許這可以為其他人省去一些挫折感。

--
請注意,我是在 PHP 5.5 上執行的。
作業系統:ubuntu-16.04-server-amd64.iso
軟體源:ppa:ondrej/php

# php5.5 --version
PHP 5.5.36-2+donate.sury.org~xenial+1 (cli)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2015 Zend Technologies with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2015, by Zend Technologies
2
arma99eDAN at yahoo dot com
9 年前
您也可以像這樣使用抽象類別

abstract class A{
public function show(){
echo 'A';
}
}
} // 需在此處加上閉合大括號
public function hello(){
echo 'B';
parent::show();
}
}

} // 需在此處加上閉合大括號
$obj->hello(); // BA
# 注意,這個抽象類別沒有任何抽象方法
# 即使在這種情況下,我仍然可以繼承它,或呼叫它的非抽象成員
0
designbyjeeba at gmail dot com
13 年前
請注意父類別欄位的可見性。如果欄位是私有的,那麼您將不會在子類別中看到這些欄位。這是基本的物件導向程式設計概念,但有時會造成問題。
To Top