SOLID:面向對象設計的前五項原則

S.O.L.I.D是Robert C. Martin提出的前五個面向對象設計(OOD)原則的首字母縮寫,他更爲人所熟知的名字是Uncle Bob。程序員

將這些原理結合在一塊兒,可以使程序員輕鬆開發易於維護和擴展的軟件。它們還使開發人員能夠輕鬆避免代碼異味,輕鬆重構代碼,而且是敏捷或自適應軟件開發的一部分。數據庫

注意:這只是一篇簡單的「歡迎使用_ S.O.L.I.D 」,它只是闡明瞭S.O.L.I.D是什麼。json

更多學習內容能夠訪問從碼農成爲架構師的修煉之路數組

S.O.L.I.D表明:

首字母縮略詞在擴展時可能看起來很複雜,可是卻很容易掌握。架構

  • S - 單一責任原則
  • O - 開閉原理
  • L - Liskov替代原理
  • I - 接口隔離原理
  • D - 依賴倒置原則

讓咱們分別看一下每一個原理,瞭解一下S.O.L.I.D爲何能夠幫助使咱們成爲更好的開發人員。ide

單一責任原則

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類,咱們只需實例化該類並傳遞形狀數組,而後在頁面底部顯示輸出。this

$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() {
        // calculate the surface area of the cuboid
    }

    public function volume() {
        // calculate the volume of the cuboid
    }
}

這是一種更好的方法,但要注意的是在類型提示這些接口時要注意,而不是使用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彷佛不多,可是經過不斷使用和遵照其準則,它成爲你和你的代碼的一部分,能夠輕鬆地對其進行擴展,修改,測試和重構,而不會出現任何問題。

更多學習內容請訪問從碼農成爲架構師的修煉之路

相關文章
相關標籤/搜索