### 強類型與鴨子類型
在之前的章節里,我們討論了依賴注入的基礎知識:什么是依賴注入;如何實現依賴注入;依賴注入有什么好處。之前的例子中也模擬了將接口注入到類里面的過程。在我們繼續學習后續內容之前,有必要深入討論一下接口,而這正是很多 PHP 開發者所不熟悉的。
在我成為 PHP 程序員之前,我是寫 .NET 的。你覺得我是喜歡原生代碼還是什么?在 .NET 里到處都是接口,而且很多接口都定義在 .NET 框架核心中了,對此有充分理由:很多 .NET 語言比如 C# 和 VB.NET 都是強類型的。在強類型語言中,當你給一個函數傳參時,必須指定變量類型。例如,在 C# 中我們會這么做:
```php
public int BillUser(User user)
{
this.biller.bill(user.GetId(), this.amount)
}
```
注意,在這里,我們不僅要定義傳進去的參數是什么類型的,還要定義這個方法的返回值是什么類型的。C# 鼓勵類型安全。除了指定的 `User` 對象之外,它不允許我們傳遞其他類型的對象到 `BillUser` 方法中。
然而 PHP 是一種鴨子類型語言。所謂鴨子類型語言,說的是一個對象的可用方法取決于其使用方式,而非這個對象繼承自誰,或者實現了什么接口。我們先來看個例子:
```php
public function billUser($user)
{
$this->biller->bill($user->getId(), $this->amount);
}
```
在 PHP 中,我們不必顯式告訴一個方法需要什么類型的參數。實際上,我們可以傳遞任何類型的對象到 `billUser` 方法,只要這個對象提供了 `getId` 方法。這里有個關于鴨子類型的解釋:如果一個東西看起來像鴨子,叫起來也像鴨子,那它就是鴨子。換言之,在本例中,如果一個對象看上去像 `User`,方法響應也像 `User`,那它就是個 `User` 對象。
> 學院君注:套用《JavaScript權威指南》對鴨子類型的解釋,在 PHP 中,如果一個對象可以像鴨子一樣走路、游泳并且嘎嘎叫,就認為這個對象是鴨子對象,哪怕它不是從鴨子類繼承而來。換句話說,PHP 是弱類型語言,對象類型在運行時動態判斷。
不過,PHP 到底有沒有任何強類型功能呢?當然有!PHP 混合了強類型和鴨子類型(弱類型)結構。為了說明這點,我們來重寫一下 `billUser` 方法:
```php
public function billUser(User $user)
{
$this->biller->bill($user->getId(), $amount);
}
```
給方法簽名加上了 `User` 類型約束后,我們現在可以確保所有傳入`billUser` 方法的對象,要么是 `User` 類的實例,要么是一個繼承自 `User` 類的對象實例。
強類型和弱類型各有優劣。在強類型語言中,編譯器通常能提供編譯時錯誤檢查的功能,這個功能在提高代碼質量方面非常有用,可以避免開發人員將危險代碼交付到線上,此外,方法的輸入和輸出也更加明確。
與此同時,強類型的特性也使得程序僵化。舉個例子,在 Eloquent ORM 中,類似 `whereEmailOrName` 這樣的動態方法就不可能在 C# 之類的強類型語言里實現。我們這里不討論強類型和弱類型哪種編程范式更好,而是要記住它們各自的優劣之處。在 PHP 里面,不管使用強類型還是弱類型,都沒問題,沒犯什么錯誤。錯誤的是不假思索,不區分具體適用場景和問題,為了使用某種類型而使用。
### 一個契約示例
接口如同契約。接口并不包含任何代碼實現,只是定義了一個實現該接口的對象必須實現的一系列方法。如果一個對象實現了一個接口,那么我們就能保證這個接口所定義的一系列方法都能在這個對象上調用。由于有接口契約保證特定方法的實現,通過多態也能使類型安全的語言變得更靈活。
> 關于多態:多態含義很廣,從本質上說,是一個實體擁有多種形式。在本書中,我們講多態說的是一個接口有多鐘實現方式。例如,`UserRepositoryInterface` 可以有 MySQL 和 Redis 兩種實現,并且每一種實現都是 `UserRepositoryInterface` 的一個實例。
為了說明接口在強類型語言中的靈活性,我們們來寫一個簡單的酒店客房預訂代碼。考慮以下接口:
```php
interface ProviderInterface
{
public function getLowestPrice($location);
public function book($location);
}
```
當用戶預訂房間時,我們需要將此事記錄在系統里。所以在 `User` 類里添加如下方法:
```php
class User
{
public function bookLocation(ProviderInterface $provider, $location)
{
$amountCharged = $provider->book($location);
$this->logBookedLocation($location, $amountCharged);
}
}
```
由于我們對 `$provider` 做了類型約束,在 `User` 類的 `bookLocation` 方法中,就可以放心大膽的認為 `$provider` 實例上的 `book` 方法是可以調用的。這給我們復用 `bookLocation` 方法帶來了靈活性,完全不必關心用戶傾向哪家酒店提供商。最后,我們編寫一些代碼來體驗下這種靈活性:
```php
$location = '希爾頓, 達拉斯';
$cheapestProvider = $this->findCheapest($location, array(
new PricelineProvider,
new OrbitzProvider,
));
$user->bookLocation($cheapestProvider, $location);
```
太棒了!不管哪家酒店是最便宜的,我們都能夠將它傳入 `User` 對象來預訂房間了。由于 `User` 對象只需要有一個遵從 `ProviderInterface` 契約的對象實例就可以了,所以未來如果有新的酒店供應商,我們的代碼也可以很好的工作。
> 忘掉細節:記住,接口實際上并不做任何事情。它只是簡單的定義了實現類必須擁有的一系列方法。
### 接口&團隊開發
當你的團隊在構建大型應用時,不同的功能模塊往往有著不同的開發進度。例如,一個開發人員在開發數據層,另一個開發人員在做前端和控制器層。前端開發者想要測試他的控制器,但是后端開發進度比較慢,無法聯調。如果這兩個開發者能以接口或契約的方式達成協議,然后后端開發的所有類都遵循這種協議,就像下面這段代碼:
```php
interface OrderRepositoryInterface
{
public function getMostRecent(User $user);
}
```
一旦建立了契約,就算契約還沒有真正實現,前端開發者也可以測試他的控制器了!這樣一來,應用中的不同組件就可以按不同的速度開發,同時仍然允許編寫適當的單元測試。此外,這種方式還可以使組件內部的改動不會影響到其它不相關的組件。要始終牢記「無知是福」。我們不想讓類知道依賴是如何工作的,只需要知道它們能做什么。所以,先定義好契約,再來寫控制器:
```php
class OrderController {
public function __construct(OrderRepositoryInterface $orders)
{
$this->orders = $orders;
}
public function getRecent()
{
$recent = $this->orders->getMostRecent(Auth::user());
return View::make('orders.recent', compact('recent'));
}
}
```
前端開發者甚至可以為這接口寫個「假」實現,然后這個應用的視圖就可以用假數據渲染了:
```php
class DummyOrderRepository implements OrderRepositoryInterface
{
public function getMostRecent(User $user)
{
return array('Order 1', 'Order 2', 'Order 3');
}
}
```
編寫好假實現之后,就可以在服務容器里將其綁定到契約上,然后在整個應用中都可以調用它了:
```php
$this->app->bind(OrderRepositoryInterface::class, function ($app) {
return new DummyOrderRepository();
});
```
接下來,如果后臺開發者寫完了真正的實現代碼,如`RedisOrderRepository`。服務容器中的綁定可以輕松切換到新的實現,整個應用將會使用開始從 Redis 讀取出來的訂單數據。
> 接口即綱領:接口有助于開發應用所提供的、已定義好的功能「框架」。 在組件的設計階段,團隊里使用接口進行討論是很方便的,例如,定義一個 `BillingNotifierInterface` 接口,然后討論它提供哪些方法。在編寫任何實現代碼前,最好先通過接口討論達成一致,這是構建一套好 API 的必要前提!