如果你曾經寫過一串「先宣告資料、再呼叫函式、最後祈禱不要改壞其他地方」的 PHP,歡迎來到物件導向的世界。這裡會把資料和相關行為放在一起,讓程式不必像咖啡店的桌面一樣,什麼東西都堆在同一區。

這篇會用簡單的 PHP 範例帶你理解類別(Class)、物件(Object)、屬性、方法、$thisnew,也會說明封裝和物件之間的組合。讀完後,你應該能看懂基本的 OOP 程式,並開始自己設計小型類別。

為什麼要學 PHP 物件導向

物件導向程式設計(Object-Oriented Programming,OOP)不是把每個變數都改名成物件,也不是看到 class 就要立刻召開架構會議。它是一種組織程式的方式:把相關的資料和操作資料的程式碼放在同一個類別裡。

這樣做有幾個實際好處:

  • 更容易組織:一個類別可以集中描述某種事物的資料與行為。
  • 減少重複:同一個類別可以建立多個物件,不必複製一整套程式碼。
  • 比較容易維護:需求改變時,通常可以先從相關類別下手,不必在整個專案裡玩尋寶。
  • 方便協作:大型專案可以依照責任拆分不同類別,讓程式碼邊界更清楚。
  • 支援封裝:可以限制外部直接修改資料的方式,降低資料被亂改的機會。

OOP 不會自動讓所有程式變漂亮;類別設計得不好,仍然可能長成一團義大利麵,只是現在義大利麵住進了 class 裡。

類別與物件是什麼

先用一個生活化的比喻:類別像設計圖,物件像依照設計圖做出來的成品

例如,Cake 類別可以描述蛋糕有哪些資料(口味、尺寸),以及可以做什麼事(烘烤)。依照這個類別建立出來的巧克力蛋糕和草莓蛋糕,就是兩個不同的物件。

換句話說:

  • 類別(Class):定義物件應該有哪些屬性和方法。
  • 物件(Object):類別建立出來的實例,擁有自己的資料。
  • 實例化(Instantiation):使用 new,依照類別建立物件的過程。

類別本身比較像規格,物件才是程式執行時真正拿來工作的東西。設計圖不能自己吃蛋糕,這點 PHP 也沒有打算替它開例外。

用 PHP 建立第一個類別

類別包含屬性與方法

以下建立一個簡單的 Cake 類別:

<?php

class Cake
{
	public $flavor;
	public $size;

	public function bake()
	{
		echo '正在烘烤 ' . $this->flavor . ' 口味的蛋糕。';
	}
}

這段程式有兩種成員:

  • flavorsize屬性(Property),用來保存物件的資料。
  • bake()方法(Method),用來描述物件可以執行的行為。

方法裡的 $this 代表「目前正在呼叫這個方法的物件」。因此,$this->flavor 讀取的是目前這個蛋糕自己的口味,而不是某個神秘的全域蛋糕。

使用 new 建立物件

有了類別之後,可以用 new 建立物件,再設定它的屬性:

$chocolateCake = new Cake();
$chocolateCake->flavor = '巧克力';
$chocolateCake->size = '大';

$chocolateCake->bake();

$chocolateCakeCake 的一個物件。箭頭運算子 -> 用來存取物件的屬性或方法,所以:

  • $chocolateCake->flavor 代表這個物件的 flavor 屬性。
  • $chocolateCake->bake() 代表呼叫這個物件的 bake() 方法。

同一個類別也能建立多個彼此獨立的物件:

$chocolateCake = new Cake();
$chocolateCake->flavor = '巧克力';

$strawberryCake = new Cake();
$strawberryCake->flavor = '草莓';

$chocolateCake->bake();
$strawberryCake->bake();

兩個物件都來自 Cake 類別,但各自保存不同的 flavor。類別像食譜,物件則是不同口味的成品;PHP 沒有規定所有蛋糕都得是巧克力,算是對甜點界的基本尊重。

用類別管理書籍

接著把概念放進一個比較完整的例子。假設我們要描述一本書,可以把書名和作者放進屬性,再提供一個方法顯示書籍資訊:

class Book
{
	public $title;
	public $author;

	public function describe()
	{
		return $this->title . ',作者:' . $this->author;
	}
}

建立物件並使用它:

$book = new Book();
$book->title = '哈比人';
$book->author = 'J. R. R. 托爾金';

echo $book->describe();

這裡的 Book 是類別,$book 是物件。describe() 不需要另外接收 $book,因為它透過 $this 就能取得目前物件的資料。

把多個物件放在一起

物件也可以成為另一個物件的資料。像書架可以保存多本 Book

class Bookshelf
{
	public $books = [];

	public function addBook(Book $book)
	{
		$this->books[] = $book;
	}

	public function listBooks()
	{
		foreach ($this->books as $book) {
			echo $book->describe() . PHP_EOL;
		}
	}
}

使用 Bookshelf

$firstBook = new Book();
$firstBook->title = '哈比人';
$firstBook->author = 'J. R. R. 托爾金';

$secondBook = new Book();
$secondBook->title = '傲慢與偏見';
$secondBook->author = '珍・奧斯汀';

$bookshelf = new Bookshelf();
$bookshelf->addBook($firstBook);
$bookshelf->addBook($secondBook);
$bookshelf->listBooks();

Bookshelfbooks 屬性是一個陣列,裡面放著多個 Book 物件。這種「一個物件管理其他物件」的做法,就是物件組合(composition)的簡單例子。

封裝與可見性

前面的屬性使用 public,代表類別外部也能直接讀寫。這對示範很方便,但在實際程式裡,未必希望任何地方都能隨意修改資料。

PHP 提供三種常見的可見性:

  • public:類別內外都可以存取。
  • protected:類別本身和繼承它的子類別可以存取。
  • private:只有宣告該成員的類別本身可以存取。

例如,把書名設為私有,並透過方法控制修改方式:

class Book
{
	private $title;

	public function setTitle($title)
	{
		$this->title = $title;
	}

	public function getTitle()
	{
		return $this->title;
	}
}

$book = new Book();
$book->setTitle('哈比人');

echo $book->getTitle();

這就是封裝(Encapsulation):把資料和操作資料的方法放在一起,並限制外部直接碰觸內部狀態的方式。日後如果需要檢查書名是否為空白,可以在 setTitle() 裡加入規則,而不必搜尋整個專案的每一個賦值動作。

這篇的重點整理

到這裡,你已經掌握 PHP OOP 的基本詞彙:

  • 類別是建立物件的定義,描述屬性與方法。
  • 物件是類別的實例,保存自己的資料。
  • 屬性代表物件的狀態,例如書名或作者。
  • 方法代表物件的行為,例如列出書籍資訊。
  • new 用來依照類別建立物件。
  • $this 在物件方法中代表目前的物件。
  • 封裝透過可見性與方法,控制資料如何被使用。
  • 物件組合可以讓一個物件管理其他物件,例如書架管理書籍。

當你開始看到程式裡有許多互相關聯的資料和函式時,可以問自己:「這些東西是不是其實屬於同一種事物?」如果答案是,那就可能是設計類別的好時機。先把責任分清楚,再來喝咖啡;不然最後連咖啡都要被迫繼承 Everything 類別。

參考資料