iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 9
3
Software Development

看到 code 寫成這樣我也是醉了,不如試試重構?系列 第 9

SOLID 之 里氏替換原則(Liskov substitution principle)

一樣要考古一下原文:

Subtypes must be substitutable for their base types.

子類別必須要能取代它的父類別。

這次的考古講得非常簡單,它背後所代表的意義是:父類別出現的地方,子類別就能代替它,而且要能做到替換而不出現任何錯誤或異常

文字描述依然抽象,我們繼續看昨天的例子:

abstract class DataResource
{
    public function getData()
    {
        // 下載資料
        // ...
        $content = curl_exec($ch);
        
        $data = $this->parse($content);

        return $data;
    }
    
    abstract protected function parse($content); 
}

class XmlResource extends DataResource
{
    protected function parse($content)
    {
        // 載入 XML
        $data = simplexml_load_string($content);
        
        // 解析 XML
        // ...
        
        return $data;
    }
}

這裡面,父類別是 DataResource ,子類別是 XmlResource 。現在有個 Model 物件需要把資料拿出來儲存,我們可以這樣寫:

class Model
{
    public $resource;
    public $storage;
    
    public function __construct(XmlResource $resource)
    {
        $this->resource = $resource;
    }
    
    public function save()
    {
        $data = $this->resource->getData();
        $this->storage->store($data);
    }
}

$model = new Model(new XmlResource());

但問題來了,昨天我們還有寫另一個 class 它也能取得資料呀!

class JsonResource extends DataResource
{
    protected function parse($content)
    {
        // 解析 JSON
        $data = json_decode($content);
        
        // ...
        
        return $data;
    }
}

可是 Model 傳入 JsonResource 是不能跑的!因為 Model 只認 XmlResource ,不認 JsonResource

$model = new Model(new JsonResource());

解決方法其實很簡單,我們只要把 Model 的定義改成兩個子類別所繼承的 DataResource 父類別即可。

class Model
{
    public $resource;
    public $storage;
    
    public function __construct(DataResource $resource)
    {
        $this->resource = $resource;
    }
    
    public function save()
    {
        $data = $this->resource->getData();
        $this->storage->store($data);
    }
}

$model = new Model(new XmlResource());

這程式能跑的原因正是一開始所提到的:「父類別出現的地方,子類別就能代替它」,但有做到「要能做到替換而不出現任何錯誤或異常」嗎。

因為 save() 使用的 getData()DataResource 所定義的公開方法,因為繼承會把父類別的所有行為繼承到子類別,因此子類別也會有 getData() 而不會讓 save() 出錯,因此有做到「不出現任何錯誤或異常」。

原本程式的做法,是 Model 只能依賴 XmlResource ,這並不符合「里氏替換原則」;改依賴 DataResource 後,程式就符合原則了,接著就會發現程式的擴展性變好, Model 的 Resource 就可以有多樣化選擇,除了 XmlResourceJsonResource 之外,甚至還可以新加 CsvResource 讓 Model 也能讀取 CSV 檔。

遵守原則的要領

為避免發生錯誤或異常,實作可以參考要領,如下:

  • 子類別必須完全實作父類別的方法
  • 子類別可以有屬於自己的屬性和方法
  • 覆寫或實作父類別的方法時,參數要與父類別定義的一樣,或是更寬鬆。比方說:父類別的定義是 List ,子類別則可以是 ListCollection
  • 覆寫或實作父類別的方法時,回傳結果要跟父類別定義的一樣,或是縮小。比方說:父類別是回傳 List ,子類別則可以回傳 ListArrayList

此處以 Java 為例, ArrayList 繼承 ListList 繼承 Collection

優點

里氏替換原則的重點是要增加程式的強健性,讓版本升級的時候也能有很好的兼容性。比方說:子類別增加或修改,並不影響其他子類別,這正是強健性的特質。

上例的使用情況是:子類別處理不同的業務邏輯,參數定義使用父類別,實際上傳遞的是子類別,這樣就能同份定義,執行不同的業務邏輯。

參考資料


上一篇
SOLID 之 開關原則(Open-Close principle)
下一篇
SOLID 之 介面隔離原則(Interface segregation principle)
系列文
看到 code 寫成這樣我也是醉了,不如試試重構?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言