封閉開放原則

You should be able to extend a classes behavior, without modifying it.
Robert C. Martin

封閉開放原則,全名 Open-closed Principle,簡稱 OCP。

定義

對擴充開放,對修改封閉。

第一次看到的話是不是很匪夷所思,要怎麼新增功能又不修改程式碼呢?
其實是新增功能(擴充),但是不修改舊有的功能(對修改封閉),也就是只要把程式寫成在之後要新增功能時,不需要改動原本有的功能就好了。

怎麼做呢?

實踐這個原則的手法有不少,還是要依照實際的情境選擇適合的方法。
以下是可以參考的作法:

  • 利用繼承
  • 利用抽象介面
  • 依賴倒轉 (Dependency Injection pattern, DIP)
  • 裝飾者模式
  • 策略模式

秘訣

如果還是不知道麼開始的話,試著用抽象層級的方式去思考類別,不要把功能寫死,試著寫成抽象的概念,然後把會造成改變的選項較給別的類別處理,主要類別還是專心處裡核心任務。最後,通常變化都是後來才出現的,一開始設計的時候,不一定會構思到要這樣寫,之後再透過重構完成即可。

範例

看個範例吧!
下面這一段程式碼,假設我們有一台 Nintendo 主機,然後我們手邊有三款遊戲可以玩,要玩的時候呼叫 play function 就可以了。
但是,如果我每買一款遊戲就要新增遊戲在這個 Nintendo 類別裡面…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Nintendo
{
public function play(string $game)
{
switch ($game) {
case 'mario':
$mario = new Mario();
$mario->start();
case 'car-racing':
$carRacing = new CarRacing();
$carRacing->start();
case 'pokemon':
$pokemon = new Pokemon();
$pokemon->start();
}
}
}

試著改改看…
把 game 抽出來寫成介面,定義一些所有遊戲都會需要的基本功能,之後每一款遊戲再各自去實作細節。

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
33
34
35
36
interface Game 
{
public function start();
}

class Mario implements Game
{
protected $mario;
protected $wario;
protected $luigi;
...
public function __construct()
{
//initialize characters...
}
public function start()
{
...
}
}

class CarRacing implements Game
{
public function start()
{
...
}
}

class Pokemon implements Game
{
public function start()
{
...
}
}

接下來,把 Nintendo 改成這樣

1
2
3
4
5
6
7
8

class Nintendo
{
public function play(Game $game)
{
$game->start();
}
}

以後,每新增一款遊戲(對擴充開放),我們也不會動到 Nintendo 了(對修改封閉),喔耶!
而且 Nintendo 在呼叫 play 時,也不用管你是什麼遊戲,只要讓遊戲呼叫 start 就好了。

Reference

PHP 也有 Day #19 - PHP 返樸歸真系列之從實例學設計模式 by 大澤木小鐵 (Jace Ju)​
物件導向設計原則 SOLID
SOLID:五則皆變
The Principles of OOD