SOLID 原則 - Interface Segregation Principle

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 原則的介紹。