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

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

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

你的解耦工具

在下面的文章中,主要會圍繞去耦給出5大工具,讓你輕松去耦。

  • Design Patterns,A Primer
  • SOLID Design Principles
  • Depedency Injection
  • Defining a Contract with Interfaces
  • Abstracting with Adapters

先介紹第一個工具:設計模式。

Design Patterns,A Primer

設計模式是對軟件中通用問題的總結,有了設計模式,方便我們進行交流,譬如一說MVC,我們就知道是怎么回事了,不然我們必須巴拉巴拉一大堆話去描述,不易于傳播、交流,一個好的設計模式必然有一個簡單易懂的名字,因為只有簡單的東西,才能被大家廣泛傳播。

如今,一談到設計模式,被大家廣泛傳播的就是1994年四人幫提出的23種模式,分為了3大類別:

  1. Creational
  2. Structural
  3. Behavioral

本書不會去按個介紹23個模式,這樣去介紹的書籍太多了,而且本書也沒有那么多篇幅去按個講,但是這些都是再進行展開的基礎,因此我們會介紹一些最近本的概念:

  1. Factory:負責生產對象,類似于語言本省提供的new關鍵字,在C++中與new的不同就在于,new不能通過string直接實例化對象,但是factory可以
  2. Repository:不是GoF中提出的設計模式,其類似于一個倉庫,負責數據的存儲和獲取
  3. Adapter:適配器,故名思議就是將接口實現轉換
  4. Strategy:目的是靈活性,將一個或一組行為封裝起來,方便的進行替換

對上面的概念具體展開

The Factory Patterns

簡單代碼:

$customer = new Customer();

如果customer足夠簡單,上面的創建沒有問題,但是如果customer創建的時候,必須進行一堆初始化,那我們就瘋了,一個簡單的想法,當然是將這些創建邏輯抽離出來,變為一個函數,進行復用,因此就有下面的版本,

復雜代碼:


class CustomerFactory {
    protected $accountManagerRepo;

    public function __construct(AccountManagerRepository $repo) { 
        $this->accountManagerRepo = $repo;
    }

    public function createCustomer($name) { 
        $customer = new Customer(); 
        $customer->setName($name); 
        $customer->setCreditLimit(0); 
        $customer->setStatus('pending'); 
        $customer->setAccountManager(
          $this->accountManagerRepo->getRandom()
        );
        return $customer; 
    }
}

總結下出現上面代碼的原因:軟件復雜度的提升,可以這么說,軟件中各種問題的出現,都是因為軟件的復雜度,不同的復雜度有不同的應對方法,總的目標都是為了降低復雜度

那上面出現的CustomerFactory帶來的好處是:

  1. Reusable code.創建的地方都可以調用這段代碼,降低了代碼的復雜度。
  2. Testable code.由于關注點分離了,方便測試,可以單獨進行創建邏輯的測試
  3. Easy to change.創建邏輯只在一處,方便修改
Static Factories
class CustomerFactory {
    public static function createCustomer($name) {
        $customer = new Customer(); 
        $customer->setName($name); 
        $customer->setCreditLimit(0); 
        $customer->setStatus('pending');
        return $customer; 
    }
}
$customer = CustomerFactory::createCustomer('ACME Corp');

靜態工廠,通過一個static方法訪問,那怎么評判是靜態工廠好,還是上面一個工廠好呢?答案是:

It depends

沒有最好的,只有更合適的,如果靜態方法能滿足需求,那就是用靜態工廠,如果你是數據源會經常變化,那就使用實例化工廠,并且將需要的依賴注入進來。

Types of Factories
  1. The Factory Method pattern:定義了創建對象的方法,但是具體創建哪個對象,由子類決定
  2. The Abstract Factory pattern:抽象工廠模式的粒度更粗一點,不僅管一個對象的創建,而是管著一組相關對象的創建

talk is cheap,show me the code

讓我們上代碼的。

class Document {

    public function createPage() {

        return new Page();
    }

}

如果我們又好幾種page,怎么辦?

class Document {

    public function createPage($type) {
      switch $type {
        case "ResumeDocument":
          return new ResumePage();
        case "PortfolioDocument":
          return new PortfolioPage();
      }
    }

}

上面代碼的問題是:我們每次新曾一個類型的page,必須要修改createPage方法,不滿足開放封閉原則(OCP),那PHP有個好處是,直接傳遞給new字符串就能創建對象,看代碼:

class Document {

    public function createPage($type) {
      return new $type;
    }
}

問題:我們無法驗證傳入$type的有效性,而且對createPage返回的類型,我們也無法檢查,那怎么辦呢?

思路是:將創建邏輯按照關注點分離的邏輯,每個類型的document創建自己的page,這就解決了$type的有效性問題,那對于返回值,我們定義一個公共接口,只有實現這個接口的才是符合預期的。

abstract class AbstractDocument {
    abstract public function createPage():PageInterface;
}
class ResumeDocument extends AbstractDocument {         
  public function createPage():PageInterface {
        return new ResumePage(); 
    }
}
class PortfolioDocument extends AbstractDocument {      
  public function createPage():PageInterface {
        return new PortfolioPage(); 
    }
}
interface PageInterface {}
class ResumePage implements PageInterface {} 
class PortfolioPage implements PageInterface {}

介紹第一個概念The Abstract Factory pattern

抽象工廠就是對于工廠方法的1+1=2的疊加。

如果我們的Document不止創建一個page,還要創建cover,那就會有兩個create方法,此時每個子類就多實現一個方法的。

Repository Pattern

倉儲模式:該模式在Eric Evan的神書:Domain-Driven Design: Tackling Complexity in the Heart of Software中詳細的進行了描述

A REPOSITORY represents all objects of a certain type as a conceptual set (usuallyemulated). It acts like a collection, except with more elaborate querying capability.

Domain-Driven Design, Eric Evans, p. 151

倉儲類似于一個數據集合,相比較集合有更多精細設計的query,當我們談論Repository的時候,我們關注的不再是“querying the database”,而是更純粹的目的:存取數據

常用的讀取數據的方法


class MyRepository {
    public function getById($id); 
    public function findById($id); 
    public function find($id); 
    public function retrieve($id);
}

常用的寫方法:

class MyRepository {
    public function persist($object); 
    public function save($object);
}

每個Repository對應一個類的存取,有多個類,就會有多個Repository。

一個Repository怎么工作?

Objects of the appropriate type are added and removed, and the machinery behindthe REPOSITORY inserts them or deletes them from the database.

Domain-Driven Design, Eric Evans, p. 151

主要給出一些資源:

Dotrine ORM:實現了Date Mapper

Eloquent ORM,Propel:實現了Active Record

Zend Framework 2:提供了Table Data Gateway模式

如果你想要自己實現一個Repository,強烈推薦Patterns of Enterprise Application Architecture

關于數據庫的各種模式,推薦一個ppt

Adapter Pattern

直接看代碼:

class GoogleMapsApi {
    public function getWalkingDirections($from, $to) {}
}
interface DistanceInterface {
    public function getDistance($from, $to);
}
class WalkingDistance implements DistanceInterface {    
  public function getDistance($from, $to) {
        $api = new GoogleMapsApi();
        $directions = $api->getWalkingDirections($from, $to);
        return $directions->getTotalDistance(); 
    }
}

適配器,非常明確的表名了意圖,通過WalkingDistance將GoogleMapsApi適配為了DistanceInterface

Strategy Pattern

也是看代碼:

public function invoiceCustomers(array $customers) { 
    foreach ($customers as $customer) {
        $invoice = $this->invoiceFactory->create(
          $customer,
          $this->orderRepository->getByCustomer($customer)
        );
        // send invoice...
    } 
}
// 此處我們根據用倉庫里獲取到的用戶賬單,通知用戶


interface InvoiceDeliveryInterface { 
  public function send(Invoice $invoice);
}

class EmailDeliveryStrategy implements InvoiceDeliveryInterface { 
    public function send(Invoice $invoice) {
    // Use an email library to send it
    } 
}
class PrintDeliveryStrategy implements InvoiceDeliveryInterface { 
    public function send(Invoice $invoice) {
    // Send it to the printer
    } 
}
// 賬單有兩種通知方式,一種是發郵件,一種是print

public function invoiceCustomers(array $customers) { 
    foreach ($customers as $customer) {
        $invoice = $this->invoiceFactory->create(
        $customer,
        $this->orderRepository->getByCustomer($customer));
    switch ($customer->getDeliveryMethod()) { 
    case 'email':
        $strategy = new EmailDeliveryStrategy();
        break; 
    case 'print': 
    default:
        $strategy = new PrintDeliveryStrategy();
        break; 
    }
    $strategy->send($invoice);
  }
}
// 通過策略模式:我們可以在runtime,根據不同的賬單,選擇而不同的發送策略
// 問題:發送策略的選擇不應該在外部直接暴露,改選擇什么通知策略,應該是用戶自己知道的,因此就有了下面的代碼:

class InvoiceDeliveryStrategyFactory {
    public function create(Customer $customer) {
        switch ($customer->getDeliveryMethod()) { 
        case 'email':
            return new EmailDeliveryStrategy();
            break; 
        case 'print': 
        default:
            return new PrintDeliveryStrategy();
            break; 
        }
    } 
}
// 此時的客戶端,根據最少職責原則,此時invoiceCustomers值負責創建賬單,并且發送,并不負責選擇什么方式寄送
public function invoiceCustomers(array $customers) {    
  foreach ($customers as $customer) {
    $invoice = $this->invoiceFactory->create(
      $customer,
      $this->orderRepository->getByCustomer($customer));
    $strategy = $this->deliveryMethodFactory->create(
      $customer);
    $strategy->send($invoice);
  }
}

更多的設計模式:

  1. 設計模式:可復用面向對象軟件的基礎
  2. Head First Design Patterns

請期待下一篇SOLID Design Principles

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,816評論 18 139
  • ?? 對設計模式的極簡說明!?? 這個話題可以輕易讓任何人糊涂。現在我嘗試通過用 最簡單 的方式說明它們,來讓你(和我...
    月球人simon閱讀 1,119評論 1 2
  • 文章作者:Tyan博客:noahsnail.com 3.4 Dependencies A typical ente...
    SnailTyan閱讀 4,189評論 2 7
  • 1 人和動物情同此理 這幾年,老媽在臨汾,兒子在長治,這兩個地方去得就多了點。去得多了,有時候,在公交車上打個盹,...
    聽風閣主人閱讀 341評論 3 2
  • “我想跟你說:你不要舍不得我,因為我也舍不得你” 阿風知道自己心里住著一個惡魔,每隔一段時間就會浮現出來。 秀走的...
    浪子峰閱讀 254評論 1 0