PHP OO 物件導向原則:單一職責原則SRP


OO_Principle_SRP

物件導向有五個原則 S.O.L.I.D. :
  • 單一職責原則 SRP (Single Responsibility Principle)
  • 開放封閉原則 OCP (Open Closed Principle)
  • 里氏替換原則 LSP (Liskov Substitution Principle)
  • 介面隔離原則 ISP (Interface Segragation Principle)
  • 依賴反轉原則 DIP (Dependency Inversion Principle)
物件導向的五個基本原則,當這些原則一起被應用時,會使得應用程式更易於維護與擴展。

更多物件導向的理論的學習內容會整理到 PHP OO 的進階教學:

 單一職責原則 SRP (Single Responsibility Principle)

一、定義與說明:

一個類別只能負責一個職責
註:若覺得職責太抽象,可把職責當作是功能。
如果一個類別做了兩件職責,就必須拆成兩個類別
當每個類別只處理一職責的時候,以後某個職責出問題時,只要修改負責那個職責的類別就好了!
如果把多件個職責寫在同一個類別,就好像把「資訊部門」與「行銷部門」放在同一個部門裡面,各自負責不同的職責。
這種情況下,你的類別就會變得龐大又複雜,你可能需要花更多的時間來測試這個類別。
這意味著,未來你需要花更多的 QA 工作時間與努力在這個類別上。

二、遵守單一職責原則的結果:

提高內聚、降低相依性
高內聚:
當一個類別的職責,越多越複雜時,內聚力越低。
當一個類別的職責,越清楚越單純,內聚力越高。(單一功能)
低耦合:
寫程式常常需要引入(include)別的套件或類別。如果一個類別需要引入的檔案越少,就是相依性較低; 反觀如果引入的檔案數多,就是相依性較高。
舉例:
一個類別有多個職責,不僅內聚力低,可能還造成更高相依性。
就好像把「資訊部門」與「行銷部門」放在同一個部門裡面,但「資訊部門」需要電腦設備;「行銷部門」需要海報、水彩等文宣工具。
那麼這個「複合式的部門」必須買齊至少兩樣工具(電腦設備及文宣工具)才能讓部門運作起來。
在這個複合式的部門裡,如果把資訊部與行銷部拆開成兩個獨立的部門,每個部門只負責各自的職責,就可以提高內聚;
每個部門只需要購買自己需要的設備,也就降低複合式的部門的相依性。

三、如何設計單一職責原則的類別?:

通常在設計一個類別的職權時,很難從需求分析的清單得到頭緒,這時候你可以從分析 UML 的 Actor 角色得到一些線索。角色是一個與系統或應用程式互動的對象,
它不一定代表一個真正的人,許多功能都是基於角色的需求慢慢修改而成的。
以下列出可能的角色:
  • 使用資料庫存取模組的角色:包含 DBA、資訊系統和軟體架構師。
  • 使用自動櫃員機(ATM)的角色:銀行主機、顧客。
  • 圖書管理系統的找書模組:使用者圖書管理員、借書的客戶、電腦。

範例一: 可以顯示”內容”的物件

假設我們有一個 Book 類別,類別內容有:書的基本資訊與方法。
class Book {
    function getTitle() {
        return "A Great Book";
    }

    function getAuthor() {
        return "John Doe";
    }

    function turnPage() {
         // pointer to next page
    }

    // printCurrentPage 顯示本頁內容
    function printCurrentPage() {
        echo "current page content";
    }
}
這看起來很像是單一職權的類別。這個 Book 類別提供 title 標題資訊、author 作者資訊和 turnPage 翻頁功能,最後這個類別還可以 printCurrentPage 將頁面內容顯示於畫面上。但是這個類別其實有一個問題,在設計類別的時候,你應該考慮到哪些角色可能會來調用 Book 物件,可能是書本管理系統,也可能是一個呈現資料的機制(如螢幕、瀏覽器、繪圖UI、文字UI 等)。
Book 類別把顯示邏輯寫在裡面,如果每增加一個顯示書本內容的方式,就必須修改一次 Book 類別的程式,最後 Book 類別會隨著顯示方式變多而越來越龐大。
而且將顯示邏輯與商業邏輯混在一起是這樣做已經違反了單一職權原則,讓我們稍微改變一下程式:
class Book {
    function getTitle() {
        return "A Great Book";

    }

    function getAuthor() {
        return "John Doe";
    }

    function turnPage() {
        // pointer to next page
    }

    function getCurrentPage() {
        return "current page content";
    }
}

interface Printer {
    function printPage($page);
}

class PlainTextPrinter implements Printer {
    function printPage($page) {
        echo $page;
    }
}

class HtmlPrinter implements Printer {
    function printPage($page) {
        echo '<div style="single-page">' . $page . '</div>';
    }
}

class PDFPrinter implements Printer {
    function printPage($page) {
        /** 省略 */
    }
}
調整後,將顯示邏輯抽出 Book 類別變成一個抽象層的介面(interface),之後如果增加新的顯示方式,
只需要實作 Printer 介面就好,不會再更動到 Book 類別的程式了!
即使這個例子非常基礎,但已經能充分瞭理解如何將顯示邏輯與商業邏輯拆開,並且展示了單一職責原則替程式帶來相當的進步與靈活性。

範例二: 可以儲存”書本”的物件

一個可以把書下載到自己電腦上的功能:
class Book {
    function getTitle() {
        return "A Great Book";
    }

    function getAuthor() {
        return "John Doe";
    }

    function turnPage() {
        // pointer to next page
    }

    function getCurrentPage() {
        return "current page content";
    }

    function save() {
        $filename = '/documents/'. $this->getTitle(). ' - ' . $this->getAuthor();
        file_put_contents($filename, serialize($this));
    }
}
我們可以再由 actors 角色來判斷這個類別目前的職責:書本管理系統、儲存檔案機制。
因此,我們應該把存擋的邏輯與書本的邏輯拆開成兩個類別,現在修改一下程式成下面的樣子:
class Book {
    function getTitle() {
        return "A Great Book";
    }

    function getAuthor() {
        return "John Doe";
    }

    function turnPage() {
        // pointer to next page
    }

    function getCurrentPage() {
        return "current page content";
    }
}

class SimpleFilePersistence {
    function save(Book $book) {
        $filename = '/documents/' . $book->getTitle() . ' - ' . $book->getAuthor();
        file_put_contents($filename, serialize($book));
    }
}
將存擋的邏輯抽離,並且移到新的類別裡,就可以清楚的分出職責。而且我們也可以在不影響 Book 類別的情況下,替換存擋的方式。
如實作一個 DatabasePersistence 資料庫儲存的類別。不管使用了哪些方法來儲存書本,都不會修改到 Book 類別。
在設計類別與模組的過程中,永遠都要考慮到單一職責原則,隨時去想這類別是不是太複雜,是否要切分成兩個類別,讓類別只負責一個職責。

結論:

一個 class/method 只做一件事。

遵守原則的結果:

特定需求改變的時候,只會有「一個」相關的 class/method 需要做修改。
此原則也可套用到: method、module、package
資料參考:

留言

這個網誌中的熱門文章

Git Commit Message 這樣寫會更好,替專案引入規範與範例

PHP OO 物件導向基礎教學

Gitlab 合併請求 Merge Request 是什麼?