美思 [PHP] 程式設計教學:類別 (Class) 和物件 (Object)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

物件導向程式設計是當代的主流範式,大部分主流的程式語言皆支援此種範式。本文說明在 PHP 中建立類別和物件的方式,適用於 PHP 5 之後的版本。

使用內建類別建立物件

在開始撰寫類別前,本節以內建類別來展示如何使用物件。本範例使用了 Directory 物件:

<?php

# Create a Directory object.
$dir = dir("/home/user");

# Get a property of the object.
echo $dir->path, "\n";

# Iterate over the object.
while (($entry = $dir->read()))
    echo $entry, "\n";

# Close the object.
$dir->close();

建立 Directory 物件的方式是使用 dir 函式傳入代表路徑的字串,而非使用 new。這是少數的例外情境。

變數 $dirDirectory 物件。物件是電腦程式中的抽象物體。在物件中,資料 (data) 和行為 (behavior) 是連動的。使用 -> 可以取得物件的屬性 (property) 或方法 (method)。屬性為物件的資料。方法則是和物件連動的函式。

$dir->path$dir 物件的屬性。該屬性儲存 Directory 物件的路徑。

$dir->read()$dir 物件的方法。該方法回傳傳入路徑內的檔案或子目錄,回傳的資料型態是字串。注意呼叫 $dir->read() 時,會搭配 while 迴圈,逐一走訪該路徑下所有的檔案和子目錄。

由於 $dir 物件內部代表 Directory 物件所在的資源,最後要用 $dir->close() 方法將此資源關閉。

雖然這個例子很短,我們可以從此範例看到使用物件的方式。

建立第一個類別

藉由撰寫類別,程式設計者可視需求擴充新的資料型態。前一節使用 PHP 內建的類別來建立物件。本節要開始撰寫新的類別,以建立物件。

本範例撰寫及使用了 Point 類別,該類別用來表示平面座標上的點:

<?php

# Create a `Point` object with default values.
$p = new Point();
# Create a `Point` object.
$q = new Point(5.0, 12.0);
# Call a static function.
echo Point::distance($p, $q), "\n";

# A point class.
class Point
{
    # Private fields
    private float $x;
    private float $y;

    # The constructor of this class.
    public function __construct(
        float $x = 0.0,
        float $y = 0.0
    ) {
        $this->setX($x);
        $this->setY($y);
    }

    # The getter of `x`.
    public function x(): float {
        return $this->x;
    }

    # The setter of `x`.
    private function setX(float $x): void {
        $this->x = $x;
    }

    # The getter of `y`.
    public function y(): float {
        return $this->y;
    }

    # The setter of `y`.
    private function setY(float $y): void {
        $this->y = $y;
    }

    # A static method.
    public static function distance(
        self $p, self $q
    ): float {
        $dx = $p->x() - $q->x();
        $dy = $p->y() - $q->y();
        return sqrt($dx * $dx + $dy * $dy);
    }
}

如同函式,使用類別的程式碼可以寫在類別宣告之前。這樣的撰碼模式易於追蹤程式碼,故此處保留此慣例。

建立新物件會使用保留字 new。建立物件 $p 時,使用預設的參數,建立位於座標原點的點。建立物件 $q 時,則傳入該點所在的 xy 座標。

接著,使用 Point::distance() 靜態方法來計算 $p$q 的距離。靜態方法是和類別連動的方法,不屬於任何物件。

類別是建立物件的藍圖。撰寫物件導向程式時,會建立一至多個類別,再由這些類別來建立物件。建立類別會使用保留字 class。按照慣例,類別名稱使用 PascalCase 的形式來命名。

我們在前一節提過,類別包括屬性和方法。前者是物件的資料,後者是物件的行為。一般來說,屬性會保持私有 (private) 狀態,方法則視需求採用公開 (public) 或私有。適當地控制屬性和方法的狀態,對日後撰寫及重構程式碼相當重要。

建構子 (constructor) 是特殊的方法,在物件建立時會呼叫建構子。PHP 類別的建構函式的名稱固定為 __construct,而且每個類別只會有一個建構子。後文會繼續探討建構子相關的議題。

$this 是類別中特殊的變數。該變數用來表示此類別日後建立的物件。所以 $this 可用 -> 來呼叫連動的屬性和方法。

Point 類別在建立 Point 物件後就不能修改座標點所在的位置,故此處使用 private 來修飾 setX()setY() 方法。若希望座標點在建立後仍可修改位置,則將其改用 public 來修飾。

在類別外部,使用 private 修飾的屬性和方法是不可視的 (invisible)。所以,類別實作者可視需求撰寫任意個私有屬性和私有方法,只要控制好公開屬性及公開方法即可。

使用 static 修飾的方法是靜態方法。靜態方法會和類別而非物件連動,所以在靜態方法中無法使用 $this

self 是類別特有的型態宣告,代表該類別的資料型態。使用 self 會比直接撰寫 Point 要好,因為前者在類別名稱更動時會自動代入新的類別名稱。

解構子 (Destructor)

除了建構子以外,在 PHP 類別中也可宣告解構子。當物件沒被變數指向時,會自動呼叫解構子。以下 PHP 虛擬碼宣告建構子和解構子:

<?php

class Klass
{
    public function __construct() {
        # Implement this constructor here.
    }

    public function __destruct() {
        # Implement this destructor here.
    }

    # Implement other methods.
}

在 PHP 中,解構子不常使用。因為 PHP 會自動管理記憶體。其他的系統資源需要精準地控制釋放的時機。

使用靜態方法宣告多個建構子

PHP 類別只能撰寫一個建構子,但有時候會有撰寫多個建構子的需求。PHP 官方文件展示了一個使用靜態方法做為替代性建構子的範例如下:

<?php

# Some examples.
$p1 = Product::fromBasicData(5, 'Widget');
$p2 = Product::fromJson($someJsonString);
$p3 = Product::fromXml($someXmlString);

class Product
{
    # Private fields
    private ?int $id;
    private ?string $name;

    # Hide our traditional constructor.
    private function __construct(?int $id = null, ?string $name = null) {
        $this->id = $id;
        $this->name = $name;
    }

    public static function fromBasicData(int $id, string $name): static {
        $new = new static($id, $name);
        return $new;
    }

    public static function fromJson(string $json): static {
        $data = json_decode($json);
        return new static($data['id'], $data['name']);
    }

    public static function fromXml(string $xml): static {
        # Implement custom logic here.
        $data = convert_xml_to_array($xml);
        $new = new static();
        $new->id = $data['id'];
        $new->name = $data['name'];
        return $new;
    }
}

靜態方法和類別連動,不能呼叫物件方法。PHP 提供 static 保留字,在靜態方法內用來建立物件。

由於這個範例出現在 PHP 官方文件中,代表這個模式是 PHP 對於宣告多個建構子的官方解答。不過,本文提供另一個替代性的模式。詳見下一節的內容。

(選擇性) 不使用靜態方法宣告多個建構子

PHP 提供 func_num_argsfunc_get_args 兩個函式來取得函式參數的數量和元素。也就是說,其實 PHP 函式可以設計成同名的多重參數函式,只要實作上有支援即可。

本小節展示一個稍長的範例虛擬碼。這個範例用上了本小節所提到的概念:

<?php

# Some examples.
$c1 = RGBColor("orange");
$c2 = RGBColor("FFA500");
$c3 = RGBColor(255, 165, 0);
$c4 = RGBColor(1, 0.647, 0);

class RGBColor
{
    private int $red;
    private int $green;
    private int $blue;

    public function __construct()
    {
        $argc = func_num_args();
        $argv = func_get_args();

        if (1 == $argc) {
            if ("string" == gettype($argv[0])) {
                if (ctype_xdigit($argv[0])) {
                    createFromHexCode($argv[0]);
                }
                else {
                    createFromName($argv[0]);
                }
            }
            else {
                die("Invalid argument");
            }
        }
        elseif (3 == $argc) {
            if (isIntArray($argv)) {
                createFromIntArray($argv);
            }
            else if (isFloatArray($argv)) {
                createFromFloatArray($argv);
            }
            else {
                die("Invalid arguments");
            }
        }
        else {
            die("Invalid argument(s)");
        }
    }

    private function createFromHexCode(string $code) {
        # Implement the constructor here.
    }

    private function createFromName(string $name) {
        # Implement the constrctor here.
    }

    private function createFromIntArray($arr) {
        # Implement the constructor here.
    }

    private function createFromFloatArray($arr) {
        # Implement the constructor here.
    }

    private function isIntArray($arr): bool {
        return count($arr) 
            == count(
                array_filter($arr,
                    fn($n) => is_int($n)));
    }

    private function isFloatArray($arr): bool {
        return count($arr)
            == count(
                array_filter($arr,
                    fn($n) => is_float($n)));
    }
}

本範例的類別 RGBColor 表面上看來有四種建立物件的建構子,但實際上共用同一個建構子。在建構子中根據參數形式呼叫不同的私有方法。藉由此模式來模擬多個建構子。

這個模式的缺點在於 PHP 無法協助程式設計者檢查建構子的參數,要由程式設計者在程式中自行檢查。這模式非官方文件提供,請自行考慮是否要用在自己的程式碼中。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。