Open Closed Principle
Entities should be open for extension, but closed for midification.
軟體中的對象,例如Class、Module、Method,這些對於擴展是開放的,但是對於修改是封閉的
OCP 是我在軟體開發中最愛使用的一個原則
對工程師們來說,開發軟體最怕的不是寫程式,而是改程式。改A 壞B,修C 又壞回A 的情況比比皆是
程式間的耦合性過高,雖然有寫測試化程式,但誰也無法保證測試的覆蓋率可以測到所有情況
OCP 便是減少修改過程中的出錯率,以及增加測試妥善率的最佳方法
而OCP 的最高境界就是達到:
藉由增加程式來擴充功能,而並非修改程式
講白了,舊有的程式你不去動他就沒事,而新增加的程式只要針對這部份補上測試程式碼就沒問題了
Example
以下是一個計算面積的類別,目前只支援正方形:
1 2 3 4 5 6 7 8 9 10 11
| class AreaCalculator { public function calculate() { foreach ($shapes as $shape) { $area[] = $shape->width * $shape->width; }
return array_sum($area); } }
|
將 $shapes
傳入當參數,並且透過 foreach
將計算好的面積透過 $area[]
暫存,再透過 array_sum
將總合輸出。
但是現在新增了一個需求, AreaCalculator
需加入計算圓形的需求
因此我們的程式碼改成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Circle { public $radius;
public function __construct($radius) { $this->radius = $radius; } }
class AreaCalculator { public function calculate() { foreach ($shapes as $shape) { if ($shape instanceof Square) { $area[] = $shape->width * $shape->width; } elseif ($shape instanceof Circle) { $area[] = $shape->radius * $shape->radius * pi(); } } } }
|
那麼,再加入一個三角形的需求呢?
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
| class Triangle { public $base;
public $height;
public function __construct($base, $height) { $this->base = $base;
$this->height = $height; } }
class AreaCalculator { public function calculate() { foreach ($shapes as $shape) { if ($shape instanceof Square) { $area[] = $shape->width * $shape->width; } elseif ($shape instanceof Circle) { $area[] = $shape->radius * $shape->radius * pi(); } else if ($shape instanceof Triangle) { } } } }
|
因為不斷的增加計算類別的需求,因此 calculate()
的結構也不斷的被破壞,而每一次的修改,便需要多一次的測試程式碼來驗證功能
Solution
有一項簡易的口訣來幫助優化:
Separate extensible behavior behind an interface, and flip the dependencies.
把所有需擴展的功能行為透過 interface
來定義,並且反轉兩邊的相依關係。
Separate extensible behavior
將行為透過interface 來定義:
1 2 3 4
| interface Shape { public function area(); }
|
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
| class Circle implements Shape { public $radius;
public function __construct($radius) { $this->radius = $radius; }
public function area() { return $this->radius * $this->radius * pi(); } }
class Square implements Shape { public $width;
public function __construct($width) { $this->width = $width; }
public function area() { return $this->width * $this->width; } }
|
Reconstruction
1 2 3 4 5 6 7 8 9 10 11
| class AreaCalculator { public function calculate(Shape $shapes) { foreach ($shapes as $shape) { $area[] = $shape->area(); }
return array_sum($area); } }
|
如此一來,當需要新增一個五邊形,或者六邊形的計算方法時,不需要再到 AreaCalculator
多寫任何一行code,而是把計算的方法反轉至所屬的class 進行。
OCP 可以運用的情境太多了,這邊再提供另一個範例
假設現在要開發第三方社群平台(Facebook、Google) 的登入以及註冊,要如何使用OCP 來進行
原先可能會這樣寫:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class AuthenticateController { public function attempt(Request $request) { if ($request->social == 'facebook') { } else if ($request->social == 'google') { } else { throw new InvalidRequestException; } } }
|
這樣的寫法如果遇到需要新增新的第三方平台時,就會破壞原有的結構,而使用了OCP 的寫法則會更改成如下:
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
| namespace SocialMedia;
interface ISocialable { public function register($token);
public function attempt($token); }
class Facebook implementss ISocialable { public function register($token) { }
public function attempt($token) { } }
class Google implementss ISocialable { public function register($token) { }
public function attempt($token) { } }
|
最後便是重構 AuthenticateController
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class AuthenticateController { public function attempt(Request $request) { $class = "\SocialMedia\\" . ucfirst($request->social);
$user = $this->fromSocialMediaLogin(new $class, $request->token); }
private function fromSocialMediaLogin(ISocialAble $type, $token) { return $type->attempt($token); } }
|
如此一來,不管擴展怎樣的第三方平台,都不需要更改 AuthenticateController
原先的邏輯