S.O.L.I.D: PHP 面向對象設計的五個基準原則

PHP

S.O.L.I.D 是 首個 5 個面向對象設計(OOD) 準則的首字母縮寫 ,這些準則是由 Robert C. Martin 提出的, 他更爲人所熟知的名字是 Uncle Bobphp

這些準則使得開發出易擴展、可維護的軟件變得更容易。也使得代碼更精簡、易於重構。一樣也是敏捷開發和自適應軟件開發的一部分。數據庫

備註這不是一篇簡單的介紹 "歡迎來到 _S.O.L.I.D" 的文章,這篇文章想要闡明 S.O.L.I.D 是什麼。json

S.O.L.I.D 意思是:

擴展出來的首字母縮略詞看起來可能很複雜,實際上它們很容易理解。api

  • S - 單一功能原則
  • O - 開閉原則
  • L - 里氏替換原則
  • I - 接口隔離原則
  • D - 依賴反轉原則

接下來讓咱們看看每一個原則,來了解爲何 S.O.L.I.D 能夠幫助咱們成爲更好的開發人員。數組

單一職責原則

縮寫是 S.R.P ,該原則內容是:ide

一個類有且只能有一個因素使其改變,意思是一個類只應該有單一職責.

例如,假設咱們有一些圖形,而且想要計算這些圖形的總面積.是的,這很簡單對不對?測試

class Circle {
    public $radius;

    public function construct($radius) {
        $this->radius = $radius;
    }
}

class Square {
    public $length;

    public function construct($length) {
        $this->length = $length;
    }
}

首先,咱們建立圖形類,該類的構造方法初始化必要的參數.接下來,建立AreaCalculator 類,而後編寫計算指定圖形總面積的邏輯代碼.this

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 或者其餘格式輸出呢?spa

全部邏輯都由 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() {
        // 計算長方體的體積
    }
}

這是一個更好的方式,可是要注意提示類型時不要僅僅提示一個 ShapeInterfaceSolidShapeInterface
你能建立其它的接口,好比 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 一開始彷佛很難掌握,但只要不斷地使用和遵照其原則,它將成爲你的一部分,使你的代碼易被擴展、修改,測試,即便重構也不容易出現問題。

文章轉自: https://learnku.com/php/t/28922

更多文章: https://learnku.com/php/c/tra...
相關文章
相關標籤/搜索