物件導向設計
Object-Oriented Programming(簡稱 OOP)
OOP 是一種現今程式語言幾乎都支援的一種設計範式,舉凡 PHP
, Javascript
, Python
, Java
...等,其運作模式就是規劃藍圖並依此進行實例化產生物件的一套流程,也可以規範藍圖上應該要有什麼方屬性、方法。
前言
不同語言的作法皆有所差異,接下來僅一律以 PHP 作為範例,以不同語言操作前請先了解該語言的特性與實踐方式。
核心元素
- 類別 - class
- 屬性 - property
- 方法 - function
核心概念
其他概念
其他
核心概念
這些內容是 OOP 的核心概念,只要缺少其中之一就不能被稱作是真正的物件導向設計。
封裝 Encapsulation
最頻繁操作的封裝便是設計藍圖(class)。
舉例來說,我想設計「鳥類」進到我的程式中,就可以透過封裝來完成。
php
class Bird{
public function __construct(
public int $eyes = 2,
public string $color = 'white'
){}
public function call(){
return 'chirp! chirp! chirp!';
}
}
$whiteBird = new Bird();
echo $whiteBird->color; // white
echo $whiteBird->call(); // chirp! chirp! chirp!
$blackBird = new Bird(color: 'black');
echo $blackBird->color; // black
echo $blackBird->call(); // chirp! chirp! chirp!
在這個範例中設計了名為 Bird
的 class,並且產生了 $blackBird
及 $whiteBird
兩個實例(Instance),它們都包含兩個屬性及一個函式,可以簡單的理解為
- class - 類別,如:鳥類(
Bird
) - property - 特徵,如:眼睛數(
$eyes
)、毛色($color
) - function - 行為,如:鳴叫(
call()
)
在實例化 Bird
時我們還傳入了 color
,這些傳入的參數都會經過可選但必經的函式 __construct()
進行建構初始化,與之相反的是在銷毀時同樣可選但必經的函式 __destructor()
進行解構收尾。
繼承 Inheritance
當 class 間存在共通點時,可以使用 extends
繼承現有類別來設計新的類別,這樣一來新的類別便可以調用繼承對象的屬性、函式,並且可以在此基礎上增加自己的屬性、函式。
php
class Duck extends Bird{
public function __construct(){
parent::__construct();
}
public function swim(){
return 'duck swimming...';
}
}
$duck = new Duck();
echo $duck->color; // white
echo $duck->call(); // chirp! chirp! chirp!
echo $duck->swim(); // duck swimming...
名為 Duck
的 class 繼承了 Bird
,這樣一來便不再需重新設計已存在於繼承對象中的屬性、函式。
若需要覆寫,亦可在 Duck
內直接設計相同屬性、函式。
注意
繼承者內部必須透過 parent
才能調用繼承對象的屬性、函式。
多型 Polymorphism
但即便 Duck
從 Bird
繼承了內容,其屬性及函式也不一定全然與繼承對象相同對吧?
這時我們便可以根據需求在繼承者內部重新設計,而這個行為便稱為多型。
php
class Duck extends Bird{
public string $color = 'black';
public function __construct(){
parent::__construct();
}
public function call(){
return 'quack! quack! quack!';
}
}
$duck = new Duck();
echo $duck->color; // black
echo $duck->call(); // quack! quack! quack!
如此一來,$duck
的屬性 $color
及函式 call()
便改變了,簡而言之就是 class 的覆寫。
抽象類 Abstract Class
到目前為止我們都能夠隨心所欲的設計類別,但如果某類別本身不應被實例化,或者被實例化的意義不大,且想要約束繼承者必須實現指定函式呢?
這時便可以使用 Abstract Class 來達成這個目的,好處是可以節省維護、溝通成本,不必再花時間去熟悉或猜測類別會有什麼函式或者其返回的類型,拿前面的 Bird
及 Duck
為例。
php
abstract class Bird{
// ...
abstract public function call(): string;
}
class Duck extends Bird{
// ...
}
// ❌ Class Duck contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Bird::swim)
$bird = new Bird(); // ❌ 錯誤:Cannot instantiate abstract class Bird
這個範例中出現兩個錯誤
Duck
因繼承Bird
而包含了一個抽象函式,必須實現該函式。Bird
為抽象類,無法被直接實例化。
當解決以上兩個錯誤後,繼承者便可調用繼承對象的非抽象內容,同時達成約束函式的目的。
補充
部分語言還存在一種介面 Interface 的概念,PHP 介面與抽象類的差異如下
- 抽象類可以提供屬性、函式給繼承者繼承,而介面只能進行函式約束。
- 抽象類可以約束
protected
函式,而介面只允許約束public
函式。 - 繼承者同時只能繼承一個抽象類,但可以同時遵從多個介面。
其他概念
這些內容是常見但並非所有支援 OOP 的語言都絕對存在的,不同語言的支援度或者設計方式有所不同。
可見度 Visibility
所有的內容都可以被繼承嗎?不,private
是無法被繼承的特例。
- public - 公開,可以在任何地方被使用。
- private - 私有,僅實例自身可用。
- protected - 保護,僅實例自身及繼承者可用。
這些修飾符能夠規範屬性、函式能在什麼時候被調用。
靜態性質 Static / Non-Static
一定要實例化後才能使用類別的屬性、函式嗎?不,靜態屬性、函式不受此限制。
php
class Calculator{
public static $name = '計算機';
public static function increase(int $x, int $y){
return $x + $y;
}
}
echo Calculator::increase(10, 20); // 30
警告
靜態屬性異動後的任何調用皆是異動後的狀態,應謹慎使用避免產生意外狀況。
php
echo Calculator::$name; // 計算機
Calculator::$name = '只有加法的計算機';
echo Calculator::$name; // ⚠️ 只有加法的計算機
其他
為什麼需要OOP?
範例需求
某系統存在兩種點數類型,他們分別對應不同的資料表、不同的算法、不同的Log。
非 OOP 設計
php
$point_one = [
'table' => 'point_one',
'scale' => 0.1,
'count' => function(array $order) use ($point_one): float{
return $order['price'] * $point_one['scale'];
},
'execute' => function(array $order) use ($point_one): void{
echo "do something to database {$point_one['table']}";
},
'log' => function(array $order) use ($point_one): void{
echo "write logs for {$point_one['table']}";
}
];
$point_two = [
'table' => 'point_two',
'scale' => 0.2,
'count' => function(array $order) use ($point_two): float{
return $order['price'] * $point_two['scale'];
},
'execute' => function(array $order) use ($point_two): void{
echo "do something to database {$point_two['table']}";
},
'log' => function(array $order) use ($point_two): void{
echo "write logs for {$point_two['table']}";
}
];
優點:
- 簡易,初學者容易理解。
缺點:
- 無法透過任何規範約束應該實現哪些函式。
例:point_one 有 execute() 而 point_two 卻忘了設計,導致開發人員調用失敗。 - 無法針對屬性、函式進行可見度、修改權限控管。
例:point_one table 被修改為 point_two 導致不可預期的行為發生。 - 無法斷言屬性類型,無法確保正確性。
例:scale 被設定為 null,雖部分語言仍能進行運算,但卻導致不可預期的意外發生。 - 多餘的代碼導致可讀性降低。
例:陣列、物件無$this
,self
,parent
概念,只能主動傳入或透過use
引入參數,增加代碼複雜度。
以 OOP 設計
php
class Point{
public function __construct(
public readonly array $order,
public readonly string $table = 'point_one',
public readonly float $scale = 0.1,
){}
public function count(): float{
return $this->order['price'] * $this->scale;
}
public function execute(): void{
echo "do something to database {$this->table}";
}
private function log(): void{
echo "write logs for {$this->table}";
}
}
優點:
- 可以透過 Interface 約束類別該實現哪些函式。
例:Point
,Order
或更多類別都需要操作資料庫異動,透過介面約束必須實現public function log(): void{}
- 可以針對屬性、函式進行可見度、修改權限控管。
例:設置public readonly string $table
使其初始化後不得被修改,避免不可預期的意外發生。
例:設置private function log(): void{}
限制其不可被類別外部調用,避免非預期的運作。 - 可以斷言屬性類別,確保正確性。
例:設置public readonly float $scale
使其必須傳入浮點數,避免不可預期的運算結果或意外發生。 - 簡化代碼。
例:透過$this
,self
,parent
調用自身或繼承對象的屬性、函式,省去手動傳入、引入的成本。 - 可以透過 Extends 繼承,基於某類別延伸的類別,透過繼承可以直接進行疊加或覆寫設計,避免不必要的重工。
例:系統需要增加一種新點數,僅基於Point
進行微調,class newPoint extends Point
即可繼續開發,毫無重工。 - 自動調用。
例:在部分語言中(如 PHP)可以在當某些屬性被修改或函式被調用時自動執行某些行為,例如範例中的execute()
被調用時可以自動執行log()
進行日誌紀錄。
缺點:
- 初學者不易理解,需額外學習。
對於我個人而言,僅討論 OOP 的情況下「非 OOP 設計的優點」及「以 OOP 設計的缺點」幾乎是不存在的,不論是以 RAS 的可靠性、可用性、可維護性三種面向來看,或者是長遠發展的可擴充性、可讀性,幾乎都是 OOP 的單方面輾壓,毫無選擇以非 OOP 方式開發的理由。
最後編輯:2025-01-15 13:03:28