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

圖片

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

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

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

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

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

本篇介紹5大設(shè)計(jì)原則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提出,這些原則主要解決了下面兩個(gè)問(wèn)題:

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

介紹第一個(gè)原則:?jiǎn)我宦氊?zé)

單一職責(zé)原則

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

Robert C. Martin描述職責(zé)是:“a reason for change”。我們寫(xiě)出來(lái)的類(lèi),如果會(huì)因?yàn)槎嘤谝环N原因而去修改,那就不符合單一職責(zé)。另一角度看單一職責(zé)是從類(lèi)的對(duì)外表現(xiàn)去看,如果類(lèi)表現(xiàn)出不止一種行為,則也違反了SRP。

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

上面的類(lèi)User違反了SRP,因?yàn)橛袃蓚€(gè)職責(zé):

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

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

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

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

知道什么是單一職責(zé)后,我們?cè)偕钊胂氯?,面?duì)已經(jīng)存在,或者新建一個(gè)類(lèi)的時(shí)候,我們?cè)趺茨軌蚍治龀鏊穆氊?zé)?

Breaking up Classes(拆分類(lèi))

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

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

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

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

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個(gè)職責(zé)拆分為4個(gè)類(lèi),然后由InvoicingService連接起來(lái)。我們看到類(lèi)InvoiceGeneratorInvoiceDeliveryService其實(shí)可以再進(jìn)一步拆分,因?yàn)閱螕?jù)會(huì)有不同的表現(xiàn)形式,寄送方式也有多種方式,此時(shí)類(lèi)InvoiceGeneratorInvoiceDeliveryService不僅負(fù)責(zé)生產(chǎn)和寄送,還負(fù)責(zé)多種策略的選擇。

那介紹了這么多SRP,最重要的問(wèn)題是:

為什么SRP那么重要?

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

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

開(kāi)閉原則

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

這樣做的好處是我們不會(huì)破會(huì)原來(lái)的功能,我們只是在上面疊加,而不是修改。

一個(gè)開(kāi)閉原則的非常好的例子就是上一篇介紹的策略模式,我們永遠(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() );

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

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

為什么LSP那么重要呢?

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

接口隔離原則

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

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

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


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

通過(guò)將讀和寫(xiě)隔離開(kāi)來(lái),我們得到了更大的靈活性。

為什么ISP重要?

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

依賴反轉(zhuǎn)原則

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

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

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
    }
}

類(lèi)GameManager依賴于KeyboardInput和ScreenOutput,帶來(lái)的問(wèn)題是:如果我們想要改變下輸入或者輸出,我們只能去修改GameManager類(lèi)。如果我們應(yīng)用之前的LSP原則,抽象出輸入和輸出接口,我們就有下面的實(shí)現(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
    }
}

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

看下面的圖:

圖片

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

為什么DIP重要?

DIP給我們的一個(gè)重要原則是:盡可能的依賴穩(wěn)定的東西,因?yàn)檫@樣子將來(lái)出錯(cuò)的概率會(huì)小。

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是相對(duì)于low level modules是穩(wěn)定的,因此不應(yīng)該依賴于不穩(wěn)定的low level modules。抽象相對(duì)于具體實(shí)現(xiàn)也是更穩(wěn)定的,因此應(yīng)該是具體實(shí)現(xiàn)依賴于抽象。

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容