S.O.L.I.D是Robert C. Martin提出的前五個物件導向設計(OOD)原則的首字母縮寫,他更為人所熟知的名字是Uncle Bob。
將這些原理結合在一起,可使程式設計師輕鬆開發易於維護和擴充套件的軟體。它們還使開發人員可以輕鬆避免程式碼異味,輕鬆重構程式碼,並且是敏捷或自適應軟體開發的一部分。
S.O.L.I.D代表:
首字母縮略詞在擴充套件時可能看起來很複雜,但是卻很容易掌握。
-
S - 單一責任原則
-
O - 開閉原理
-
L - Liskov替代原理
-
I - 介面隔離原理
-
D - 依賴倒置原則
讓我們分別看一下每個原理,瞭解一下S.O.L.I.D為什麼可以幫助使我們成為更好的開發人員。
單一責任原則
SRP的簡稱-此原則指出:
一個類有且只能有一個因素使其改變,意思是一個類只應該有單一職責.
例如,假設我們有一些形狀,我們想對形狀的所有區域求和。好吧,這很簡單對吧?
class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } class Square { public $length; public function construct($length) { $this->length = $length; } }
首先,我們建立形狀類,並讓建構函式設定所需的引數。接下來,我們繼續建立AreaCalculator類,然後編寫邏輯以總結所有提供的形狀的面積。
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( "", "Sum of the areas of provided shapes: ", $this->sum(), "" )); } }
要使用AreaCalculator類,我們只需例項化該類並傳遞形狀陣列,然後在頁面底部顯示輸出。
$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); echo $areas->output();
輸出方法的問題在於AreaCalculator處理邏輯以輸出資料。因此,如果使用者希望將資料輸出為json或其他內容怎麼辦呢?
所有這些邏輯都將由AreaCalculator類處理,這是SRP所反對的。在AreaCalculator類應該只提供總結形狀的區域,它不應該關心使用者是否希望JSON或HTML。
因此,要解決此問題,你可以建立一個SumCalculatorOutputter類,並使用它來處理處理所有提供的形狀的總面積如何顯示所需的任何邏輯。
該SumCalculatorOutputter類會的工作是這樣的:
$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方法。
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方法能夠對更多形狀的區域求和,則必須新增更多if / else塊,這違背了Open-closed原理。
我們可以使這種求和方法更好的一種方法是從求和方法中刪除用於計算每個形狀的面積的邏輯,並將其附加到形狀的類中。
class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } }
對Circle類應該做同樣的事情,應該新增一個area方法。現在,要計算提供的任何形狀的總和應該很簡單:
public function sum() { foreach($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); }
現在,我們可以建立另一個形狀類,並在計算總和時傳遞它,而不會破壞我們的程式碼。但是,現在又出現了另一個問題,我們如何知道傳遞到AreaCalculator中的物件實際上是一個形狀,或者該形狀是否具有名為area的方法?
編碼介面是S.O.L.I.D不可或缺的一部分,一個簡單的示例是我們建立一個介面,每種形狀都可以實現:
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); } }
在我們的AreaCalculatorsum方法中,我們可以檢查所提供的形狀是否實際上是ShapeInterface的例項,否則我們丟擲異常:
public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException; } return array_sum($area); }
Liskov替代原則
如果對每一個型別為 T1 的物件 o1,都有型別為 T2 的物件 o2,使得以 T1 定義的所有程式 P 在所有的物件 o1 都代換成 o2 時,程式 P 的行為沒有發生變化,那麼型別 T2 是型別 T1 的子型別。
所有這一切都說明,每個子類/派生類都可以替代其基類/父類。
仍然使用OutAreaCalculator類,例如我們有一個VolumeCalculator類,它擴充套件了AreaCalculator類:
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); } }
在SumCalculatorOutputter類中:
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( '', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '' )); } }
如果我們嘗試執行這樣的一個例子:
$areas = new AreaCalculator($shapes); $volumes = new AreaCalculator($solidShapes); $output = new SumCalculatorOutputter($areas); $output2 = new SumCalculatorOutputter($volumes);
該程式不會出問題,但是當我們在$ output2物件上呼叫HTML方法時,會收到E _ NOTICE錯誤,通知我們陣列轉換為字串。
若要解決此問題,而不是從VolumeCalculator類的sum方法返回陣列,你應該簡單地:
public function sum() { // logic to calculate the volumes and then return and array of output return $summedData; }
求和後的資料為浮點,雙精度或整數。
介面隔離原理
使用方(client)不應該依賴強制實現不使用的介面,或不應該依賴不使用的方法。
仍然使用形狀示例,我們知道我們也有實體形狀,因此由於我們還想計算形狀的體積,因此可以向ShapeInterface新增另一個協定:
interface ShapeInterface { public function area(); public function volume(); }
我們建立的任何形狀都必須實現volume方法,但是我們知道正方形是扁平形狀並且它們沒有體積,因此此介面將強制Square類實現一種不使用的方法。
ISP 原則不允許這麼去做,所以我們應該建立另外一個擁有 volume 方法的 SolidShapeInterface 介面去代替這種方式,這樣類似立方體的實心體就可以實現這個介面了:
interface ShapeInterface { public function area(); } interface SolidShapeInterface { public function volume(); } class Cuboid implements ShapeInterface, SolidShapeInterface { public function area() { //計算長方體的表面積 } public function volume() { // 計算長方體的體積 } }
這是一種更好的方法,但要注意的是在型別提示這些介面時要注意,而不是使用ShapeInterface或SolidShapeInterface。
您可以建立另一個介面,也許是ManageShapeInterface,並在平面和實體形狀上都實現它,這樣您就可以輕鬆地看到它具有用於管理形狀的單個API。例如:
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 。
依賴倒置原則
最後但並非最不重要的一點是:
實體必須依賴於抽象而不依賴於具體。它指出高階模組一定不能依賴於低階模組,而應該依賴於抽象。
這也許聽起來讓人頭大,但確實很容易理解。該原理允許去耦,這個例子似乎是解釋該原理的最佳方法:
class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } }
首先,MySQLConnection是低等級模組,然而PasswordReminder是高等級模組,但是根據 S.O.L.I.D. 中 D 的解釋:依賴於抽象而不依賴與實現, 上面的程式碼段違背了這一原則,因為PasswordReminder類被強制依賴於MySQLConnection類。
以後,如果您要更改資料庫引擎,則還必須編輯PasswordReminder類,從而違反了Open-close原理。
該PasswordReminder類不應該關心什麼資料庫應用程式使用,以解決這個問題,我們再次“程式碼的介面”,因為高層次和低層次的模組應該依賴於抽象,我們可以建立一個介面:
interface DBConnectionInterface { public function connect(); }
該介面具有一個connect方法,而MySQLConnection類實現了此介面,而且也沒有直接在PasswordReminder的建構函式中直接提示MySQLConnection類,而是改為提示該介面,無論您的應用程式使用哪種資料庫型別,PasswordReminder類可以輕鬆連線到資料庫而不會出現任何問題,並且不會違反OCP。
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似乎很少,但是通過不斷使用和遵守其準則,它成為你和你的程式碼的一部分,可以輕鬆地對其進行擴充套件,修改,測試和重構,而不會出現任何問題。
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(); }}