The Clean Architecture in PHP 讀書筆記(三)

圖片

上篇最重要的是介紹了去耦的工具之一設(shè)計模式,本篇將繼續(xù)介紹去耦工具:設(shè)計原則。

本文為系列文章的第三篇,第一、二篇地址是

The Clean Architecture in PHP 讀書筆記(一)

The Clean Architecture in PHP 讀書筆記(二)

The Clean Architecture in PHP 讀書筆記(三)

本篇介紹5大設(shè)計原則SOLID

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

這5大原則最初是由Robert C. Martin提出,這些原則主要解決了下面兩個問題:

  • 類應(yīng)該怎么設(shè)計?彼此之間如何交互
  • 我們?nèi)绾谓M織這些類

介紹第一個原則:單一職責(zé)

單一職責(zé)原則

單一職責(zé),要想理解這個原則,我們先來看下什么是職責(zé)?

Robert C. Martin描述職責(zé)是:“a reason for change”。我們寫出來的類,如果會因為多于一種原因而去修改,那就不符合單一職責(zé)。另一角度看單一職責(zé)是從類的對外表現(xiàn)去看,如果類表現(xiàn)出不止一種行為,則也違反了SRP。

class User {
    public function getName(){}
    public function getEmail(){}
    public function find( $id ){}
    public function save(){}
}

上面的類User違反了SRP,因為有兩個職責(zé):

  • 管理user的狀態(tài)
  • 管理user的存取

因此我們需要將其重構(gòu)為下面兩個類:

class User {
    public function getName() {}
    public function getEmail() {}
}
class UserRepository {
    public function find($id) {}
    public function save(User $user) {}
}

此時User負(fù)責(zé)狀態(tài),UserRepository負(fù)責(zé)存取,可能會引起UserRepository的改變的只有存儲user地方的改變。

知道什么是單一職責(zé)后,我們再深入下去,面對已經(jīng)存在,或者新建一個類的時候,我們怎么能夠分析出它的職責(zé)?

Breaking up Classes(拆分類)

class InvoicingService {
    public function generateAndSendInvoices() {}
    protected function generateInvoice($customer) {}
    protected function createInvoiceFile($invoice){}
    protected function sendInvoice($invoice) {}
}

看方法名generateAndSendInvoices,直觀上來至少有2個職責(zé),生產(chǎn)并且發(fā)送單據(jù),分析后,InvoicingService至少有下面4個職責(zé):

  • 負(fù)責(zé)哪個單據(jù)需要創(chuàng)建
  • 在數(shù)據(jù)庫中產(chǎn)生單據(jù)記錄
  • 產(chǎn)生單據(jù)的物理表示(PDF,Excel,CSV等)
  • 通過某些手段發(fā)送單據(jù)給用戶

分析出職責(zé)后,我們下一步就是將InvoicingService類拆分為小的,符合SRP的類

class OrderRepository {
    public function getOrdersByMonth($month);
}
class InvoicingService {
    public function generateAndSendInvoices() {}
}
class InvoiceFactory {
    public function createInvoice(Order $order) {}
}
class InvoiceGenerator {
    public function createInvoiceFormat(
        Invoice $invoice,
        $format ) {} 
}
class InvoiceDeliveryService { 
    public function sendInvoice(
    Invoice $invoice,
    $method ) {} 
}

根據(jù)4個職責(zé)拆分為4個類,然后由InvoicingService連接起來。我們看到類InvoiceGeneratorInvoiceDeliveryService其實可以再進(jìn)一步拆分,因為單據(jù)會有不同的表現(xiàn)形式,寄送方式也有多種方式,此時類InvoiceGeneratorInvoiceDeliveryService不僅負(fù)責(zé)生產(chǎn)和寄送,還負(fù)責(zé)多種策略的選擇。

那介紹了這么多SRP,最重要的問題是:

為什么SRP那么重要?

關(guān)鍵點還是SRP有助于我們實現(xiàn)解耦,去耦是貫穿全文的主題。

總結(jié)起來:類越小,越容易測試,越容易重構(gòu),也更不容易出現(xiàn)缺陷。

開閉原則

對擴(kuò)展開發(fā),對修改封閉

這樣做的好處是我們不會破會原來的功能,我們只是在上面疊加,而不是修改。

一個開閉原則的非常好的例子就是上一篇介紹的策略模式,我們永遠(yuǎn)只需要新增策略,而不用去修改現(xiàn)有的策略。

里氏替換原則

先看代碼的:

interface HelloInterface {
    
    public function getHello();
}

class EnglishHello implements HelloInterface {

    public function getHello()
    {
        return "Hello";
    }
}

class SpanishHello implements HelloInterface {

    public function getHello()
    {
        return "Hola";
    }
}

class FrenchHello implements HelloInterface {

    public function getHello()
    {
        return "Bonjour";
    }
}
class Greeter {

    public function sayHello( HelloInterface $hello )
    {
        echo $hello->getHello() . "!\n";
    }
}

$greeter = new Greeter();
$greeter->sayHello( new EnglishHello() );
$greeter->sayHello( new SpanishHello() );
$greeter->sayHello( new FrenchHello() );

我們看上面的類:在Greeter()中我們使用了HelloInterface,我們可以使用該接口的不同實現(xiàn),輸出的內(nèi)容會改變,但是不會改變sayHello這個行為本身。

總結(jié)起來就是:如果一個類使用了一個接口的一個實現(xiàn)類,那么該接口的任何其他實現(xiàn)類也可以被這里直接使用,不用做出任何修改。

為什么LSP那么重要呢?

LSP使得我們重構(gòu)代碼變的有理可循。任何依賴于的接口的使用方,都不用關(guān)心具體實現(xiàn)是什么,因為所有的具體實現(xiàn)都會使得程序行為是正確的。

接口隔離原則

接口隔離和單一職責(zé)相關(guān),如果一個類只有一個職責(zé),自然其接口就是隔離的,這么說可能還不是特別明白,我們看代碼:

interface LoggerInterface {
    public function write( $message );
    public function read( $messageCount );
}
class FileLogger implements LoggerInterface {
    ....
}
class EmailLogger implements LoggerInterface{
    ....
}

上面我們定義了一個日志接口,并且有兩個實現(xiàn),一個基于文件,一個基于Email,但是Email的實現(xiàn)中,對于read接口,我們難道要去判斷是正常郵件還是日志嘛?我們實現(xiàn)EmailLogger,只是想要將關(guān)鍵日志以郵件發(fā)送出來,對于read其實是沒有需求的,因此我們做如下的重構(gòu):


interface LogWriterInterface { 
  public function write($message);
}
interface LogReaderInterface {
    public function read($messageCount);
}
interface LogManagerInterface extends LogReaderInterface, LogWriterInterface {
}

通過將讀和寫隔離開來,我們得到了更大的靈活性。

為什么ISP重要?

ISP的目標(biāo)同樣是解耦。所有使用接口的使用者都意味著和這個接口耦合,不管你是用還是不用里面的方法,這帶來的風(fēng)險是,如果接口的實現(xiàn)中某個方法有錯誤,使用者就得承擔(dān)風(fēng)險。

依賴反轉(zhuǎn)原則

該原則是后面介紹的The Clean Architecture的核心,因此我們來仔細(xì)討論下:

我們設(shè)想下有下面的場景:假設(shè)一個class控制了一個簡單的游戲。游戲負(fù)責(zé)接收用戶的輸入并且將結(jié)果展示在屏幕上,具體類如下:

class GameManager {

    protected $input;
    protected $video;

    public function __construct()
    {
        $this->input = new KeyboardInput();
        $this->video = new ScreenOutput();
    }

    public function run()
    {
        // accept user input from $this->input // draw the game state on $this->video
    }
}

GameManager依賴于KeyboardInput和ScreenOutput,帶來的問題是:如果我們想要改變下輸入或者輸出,我們只能去修改GameManager類。如果我們應(yīng)用之前的LSP原則,抽象出輸入和輸出接口,我們就有下面的實現(xiàn):

class GameManager {

    protected $input;
    protected $video;

    public function __construct( InputInterface $input, OutputInterface $output
    )
    {
        $this->input = $input;
        $this->video = $output;
    }

    public function run()
    {
        // accept user input from $this->input // draw the game state on $this->video
    }
}

此時我們實現(xiàn)了依賴的反轉(zhuǎn),怎么回事呢?

看下面的圖:

圖片

上面的圖中:原先GameManager依賴于下面的實現(xiàn),而在右邊:KeyboardInput和ScreenOutput依賴GameManager聲明的接口,實現(xiàn)了依賴的大逆轉(zhuǎn)

為什么DIP重要?

DIP給我們的一個重要原則是:盡可能的依賴穩(wěn)定的東西,因為這樣子將來出錯的概率會小。

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

High level modules是相對于low level modules是穩(wěn)定的,因此不應(yīng)該依賴于不穩(wěn)定的low level modules。抽象相對于具體實現(xiàn)也是更穩(wěn)定的,因此應(yīng)該是具體實現(xiàn)依賴于抽象。

SOLID原則使得我們的代碼更易擴(kuò)展、重構(gòu)和測試,下面會繼續(xù)解耦的第三個工具:依賴反轉(zhuǎn)。

最后推薦下介紹SOLID的非常好的書:Laravel - 從百草園到三味書屋 "From Apprentice To Artisan"目錄

這是The Clean Architecture in PHP的第三篇,你的鼓勵是我繼續(xù)寫下去的動力,期待我們共同進(jìn)步。
?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容