物件導向程式設計由三個最重要的概念構成:封裝、繼承與多型。
所謂封裝,就是把程式包成 class,隱藏內部實現細節(private的property、method),提供外部訪問方式。
想像一下,你在開發一個公司內部的員工管理系統,裡面有一個 Employee 類別,負責管理員工的姓名、薪水、入職日期等等資料。這些資料是有規則的,例如薪水不能是負數、入職日期不能晚於今天,對吧?
如果每個開發者都可以直接任意的修改這些資料,在多人協作的情況下就很容易出錯:有人可能在不夠清楚相關業務邏輯的情況下誤改了薪水,有人不小心把入職日期改成過去的日期,這樣系統就亂掉了。
即便你是一人專案,隨著開發時間拉長,系統越長越大跟複雜,也會對於久遠的程式碼或業務邏輯越來越模糊。而使用封裝更能避免自己寫錯邏輯、方便未來維護與擴充。
所以我們就用封裝(Encapsulation)的概念,把資料藏起來(用 private 或 protected),然後提供一組「合法的操作方法」,也就是 public 方法給外部程式使用。
其中專門用來讀取或修改屬性的 public 方法,可以稱為 getter / setter。
Getter:用來讀取 private 或 protected 屬性的值,例如 getSalary() 取得員工薪水。
Setter:用來修改 private 或 protected 屬性的值,同時可以在裡面加上規則檢查,例如薪水不能是負數。
這樣做有幾個好處:
保護資料:外部程式想改資料,必須照我們規定的、外部可見的方法操作,這樣被亂改資料的機會就小多了。
集中規則:所有關於這個類別的商業邏輯都集中在這個 class 裡,像薪水不能是負數、入職日期要合法等規則,只寫在這裡就好。
維護方便:未來要改規則,只要改這個 class 裡的方法就行,不用跑遍整個專案找散落的邏輯,省時又安全。
簡單來說,封裝就像給 class 加了一個安全的介面:外面的人可以跟它互動,但想改資料,必須遵守規則。這樣程式更安全,也更容易維護,特別是當專案有多人合作的時候。
<?php
// 用 protected 讓子類別可以存取
protected $name;
protected $hireDate;
public function __construct($name, $hireDate) {
$this->name = $name;
$this->hireDate = $hireDate;
}
// 取得員工姓名 (Getter)
public function getName() {
return $this->name;
}
// 設定員工姓名(Setter)
public function setName($name) {
$this->name = $name;
}
// 取得員工到職日期(Getter)
public function getHireDate() {
return $this->hireDate;
}
// 設定員工到職日期(Setter)
public function setHireDate($date) {
// 簡單驗證:入職日期不能晚於今天
if (strtotime($date) <= time()) {
$this->hireDate = $date;
} else {
echo "入職日期不可晚於今天!\n";
}
}
// 計算薪資,父類別不實作(留給子類別多型實作)
public function calculatePay() {
return 0;
}
// 計算獎金,(假設獎金是薪資的 10%)
public function calculateAnnualBonus() {
return $this->calculatePay() * 0.1;
}
上述範例展示了以下幾個概念:
在物件導向程式設計中,子類別(Subclass)可以繼承父類別(Superclass)的特徵,包括屬性(properties)和方法(methods)。
這樣一來,子類別可以重複使用父類別已定義的程式碼,而不需要重新撰寫相同邏輯;同時,子類別也可以加入自己獨有的特徵,或覆寫(override)父類別的方法以改變行為。
我們可以說,子類別是一種「特殊化」(specialization)的父類別,也就是說在保有父類別共通特性之上,加入自己的擴充功能。
以我們這個員工管理系統的例子來說,一間公司可能有很多種類型的員工:有正職員工(薪水以月薪計算)、有計時人員(薪水以時薪乘上工時計算)...等等。隨著員工種類增加,如果把所有不同類型員工的薪水計算邏輯都寫在 Employee 這個父類別中,方法會變得又長又複雜,難以維護。
這時就可以使用繼承,將不同種類的員工分成不同的子類別,同時把所有員工共通的屬性與方法(例如姓名、入職日期等)寫在父類別 Employee 中,這樣既能重複利用程式碼,又能讓每個子類別保有自己的特有邏輯,例如薪水計算方式。
簡而言之,繼承就是讓子類別重複利用父類別已有的程式碼,同時增加自己獨有的特徵。
多型(Polymorphism)的概念是:不同的子類別可以對同一個方法名稱提供不同的實作方式,也就是「呼叫同一個方法,結果依子類別而不同」。
以我們的員工管理系統為例,不同類型的員工薪資計算方式不同:時薪員工要用時薪 × 工時計算,正職員工則用固定月薪計算。這時就可以利用多型的概念,在各個員工的子類別中分別實作 calculatePay() 方法,依需求計算薪資。呼叫 calculatePay() 時,不論是時薪員工還是正職員工,都可以使用同一個方法名稱,但計算結果會依員工類型不同而不同。
//父類別
class Employee {
protected $name;
protected $hireDate;
public function __construct($name, $hireDate){
$this->name = $name;
$this->hireDate = $hireDate;
}
// 多型方法:不同子類別會有不同計算薪水方式
public function calculatePay() {
// 父類別不直接計算
return 0;
}
public function getName() {
return $this->name;
}
public function getHireDate() {
return $this->hireDate;
}
}
// 子類別:時薪員工
class HourlyEmployee extends Employee {
protected $hourlyRate;
protected $hoursWorked;
public function __construct($name, $hireDate, $hourlyRate, $hoursWorked) {
parent::__construct($name, $hireDate);
$this->hourlyRate = $hourlyRate;
$this->hoursWorked = $hoursWorked;
}
public function calculatePay() {
return $this->hourlyRate * $this->hoursWorked;
}
}
// 子類別:固定薪資員工
class SalariedEmployee extends Employee {
protected $monthlySalary;
public function __construct($name, $hireDate, $monthlySalary) {
parent::__construct($name, $hireDate);
$this->monthlySalary = $monthlySalary;
}
public function calculatePay() {
return $this->monthlySalary;
}
}