本文為系列文章的第二篇,第一篇地址是
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大類別:
- Creational
- Structural
- Behavioral
本書不會去按個介紹23個模式,這樣去介紹的書籍太多了,而且本書也沒有那么多篇幅去按個講,但是這些都是再進行展開的基礎,因此我們會介紹一些最近本的概念:
- Factory:負責生產對象,類似于語言本省提供的new關鍵字,在C++中與new的不同就在于,new不能通過string直接實例化對象,但是factory可以
- Repository:不是GoF中提出的設計模式,其類似于一個倉庫,負責數據的存儲和獲取
- Adapter:適配器,故名思議就是將接口實現轉換
- 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
帶來的好處是:
- Reusable code.創建的地方都可以調用這段代碼,降低了代碼的復雜度。
- Testable code.由于關注點分離了,方便測試,可以單獨進行創建邏輯的測試
- 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
- The Factory Method pattern:定義了創建對象的方法,但是具體創建哪個對象,由子類決定
- 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);
}
}
更多的設計模式:
請期待下一篇SOLID Design Principles