Interface Segregation Principle
A client should not be forced to implement an interface that it doesn’t use.
對於所有待實作的類別,不該強迫實作它不需要的方法。
有沒有過一種經驗,在開發時為了方便架構類別間的相依關係,把相仿類別中的部分功能抽象成 interface
,並讓類別實作該 interface
內所描述的 method
,但是往往一不注意會將越來越多的 method
抽象至 interface
,可是這些多出來的動作對於其他類別並沒有實作意義,為了讓程式能順利通過所以又實作了一個空動作,長久累積下來,類別中多了一大堆沒意義的 method
。
而ISP 原則便是在解決這種狀況,降低 interface
的耦合度並且提高內聚力。
最近勞基法很火紅,舉個勞方及資方的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Worker { public function work() { }
public function sleep() { } }
class Captain { public function manage(Worker $worker) { $worker->work();
$worker->sleep(); } }
|
勞方實作了兩個動作,分別是工作以及休息,而資方則可以管理勞工
但是這樣有點單調,勞方除了有人類也有機器人吧,所以將勞方原本的 method
抽象出來至 interface
,再讓人類以及機器人去實作該有的 method
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 32
| interface WorkerInterface { public function work();
public function sleep(); }
class HumanWorker implements WorkerInterface { public function work() { return 'human working'; }
public function sleep() { return 'human sleeping'; } }
class AndroidWorker implements WorkerInterface { public function work() { return 'android working'; }
public function sleep() { return null; } }
|
問題來了,機器人不會睡覺,但是我需要實作 WorkerInterface
,就必須將 sleep()
也實作出來,所以只好讓他保持為空,這看起來一點也不合邏輯!
為了解決這點,我們可以將 WorkerInterface
的粒度降低,也就是拆成多個 interface
:
1 2 3 4 5 6 7 8 9
| class WorkableInterface { public function work(); }
class SleepableInterface { public function sleep(); }
|
再讓人類以及機器人實作該有的介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class HumanWorker implements WorkableInterface, SleepableInterface { public function work() { return 'human working'; }
public function sleep() { return 'human sleeping'; } }
class AndroidWorker implements WorkableInterface { public function work() { return 'android working'; } }
|
現在的類別看起來漂亮多了,讓類別們只實作必要的動作
再來回到我們的 Captain
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Captain { public function manage(WorkableInterface $worker) { $worker->work();
if ($worker instanceof AndroidWorker) { return ; }
$worker->sleep(); } }
|
因為 Worker
的動作有差異了,因此 Captain
在管理的時候就需要判斷目前的 Worker
類型,還記得 OCP 原則嗎,無論相依類別或者功能怎麼修改,都不需要修改舊有類別的程式碼
接著我們使用 OCP
的兩大原則來修改 Captain
:
Separate extensible behavior behind an interface
把所有需擴展的功能行為透過 interface
來定義
1 2 3 4 5 6 7 8 9 10 11 12
| interface ManageableInterface { public function beManaged(); }
class Captain { public function manage(ManageableInterface $worker) { $worker->beManaged(); } }
|
Flip the dependencies
反轉相依關係
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 32
| class HumanWorker implements WorkableInterface, SleepableInterface, ManageableInterface { public function work() { return 'human working'; }
public function sleep() { return 'human sleeping'; }
public function beManaged() { $this->work();
$this->sleep(); } }
class AndroidWorker implements WorkableInterface, ManageableInterface { public function work() { return 'human working'; }
public function beManaged() { $this->work(); } }
|
現在程式碼看起來更優雅,透過 interface
隔離類別所屬的動作,除了可以增加程式的閱讀性,也能增加類別的擴充性,以上便是ISP 原則的介紹。