物件導向設計

物件導向是一種主流的程式設計範式,以模擬現實世界中的物件和它們之間的互動為基礎,讓程式更容易理解、設計和維護。

物件導向設計

Object-Oriented Programming(簡稱 OOP)

OOP 是一種現今程式語言幾乎都支援的一種設計範式,舉凡 PHP, Javascript, Python, Java...等,其運作模式就是規劃藍圖並依此進行實例化產生物件的一套流程,也可以規範藍圖上應該要有什麼方屬性、方法。

前言

不同語言的作法皆有所差異,接下來僅一律以 PHP 作為範例,以不同語言操作前請先了解該語言的特性與實踐方式。

核心元素

  • 類別 - class
  • 屬性 - property
  • 方法 - function

核心概念

其他概念

其他


核心概念

這些內容是 OOP 的核心概念,只要缺少其中之一就不能被稱作是真正的物件導向設計。

封裝 Encapsulation

最頻繁操作的封裝便是設計藍圖(class)。
舉例來說,我想設計「鳥類」進到我的程式中,就可以透過封裝來完成。

php Copy
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 Copy
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

但即便 DuckBird 繼承了內容,其屬性及函式也不一定全然與繼承對象相同對吧?
這時我們便可以根據需求在繼承者內部重新設計,而這個行為便稱為多型。

php Copy
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 來達成這個目的,好處是可以節省維護、溝通成本,不必再花時間去熟悉或猜測類別會有什麼函式或者其返回的類型,拿前面的 BirdDuck 為例。

php Copy
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

這個範例中出現兩個錯誤

  1. Duck 因繼承 Bird 而包含了一個抽象函式,必須實現該函式。
  2. Bird 為抽象類,無法被直接實例化。

當解決以上兩個錯誤後,繼承者便可調用繼承對象的非抽象內容,同時達成約束函式的目的。

補充

部分語言還存在一種介面 Interface 的概念,PHP 介面與抽象類的差異如下

  1. 抽象類可以提供屬性、函式給繼承者繼承,而介面只能進行函式約束。
  2. 抽象類可以約束 protected 函式,而介面只允許約束 public 函式。
  3. 繼承者同時只能繼承一個抽象類,但可以同時遵從多個介面。

其他概念

這些內容是常見但並非所有支援 OOP 的語言都絕對存在的,不同語言的支援度或者設計方式有所不同。

可見度 Visibility

所有的內容都可以被繼承嗎?不,private 是無法被繼承的特例。

  • public - 公開,可以在任何地方被使用。
  • private - 私有,僅實例自身可用。
  • protected - 保護,僅實例自身及繼承者可用。

這些修飾符能夠規範屬性、函式能在什麼時候被調用。

靜態性質 Static / Non-Static

一定要實例化後才能使用類別的屬性、函式嗎?不,靜態屬性、函式不受此限制。

php Copy
class Calculator{
    public static $name = '計算機';
    public static function increase(int $x, int $y){
        return $x + $y;
    }
}

echo Calculator::increase(10, 20); // 30

警告

靜態屬性異動後的任何調用皆是異動後的狀態,應謹慎使用避免產生意外狀況。

php Copy
echo Calculator::$name; // 計算機
Calculator::$name = '只有加法的計算機';
echo Calculator::$name; // ⚠️ 只有加法的計算機

其他

為什麼需要OOP?

範例需求

某系統存在兩種點數類型,他們分別對應不同的資料表、不同的算法、不同的Log。

非 OOP 設計

php Copy
$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']}";
    }
];

優點:

  1. 簡易,初學者容易理解。

缺點:

  1. 無法透過任何規範約束應該實現哪些函式。
    例:point_one 有 execute() 而 point_two 卻忘了設計,導致開發人員調用失敗。
  2. 無法針對屬性、函式進行可見度、修改權限控管。
    例:point_one table 被修改為 point_two 導致不可預期的行為發生。
  3. 無法斷言屬性類型,無法確保正確性。
    例:scale 被設定為 null,雖部分語言仍能進行運算,但卻導致不可預期的意外發生。
  4. 多餘的代碼導致可讀性降低。
    例:陣列、物件無 $this, self, parent 概念,只能主動傳入或透過 use 引入參數,增加代碼複雜度。

以 OOP 設計

php Copy
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}";
    }
}

優點:

  1. 可以透過 Interface 約束類別該實現哪些函式。
    例:Point, Order 或更多類別都需要操作資料庫異動,透過介面約束必須實現 public function log(): void{}
  2. 可以針對屬性、函式進行可見度、修改權限控管。
    例:設置 public readonly string $table 使其初始化後不得被修改,避免不可預期的意外發生。
    例:設置 private function log(): void{} 限制其不可被類別外部調用,避免非預期的運作。
  3. 可以斷言屬性類別,確保正確性。
    例:設置 public readonly float $scale 使其必須傳入浮點數,避免不可預期的運算結果或意外發生。
  4. 簡化代碼。
    例:透過 $this, self, parent 調用自身或繼承對象的屬性、函式,省去手動傳入、引入的成本。
  5. 可以透過 Extends 繼承,基於某類別延伸的類別,透過繼承可以直接進行疊加或覆寫設計,避免不必要的重工。
    例:系統需要增加一種新點數,僅基於 Point 進行微調,class newPoint extends Point 即可繼續開發,毫無重工。
  6. 自動調用。
    例:在部分語言中(如 PHP)可以在當某些屬性被修改或函式被調用時自動執行某些行為,例如範例中的 execute() 被調用時可以自動執行 log() 進行日誌紀錄。

缺點:

  1. 初學者不易理解,需額外學習。

對於我個人而言,僅討論 OOP 的情況下「非 OOP 設計的優點」及「以 OOP 設計的缺點」幾乎是不存在的,不論是以 RAS 的可靠性、可用性、可維護性三種面向來看,或者是長遠發展的可擴充性、可讀性,幾乎都是 OOP 的單方面輾壓,毫無選擇以非 OOP 方式開發的理由。


最後編輯:2025-01-15 13:03:28