SOLID 原則 - Liskov Substitution Principle

Liskov Substitution Principle

Derived classes must be substitutable for their base classes.

對於父類別或者interface 出現的地方,都可以透過子類別或者該interface 的實作來取代,而不能破壞原有的行為

舉個例子來說,我們假設有一個取得所有課程的類別,而取得課程的方式可以透過Database 或者FileSystem 取得,因此我們可以將類別規劃如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
interface LessonRepositoryInterface
{
public function getAll();
}

class FileLessonRepository implements LessonRepositoryInterface
{
/**
* Return through filesystem
*
* @return array
*/
public function getAll()
{
// return through filesystem
return [];
}
}

class DBLessonRepository implements LessonRepositoryInterface
{
/**
* Return through database
*
* @return Collection
*/
public function getAll()
{
return Lesson::all();
}
}

我們可以透過以下的方法取得我們想要的課程:

1
2
3
4
5
6
7
8
9
10
function foo(LessonRepositoryInterface $lesson)
{
$lessons = $lesson->getAll();

if (is_array($lessons)) {
// do something with it.
} else if ($lesson instanceof Collection) {
// do something with it.
}
}

但這顯然是一個很糟糕的實踐方法,因為已經違反了OCP 原則,還不曉得OCP 原則的話可以參考筆者的前一篇文章

當破壞LSP 原則的同時反而會讓你的程式碼在實踐的過程中同時破壞OCP 原則,因為 FileLessonRepository 以及 DBLessonRepository,這兩個class 所回傳的 DataType 不相同,進而導致取得資料時必須依據取得的類型而有不同的處理方式。

因此我們先將所實踐的類別改成以下寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class FileLessonRepository implements LessonRepositoryInterface
{
/**
* Return through filesystem
*
* @return array
*/
public function getAll()
{
// return through filesystem
return [];
}
}

class DBLessonRepository implements LessonRepositoryInterface
{
/**
* Return through database
*
* @return array
*/
public function getAll()
{
return Lesson::all()->toArray();
}
}

將方法回傳的值全部限定為 array,在實踐上就不需要預期有不同的回傳結果

1
2
3
4
5
6
function foo(LessonRepositoryInterface $lesson)
{
$lessons = $lesson->getAll();

dump($lessons);
}

總結一下,在不違反LSP 原則的前提下,所設計的 class 需遵守以下四點原則:

  1. Signature must match.
  2. Preconditions can’t be greater.
  3. Postconditions at least equal to.
  4. Exception types must match.

譯:

  1. 子類別必須完全實現父類別的方法
  2. 每個方法使用前,必須檢驗傳入參數的正確性,例如 intdouble
  3. 執行的結果必須符合 contracts,例如回傳的參數型態
  4. 拋出的例外狀況必須相同