Table of Contents
寫程式很簡單,但要寫出易讀好維護又好擴充的好程式,就需要一點努力,好在有 SOLID 原則可以參考,遵守這些原則,很自然的,程式自己會形成堅固的模組。
在眾多程式開發原則當中,SOLID原則無疑是最受歡迎和最廣泛應用的一組。這五個原則像是一個指南針,幫助你在複雜的開發過程中保持正確的方向。不僅如此,它們還能讓你的代碼更容易被其他開發者理解,這對於團隊合作來說是非常寶貴的。所以,無論你是新手還是資深開發者,掌握和應用SOLID原則都會為你帶來長遠的好處。
Single Responsibility Principle (SRP)
單一職責原則(Single Responsibility Principle, SRP)其實是一個很直觀的概念。它基本上就是說:「一個類(或函數、模塊等)應該只做一件事情,而且做得好。」
為什麼 SRP 是重要的?
- 易於理解:當一個類只做一件事,理解它就變得容易多了。
- 易於修改:如果需求變了,你只需要修改一個地方。
- 減少錯誤:做少量的事情通常可以減少錯誤。
不好的設計範例
// 不好的例子:這個類別有多個職責
class Employee {
public function calculateSalary() {
// 計算薪資
}
public function saveToDatabase() {
// 儲存員工資料到資料庫
}
public function displayProfile() {
// 顯示員工資料
}
}
假設有一個 Employee
類,它負責員工的薪資計算、報告生成、和數據儲存等。這個類做了太多事情,所以當你想改其中一個功能(比如薪資計算)時,很容易影響到其他功能(比如報告生成)。
好的設計
// 好的例子:每個類別都只有一個職責
class Employee {
// 只負責管理員工的基本資料
}
class SalaryCalculator {
public function calculate(Employee $employee) {
// 計算薪資
}
}
class EmployeeStorage {
public function save(Employee $employee) {
// 儲存員工資料到資料庫
}
}
class EmployeeProfile {
public function display(Employee $employee) {
// 顯示員工資料
}
}
將這些不同的功能拆分成不同的類。比如,一個 SalaryCalculator
類只負責薪資計算,一個 ReportGenerator
類只負責報告生成,等等。這樣,每個類都只做一件事,而且做得好。
單一職責原則(SRP)就是讓你的代碼更整潔、更易於維護的一種方式。它鼓勵你將不同的功能拆分成不同的部分,這樣每個部分都容易理解和修改。
Open Close Principle (OCP)
開放封閉原則(Open/Closed Principle, OCP)基本上就是說:你的代碼應該是「對擴展開放,對修改封閉」。這句話的意思是,你應該能夠添加新功能而無需修改現有的代碼。
為什麼 OCP 是重要的?
- 減少錯誤:不需要改動現有的代碼,就減少了破壞它的風險。
- 易於擴展:想要添加新功能?只需擴展,不需修改。
- 提高可維護性:因為新功能是獨立添加的,所以更容易理解和維護。
不好的設計範例
假設我們有一個計算各種形狀面積的函數。
function calculateArea($shape) {
if ($shape instanceof Rectangle) {
return $shape->width * $shape->height;
} elseif ($shape instanceof Circle) {
return pi() * pow($shape->radius, 2);
}
// 每次添加新形狀,都得改這裡
}
這個設計不好,因為每次添加新的形狀,都得改這個函數。
好的設計範例
我們可以讓每個形狀都有自己的 area()
方法。
interface Shape {
public function area();
}
class Rectangle implements Shape {
public function area() {
return $this->width * $this->height;
}
}
class Circle implements Shape {
public function area() {
return pi() * pow($this->radius, 2);
}
}
function calculateArea(Shape $shape) {
return $shape->area();
}
這樣一來,每次有新形狀時,我們只需要添加一個實現了 Shape
接口的新類,而不用去改 calculateArea
函數。
開放封閉原則(OCP)就是讓你的代碼更靈活和易於維護的一種方式。它讓你能輕鬆添加新功能,而不用去改現有的代碼,這樣就減少了出錯的機會。
Liskov Substitution Principle (LSP)
里氏替換原則(Liskov Substitution Principle, LSP)聽起來可能有點專業,但其實它的核心觀念很簡單:如果有一個地方可以用父類別(比如「鳥」)的物件,那麼它應該可以不做任何修改地用子類別(比如「麻雀」)的物件來替換。
為什麼 LSP 是重要的?
- 易用性:你可以很自然地用子類別來替換父類別,不用擔心會出錯。
- 可維護性:遵循這個原則會讓你的代碼更容易維護和擴展。
- 減少錯誤:如果每個子類別都能順利替換父類別,那麼出錯的機會就會減少。
不好的設計範例
假設我們有一個「鳥」類別,它有一個「飛」的方法。然後我們有一個「企鵝」類別,繼承自「鳥」類別。
class Bird {
public function fly() {
// 飛
}
}
class Penguin extends Bird {
public function fly() {
// 企鵝不能飛
throw new Exception("I can't fly!");
}
}
這個設計有問題,因為企鵝其實不能飛。所以,當你嘗試讓企鵝飛的時候,程式就會出錯。
好的設計範例
更好的做法是讓「鳥」類別和「企鵝」類別都繼承自一個更一般的類別,比如「動物」。然後,只有真正會飛的鳥類(比如「麻雀」)才繼承自「鳥」類別。
class Animal {
// 基本的動物行為
}
class Bird extends Animal {
public function fly() {
// 飛
}
}
class Sparrow extends Bird {
// 麻雀也會飛
}
class Penguin extends Animal {
// 企鵝不會飛,所以它直接繼承自「動物」
}
簡單來說,里氏替換原則(LSP)就是告訴我們:子類別應該能夠完全替換父類別,而不會讓使用它的代碼出錯。這樣一來,你的代碼就會更容易理解和維護。
Interface Segregation Principle (ISP)
介面隔離原則(Interface Segregation Principle, ISP)其實是一個很實用的觀念。它基本上就是告訴我們:「嘿,不要把太多的東西塞進一個接口裡。」簡單來說,就是讓每個接口都保持小而專注,這樣用到它的類別就不會被迫去實現一堆它用不到的方法。
為什麼 ISP 是重要的?
- 易懂:小而簡單的接口更容易理解。
- 靈活:你可以更容易地組合不同的接口來滿足需求。
- 好維護:當需求變更時,你只需要調整相關的小接口,而不是一個大而全的接口。
不好的設計範例
假設我們有一個 Worker
接口,它包括了寫代碼、測試、做飯和打掃等方法。
interface Worker {
public function code();
public function test();
public function cook();
public function clean();
}
這樣不太好,因為一個程式員可能只需要寫代碼和測試,而不需要做飯和打掃,對吧?
好的設計範例
更好的做法是把這個大接口拆成幾個小接口:
interface Coder {
public function code();
}
interface Tester {
public function test();
}
interface Cook {
public function cook();
}
interface Cleaner {
public function clean();
}
這樣,一個程式員類別就可以只實現 Coder
和 Tester
接口,而不用管 Cook
和 Cleaner
。
介面隔離原則(ISP)就是讓你的接口保持小而美,這樣用到它的類別就會更開心,因為它們不用去實現一堆不相干的方法。這樣一來,你的代碼就會更容易理解和維護。
Dependency Inversion Principle (DIP)
依賴反轉原則(Dependency Inversion Principle, DIP)是一個讓人覺得有點高大上的名字,但其實它的核心觀念很簡單:高層(比如業務邏輯)不應該依賴於低層(比如數據存儲或具體的實現),兩者都應該依賴於抽象(比如接口或抽象類)。
為什麼 DIP 是重要的?
- 靈活性:當你想更換一個組件時,不需要大動干戈地改其他代碼。
- 可測試性:更容易寫單元測試,因為你可以用模擬對象(mocks)來代替真實的依賴。
- 解耦合:各部分之間的依賴減少,代碼更容易理解和維護。
不好的設計範例
假設我們有一個 User
類別,它直接依賴於一個 MySQLDatabase
類別來儲存數據。
class MySQLDatabase {
public function save($data) {
// 儲存數據到 MySQL
}
}
class User {
private $database;
public function __construct() {
$this->database = new MySQLDatabase();
}
public function save() {
$this->database->save($this->data);
}
}
這個設計不好,因為 User
類別緊緊依賴於 MySQLDatabase
。如果我們想改用其他數據庫,就得改 User
類別的代碼。
好的設計範例
我們可以用一個接口來改善這個設計。
interface Database {
public function save($data);
}
class MySQLDatabase implements Database {
public function save($data) {
// 儲存數據到 MySQL
}
}
class User {
private $database;
public function __construct(Database $database) {
$this->database = $database;
}
public function save() {
$this->database->save($this->data);
}
}
在這個例子裡,User
類別不再直接依賴於 MySQLDatabase
,而是依賴於一個 Database
接口。這樣,我們就可以隨時換用其他實現了 Database
接口的數據庫,而不用改 User
的代碼。
好的設計(遵循 DIP)讓你的代碼更靈活、更容易測試和維護。不好的設計則會讓你的代碼變得僵硬和難以管理。總之,DIP 就是告訴我們:「嘿,不要讓你的代碼太過依賴具體的東西,讓它們依賴於一個抽象,這樣未來要變來變去就容易多了!」
更多參考教學