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面向对象视频教程》