S.O.L.I.D 是物件導向設計(OOD)的頭五大基本原則的首字母縮寫,由俗稱「鮑勃大叔」的 Robert C. Martin 提出。
這些原則,結合在一起能夠方便程式設計師開發易於維護和擴充套件的軟體,也讓開發人員輕鬆避免程式碼異味,易於重構程式碼,也是敏捷或自適應軟體開發的一部分。
注意:這只是一篇“歡迎來到S.O.L.I.D”的簡單介紹文章,它只是揭示了S.O.L.I.D是什麼。
S.O.L.I.D 代表什麼:
雖然縮略詞展開後看似複雜,但其實非常容易掌握。
- S – 單一職責原則
- O – 開放封閉原則
- L – 里氏替換原則
- I – 介面隔離原則
- D – 依賴倒置原則
讓我們來單獨看看每個原則,來理解為什麼 S.O.L.I.D 能幫助我們成為更優秀的開發人員。
單一職責原則
(伯樂線上配圖)
S.R.P(簡稱)原則指出:
一個類應該有且只有一個去改變它的理由,這意味著一個類應該只有一項工作。
例如,假設我們有一些shape(形狀),並且我們想求所有shape的面積的和。這很簡單對嗎?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Circle { public $radius; public function __construct($radius) { $this->radius = $radius; } } class Square { public $length; public function __construct($length) { $this->length = $length; } } |
首先,我們建立shape類,讓建構函式設定需要的引數。接下來,我們繼續通過建立AreaCalculator類,然後編寫求取所提供的shape面積之和的邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class AreaCalculator { protected $shapes; public function __construct($shapes = array()) { $this->shapes = $shapes; } public function sum() { // logic to sum the areas } public function output() { return implode('', array( "<h1>", "Sum of the areas of provided shapes: ", $this->sum(), "</h1>" )); } } |
使用AreaCalculator類,我們簡單地例項化類,同時傳入一個shape陣列,並在頁面的底部顯示輸出。
1 2 3 4 5 6 7 8 9 |
$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); echo $areas->output(); |
輸出方法的問題在於,AreaCalculator處理了輸出資料的邏輯。因此,如果使用者想要以json或其他方式輸出資料該怎麼辦?
所有的邏輯將由AreaCalculator類處理,這是違反單一職責原則(SRP)的;AreaCalculator類應該只對提供的shape進行面積求和,它不應該關心使用者是需要json還是HTML。
因此,為了解決這個問題,你可以建立一個SumCalculatorOutputter類,使用這個來處理你所需要的邏輯,即對所提供的shape進行面積求和後如何顯示。
SumCalculatorOutputter類按如下方式工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); $output = new SumCalculatorOutputter($areas); echo $output->JSON(); echo $output->HAML(); echo $output->HTML(); echo $output->JADE(); |
現在,不管你需要何種邏輯來輸出資料給使用者,皆由SumCalculatorOutputter類處理。
開放封閉原則
物件或實體應該對擴充套件開放,對修改封閉。
這就意味著一個類應該無需修改類本身但卻容易擴充套件。讓我們看看AreaCalculator類,尤其是它的sum方法。
1 2 3 4 5 6 7 8 9 10 11 |
public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } else if(is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); } |
如果我們希望sum方法能夠對更多的shape進行面積求和,我們會新增更多的If / else塊,這違背了開放封閉原則。
能讓這個sum方法做的更好的一種方式是,將計算每個shape面積的邏輯從sum方法中移出,將它附加到shape類上。
1 2 3 4 5 6 7 8 9 10 11 |
class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } } |
對Circle類應該做同樣的事情,area方法應該新增。現在,計算任何所提的shape的面積的和的方法應該和如下簡單:
1 2 3 4 5 6 7 |
public function sum() { foreach($this->shapes as $shape) { $area[] = $shape->area; } return array_sum($area); } |
現在我們可以建立另一個shape類,並在計算和時將其傳遞進來,這不會破壞我們的程式碼。然而,現在另一個問題出現了,我們怎麼知道傳遞到AreaCalculator上的物件確實是一個shape,或者這個shape具有一個叫做area的方法?
對介面程式設計是S.O.L.I.D不可或缺的一部分,一個快速的例子是我們建立一個介面,讓每個shape實現它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
interface ShapeInterface { public function area(); } class Circle implements ShapeInterface { public $radius; public function __construct($radius) { $this->radius = $radius; } public function area() { return pi() * pow($this->radius, 2); } } |
在我們AreaCalculator的求和中,我們可以檢查所提供的shape確實是ShapeInterface的例項,否則我們丟擲一個異常:
1 2 3 4 5 6 7 8 9 10 11 12 |
public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException; } return array_sum($area); } |
里氏替換原則
(伯樂線上配圖)
在物件 x 為型別 T 時 q(x) 成立,那麼當 S 是 T 的子類時,物件 y 為型別 S 時 q(y) 也應成立。(即對父類的呼叫同樣適用於子類)
這一切說明的是,每一個子類或派生類應該可以替換它們基類或父類。
還利用AreaCalculator類,我們有一個VolumeCalculator類,它擴充套件了AreaCalculator類:
1 2 3 4 5 6 7 8 9 10 |
class VolumeCalculator extends AreaCalulator { public function __construct($shapes = array()) { parent::__construct($shapes); } public function sum() { // logic to calculate the volumes and then return and array of output return array($summedData); } } |
In the SumCalculatorOutputter class:
在SumCalculatorOutputter類中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class SumCalculatorOutputter { protected $calculator; public function __constructor(AreaCalculator $calculator) { $this->calculator = $calculator; } public function JSON() { $data = array( 'sum' => $this->calculator->sum(); ); return json_encode($data); } public function HTML() { return implode('', array( '<h1>', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '</h1>' )); } } |
如果我們試圖這樣來執行一個例子:
1 2 3 4 5 |
$areas = new AreaCalculator($shapes); $volumes = new AreaCalculator($solidShapes); $output = new SumCalculatorOutputter($areas); $output2 = new SumCalculatorOutputter($volumes); |
程式可以執行,但是當我們在$output2物件呼叫HTML方法,我們得到一個E_NOTICE錯誤,提示陣列到字串的轉換。
為了解決這個問題,不要從VolumeCalculator類的sum方法返回一個陣列,你應該:
1 2 3 4 |
public function sum() { // logic to calculate the volumes and then return and array of output return $summedData; } |
求和的結果作為一個浮點數,雙精度或整數。
介面隔離原則
不應強迫客戶端實現一個它用不上的介面,或是說客戶端不應該被迫依賴它們不使用的方法。
仍然以shape為例,我們知道也有立體shape,如果我們也想計算shape的體積,我們可以新增另一個合約到ShapeInterface:
1 2 3 4 |
interface ShapeInterface { public function area(); public function volume(); } |
任何我們建立的shape必須實現volume的方法,但是我們知道正方形是平面形狀沒有體積,所以這個介面將迫使正方形類實現一個它沒有使用的方法。
介面隔離原則(ISP)不允許這樣,你可以建立另一個名為SolidShapeInterface的介面,它有一個volume合約,對於立體形狀比如立方體等等,可以實現這個介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface ShapeInterface { public function area(); } interface SolidShapeInterface { public function volume(); } class Cuboid implements ShapeInterface, SolidShapeInterface { public function area() { // calculate the surface area of the cuboid } public function volume() { // calculate the volume of the cuboid } } |
這是一個更好的方法,但小心一個陷阱,當這些介面做型別提示時,不要使用ShapeInterface或SolidShapeInterface。
你可以建立另一個介面,可以是ManageShapeInterface,平面和立體shape都可用,這樣你可以很容易地看到它有一個管理shape的單一API。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
interface ManageShapeInterface { public function calculate(); } class Square implements ShapeInterface, ManageShapeInterface { public function area() { /*Do stuff here*/ } public function calculate() { return $this->area(); } } class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface { public function area() { /*Do stuff here*/ } public function volume() { /*Do stuff here*/ } public function calculate() { return $this->area() + $this->volume(); } } |
現在AreaCalculator類中,我們可以輕易用calculate替代area呼叫,同時可以檢查一個物件是ManageShapeInterface而不是ShapeInterface的例項。
依賴反轉原則
最後一條,但肯定不是最無足輕重的一條:
實體必須依靠抽象而不是具體實現。它表示高層次的模組不應該依賴於低層次的模組,它們都應該依賴於抽象。
這聽起來可能有點繞,但它很容易理解。這一原則允許解耦,這似乎是用來解釋這一原則最好的例子:
1 2 3 4 5 6 7 |
class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } } |
首先MySQLConnection是低層次模組,而PasswordReminder處於高層次,但根據S.O.L.I.D.中D的定義,即依賴抽象而不是具體實現,上面這段程式碼違反這一原則,PasswordReminder類被迫依賴於MySQLConnection類。
以後如果你改變資料庫引擎,你還必須編輯PasswordReminder類,因此違反了開閉原則。
PasswordReminder類不應該關心你的應用程式使用什麼資料庫,為了解決這個問題我們又一次“對介面程式設計”,因為高層次和低層次模組應該依賴於抽象,我們可以建立一個介面:
1 2 3 |
interface DBConnectionInterface { public function connect(); } |
介面有一個connect方法,MySQLConnection類實現該介面,在PasswordReminder類的建構函式不使用MySQLConnection類,而是使用介面替換,不用管你的應用程式使用的是什麼型別的資料庫,PasswordReminder類可以很容易地連線到資料庫,沒有任何問題,且不違反OCP。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MySQLConnection implements DBConnectionInterface { public function connect() { return "Database connection"; } } class PasswordReminder { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } } |
根據上面的程式碼片段,你現在可以看到,高層次和低層次模組依賴於抽象。
結論
老實說,S.O.L.I.D初看起來可能棘手,但只要通過連續使用並遵守其指導方針,它就會變成你和你的程式碼的一部分,可以讓你的程式碼很容易地擴充套件、修改、測試和重構,不出任何問題。