SOLID 原則簡單易懂版,讓你的 PHP 代碼更堅固

Gray and Brown Concrete Brick Wall

寫程式很簡單,但要寫出易讀好維護又好擴充的好程式,就需要一點努力,好在有 SOLID 原則可以參考,遵守這些原則,很自然的,程式自己會形成堅固的模組。

在眾多程式開發原則當中,SOLID原則無疑是最受歡迎和最廣泛應用的一組。這五個原則像是一個指南針,幫助你在複雜的開發過程中保持正確的方向。不僅如此,它們還能讓你的代碼更容易被其他開發者理解,這對於團隊合作來說是非常寶貴的。所以,無論你是新手還是資深開發者,掌握和應用SOLID原則都會為你帶來長遠的好處。

Single Responsibility Principle (SRP)

單一職責原則(Single Responsibility Principle, SRP)其實是一個很直觀的概念。它基本上就是說:「一個類(或函數、模塊等)應該只做一件事情,而且做得好。」

為什麼 SRP 是重要的?

  1. 易於理解:當一個類只做一件事,理解它就變得容易多了。
  2. 易於修改:如果需求變了,你只需要修改一個地方。
  3. 減少錯誤:做少量的事情通常可以減少錯誤。

不好的設計範例

// 不好的例子:這個類別有多個職責
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 是重要的?

  1. 減少錯誤:不需要改動現有的代碼,就減少了破壞它的風險。
  2. 易於擴展:想要添加新功能?只需擴展,不需修改。
  3. 提高可維護性:因為新功能是獨立添加的,所以更容易理解和維護。

不好的設計範例

假設我們有一個計算各種形狀面積的函數。

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 是重要的?

  1. 易用性:你可以很自然地用子類別來替換父類別,不用擔心會出錯。
  2. 可維護性:遵循這個原則會讓你的代碼更容易維護和擴展。
  3. 減少錯誤:如果每個子類別都能順利替換父類別,那麼出錯的機會就會減少。

不好的設計範例

假設我們有一個「鳥」類別,它有一個「飛」的方法。然後我們有一個「企鵝」類別,繼承自「鳥」類別。

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 是重要的?

  1. 易懂:小而簡單的接口更容易理解。
  2. 靈活:你可以更容易地組合不同的接口來滿足需求。
  3. 好維護:當需求變更時,你只需要調整相關的小接口,而不是一個大而全的接口。

不好的設計範例

假設我們有一個 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();
}

這樣,一個程式員類別就可以只實現 CoderTester 接口,而不用管 CookCleaner

介面隔離原則(ISP)就是讓你的接口保持小而美,這樣用到它的類別就會更開心,因為它們不用去實現一堆不相干的方法。這樣一來,你的代碼就會更容易理解和維護。

Dependency Inversion Principle (DIP)

依賴反轉原則(Dependency Inversion Principle, DIP)是一個讓人覺得有點高大上的名字,但其實它的核心觀念很簡單:高層(比如業務邏輯)不應該依賴於低層(比如數據存儲或具體的實現),兩者都應該依賴於抽象(比如接口或抽象類)。

為什麼 DIP 是重要的?

  1. 靈活性:當你想更換一個組件時,不需要大動干戈地改其他代碼。
  2. 可測試性:更容易寫單元測試,因為你可以用模擬對象(mocks)來代替真實的依賴。
  3. 解耦合:各部分之間的依賴減少,代碼更容易理解和維護。

不好的設計範例

假設我們有一個 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 就是告訴我們:「嘿,不要讓你的代碼太過依賴具體的東西,讓它們依賴於一個抽象,這樣未來要變來變去就容易多了!」


更多參考教學

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *