SOLID 原則 - Liskov Substitution Principle(里式替換原則)

less than 1 minute read

Derived classes(Subtypes) must be substitutable for their base classes(base types)

若程式內有使用到繼承,或是Interface的實作,則在系統中,凡base types(父類別或是interface)出現的地方,都可以被subtypes(子類別或是該interface的實作)來取代,而不會破壞程式原有的行為。

先舉個簡單的例子:

假設有一個類別A,裡面有的fire的方法,另外有個類別B繼承了類別A

若有個doSomething的方法會用到類別A,按照LSP的原則,類別A可以被替換成類別B而不出錯。

<?php
class A {
    public function fire(){}
}

class B extends A{
    public function fire(){}
}

function doSomething(A obj)
{
    
}

乍看之下這個原則是乎很容易理解且達成,但是我們在開發時往往會出現以下狀況:

現在假設一個VideoPlayer類別,包含了play的方法。play方法預計會返回一個結果。

現在有另一個AviVideoPlayer類別,繼承了VideoPlayer,也實作了play的方法,並加入了檔名的判斷,若非avi的檔名就會拋出錯誤例外。

<?php
class VideoPlayer{
    public function play($file){
        
    }
}

class AviVideoPlayer{
    public function play($file){
    	if(pathinfo($file, PATHINFO_EXTENSION) !== 'avi')
    	{
            throw new Exception(); //violate the LSP
    	}
    }
}

但在這樣的情況下就違反了LSP原則:因為AviVideoPlayerplay方法內,有可能會拋出例外,和VideoPlayer的預期結果不符。所以在替換時,可能會造成系統會有錯誤的情況發生。

上面想個都是以類別當做例子,接下來我們來看看interface的例子:

假設一個interface取名為LessonRepositoryInterface 裡面定義了getAll()的方法。因為是課程的Repository,所以我們可能會用file或是Db(database)的方式去取得課程資訊。

<?php

interface LessonRepositoryInterface {
    public function getAll();
}

class FileLessonRepository implements LessonRepositoryInterface{
    public function getAll()
    {
        return [];
    }
}

class DbLessonRepository implements LessonRepositoryInterface{
    public function getAll()
    {
        return Lesson::all();
    }
}

function foo(LessonRepositoryInterface $lesson)
{
    $lessons = $lesson->getAll();
    
    if(is_array($lessons)){
        //...
    }elseif($lessons instanceof Collection)){
        $lessons->toArray();
    }
}

這也很明顯的違反了LSP 。在Laravel中,Lesson::all()會返回的是Collection,而非陣列,所以對兩者來說,他們的輸出output並不相同,可能會造成系統的異常。

而對於foo這個方法來說,他也違反了OCP(Open-Closed Principle)原則,在方法內來判斷輸入類別。

所以我們可以知道當破壞了LSP的同時,可能也代表你的程式碼在設計的過程中也違反了OCP

透過OCP(Open-Closed Principle)的方式,我們可以定義interface的方法,可是僅能限制輸入(input),但是我們卻無法去限制輸出,在PHP7以前由於受限於PHP本身限制,我們無法定義輸出的Postconditions,所以我們僅能在Interface內利用PHPDOC的方式註明回傳的型別。來讓工程師知道該方法在實作時應該要符合相對應的輸出類別。

interface LessonRepositoryInterface {
	/**
	* Fetch all records
	*
	* @return array
	*/
    public function getAll();
}

class DbLessonRepository implements LessonRepositoryInterface{
    public function getAll()
    {
        return Lesson::all()->toArray();
    }
}

function foo(LessonRepositoryInterface $lesson)
{
    $lessons = $lesson->getAll();
}

總結來說,在不違反LSP的原則下設計類別/Interface ,需遵守下列四點:

  1. Signature must match.(子類別的方法必須和父類別一致)
  2. Preconditions can’t be greater.
  3. Postconditions at least equal to.
  4. Execption types must match.

Categories:

Updated: