S.O.L.I.D 是首個5 個物件導向設計(OOD) 準則的首字母縮寫 ,這些準則是由Robert C. Martin 提出的, 他更為人所熟知的名字是Uncle Bob。
這些準則使得開發出易擴充、可維護的軟體變得更容易。也使得程式碼更精簡、易於重構。同樣也是敏捷開發和自適應軟體開發的一部分。
備註: 這不是一篇簡單的介紹"歡迎來到_S.O.L.I.D" 的文章,這篇文章想要闡明S.O.L.I.D 是什麼。 (相關教程推薦:php影片教學)
#擴展出來的首字母縮寫看起來可能很複雜,實際上它們很容易理解。
S.R.P ,該原則內容是:
一個類別有且只能有一個因素使其改變,意思是一個類別只應該有單一職責.例如,假設我們有一些圖形,並且想要計算這些圖形的總面積.是的,這很簡單對不對?
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 blocks ,然而這違背了開閉原則.
讓這個sum 方法變得更好的方式是將計算每個形狀面積的程式碼邏輯移出sum 方法,將其放進各個形狀類別:
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); } }在我們的
AreaCalculator 的sum 方法中,我們可以檢查提供的形狀類別的實例是否是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); }裡氏替換原則
如果對每一個類型為T1的物件o1,都有一個類型為T2 的物件o2,使得以T1定義的所有程式P 在所有的物件o1 都代換成o2時,程式P 的行為沒有發生變化,那麼類型T2 是類型T1 的子類型。
这句定义的意思是说:每个子类或者衍生类可以毫无问题地替代基类/父类。
依然使用 AreaCalculator 类, 假设我们有一个 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 错误,提示我们 数组被当做字符串使用的错误。
为了修复这个问题,只需:
public function sum() { // logic to calculate the volumes and then return and array of output return $summedData; }
而不是让VolumeCalculator 类的 sum 方法返回数组。
$summedData
是一个浮点数、双精度浮点数或者整型。
使用方(client)不应该依赖强制实现不使用的接口,或不应该依赖不使用的方法。
继续使用上面的 shapes
例子,已知拥有一个实心块,如果我们需要计算形状的体积,我们可以在 ShapeInterface 中添加一个方法:
interface ShapeInterface { public function area(); public function volume(); }
任何形状创建的时候必须实现 volume 方法,但是【平面】是没有体积的,实现这个接口会强制的让【平面】类去实现一个自己用不到的方法。
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 principle.
此 PasswordReminder 类不应该关注你的应用使用了什么数据库,为了进一步解决这个问题,我们「面向接口写代码」,由于高等级和低等级模块都应该依赖于抽象,我们可以创建一个接口:
interface DBConnectionInterface { public function connect(); }
这个接口有一个连接数据库的方法,MySQLConnection 类实现该接口,在 PasswordReminder 的构造方法中不要直接将类型约束设置为 MySQLConnection 类,而是设置为接口类,这样无论你的应用使用什么类型的数据库,PasswordReminder 类都能毫无问题地连接数据库,且不违背 开闭原则.
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 一开始似乎很难掌握,但只要不断地使用和遵守其原则,它将成为你的一部分,使你的代码易被扩展、修改,测试,即使重构也不容易出现问题。
相关PHP面向对象视频教程推荐:《PHP面向对象视频教程》