### 簡介
別擔心,里氏替換原則名字起的高大上,但是其實很簡單。該原則可以描述為:一個抽象的任意實現都可以在聲明該抽象的地方替換它。讀起來有點繞口,通俗點說就是:如果一個類使用了某個接口的實現,那么一定可以通過該接口的其它實現來替換它,不用做出任何修改。
> 里氏替換原則規定對象可以被其子類的實例所替換,并且不會影響到程序的正確性。
### 實戰
為了說明該原則,我們繼續使用前面編寫的 `OrderProcessor` 類作為示例。請看下面的方法:
```php
public function process(Order $order)
{
// Validate order...
$this->orders->logOrder($order);
}
```
注意,當 `Order` 通過驗證后,我們就會通過 `OrderRepositoryInterface` 的實現類實例將其記錄下來。假設訂單處理業務剛起步時,我們將所有訂單都存儲到了 CSV 格式的文件系統中。對應的,我們的 `OrderRepositoryInterface` 的實現類就應該是`CsvOrderRepository`。現在,隨著訂單增多,我們想用一個關系數據庫來存儲訂單。下面我們就來看看新的訂單資料庫類該怎么編寫吧:
```php
class DatabaseOrderRepository implements OrderRepositoryInterface
{
protected $connection;
public function connect($username, $password)
{
$this->connection = new DatabaseConnection($username, $password);
}
public function logOrder(Order $order)
{
$this->connection->run('insert into orders values (?, ?)', array(
$order->id, $order->amount
));
}
}
```
現在,我們來研究如何使用這個實現類:
```php
public function process(Order $order)
{
// Validate order...
if($this->repository instanceof DatabaseOrderRepository)
{
$this->repository->connect('root', 'password');
}
$this->repository->logOrder($order);
}
```
注意在這段代碼中,我們不得不在調用的地方檢查 `OrderRepositoryInterface` 接口是否是通過數據庫實現的。如果是的話,則必須連接到數據庫。在很小的應用中,這可能看起來沒什么問題,但如果`OrderRepositoryInterface` 在很多類中被調用呢?我們可能就要把這段「啟動」代碼在每一個調用的地方重復實現。這讓人非常頭疼,不僅難以維護,而且非常容易出錯誤,并且一旦我們忘了將所有調用的地方進行同步修改,那程序恐怕就會出問題。
很明顯,上面的例子違背了里氏替換原則。因為我們不能在不修改調用方代碼的情況下注入接口的實現。所以,既然已經定位到問題所在,接下來就要修復它。下面就是新的 `DatabaseOrderRepository` 實現:
```php
class DatabaseOrderRepository implements OrderRepositoryInterface
{
protected $connector;
public function __construct(DatabaseConnector $connector)
{
$this->connector = $connector;
}
public function connect()
{
return $this->connector->bootConnection();
}
public function logOrder(Order $order)
{
$connection = $this->connect();
$connection->run('insert into orders values (?, ?)', array(
$order->id, $order->amount
));
}
}
```
現在 `DatabaseOrderRepository` 自己接管了數據庫連接,這樣我們就可以把數據庫「啟動」代碼從 `OrderProcessor` 中移除了:
```php
public function process(Order $order)
{
// Validate order...
$this->repository->logOrder($order);
}
```
這樣一改,我們就可以在 `CsvOrderRepository` 和 `DatabaseOrderRepository` 實現之間進行切換了,不用對 `OrderProcessor` 做任何修改。我們的代碼終于實現了里氏替換原則!需要注意的是,我們討論過的許多架構概念都和「知識」相關。具體來說,一個類所具備的「周邊」知識,例如外圍代碼和依賴,會幫助這個類完成它的工作。當你想要構建一個健壯的大型應用時,限制類的知識會是一個反復出現、非常重要的主題。
還要注意如果不遵守里氏替換原則,那么可能會影響到我們之前已經討論過的其他原則。不遵守里氏替換原則,那么開放封閉原則一定也會被打破。因為,如果調用者必須檢查實例屬于哪個子類,則一旦有了新的子類,調用者就得做出改變。
> 你可能已經注意到這個原則和前面提到的「泄露抽象實現細節」密切相關。數據庫倉庫類的實現細節泄露就是里氏替換原則被破壞的第一跡象。所以要時刻留意那些泄露!