面向對象基本原則(3)- 最少知道原則與開閉原則

面向對象基本原則(3)- 最少知道原則與開閉原則

面向對象基本原則(1)- 單一職責原則與接口隔離原則
面向對象基本原則(2)- 裏式代換原則與依賴倒置原則
面向對象基本原則(3)- 最少知道原則與開閉原則php


5、最少知道原則【迪米特法則】

1. 最少知道原則簡介

最少知識原則(Least KnowledgePrinciple,LKP)也稱爲迪米特法則(Law of Demeter,LoD)。雖然名字不一樣,但描述的是同一個規則:一個對象應該對其餘對象有最少的瞭解。算法

通俗地講,一個類應該對本身須要耦合或調用的類知道得最少,你(被耦合或調用的類)的內部是如何複雜都和我不要緊,那是你的事情,我就知道你提供的這麼多public方法,我就調用這麼多,其餘的我一律不關心。數據庫

2. 最少知道原則實現

只與直接關聯的類交流

每一個對象都必然會與其餘對象有耦合關係,耦合關係的類型有不少,例如組合、聚合、依賴等。
出如今成員變量、方法的輸入輸出參數中的類稱爲直接關聯的類,而出如今方法體內部的類不屬於直接關聯的類。編程

下面舉例說明如何才能作到只與直接關聯的類交流。segmentfault

場景:老師想讓班長清點女生的數量設計模式

  • Bad
/**
 * 老師類
 * Class Teacher
 */
class Teacher {
    /**
     * 老師對班長髮布命令,清點女生數量
     * @param GroupLeader $groupLeader
     */
    public function command(GroupLeader $groupLeader)
    {
        // 產生一個女生羣體
        $girlList = new \ArrayIterator();
        // 初始化女生
        for($i = 0; $i < 20; $i++){
            $girlList->append(new Girl());
        }

        // 告訴班長開始執行清點任務
        $groupLeader->countGirls($girlList);
    }
}

/**
 * 班長類
 * Class GroupLeader
 */
class GroupLeader {

    /**
     * 清點女生數量
     * @param \ArrayIterator $girlList
     */
    public function countGirls($girlList)
    {
        echo "女生數量是:", $girlList->count(), "\n";
    }
}

/**
 * 女生類
 * Class Girl
 */
class Girl {

}

$teacher= new Teacher();
//老師發佈命令
$teacher->command(new GroupLeader()); // 女生數量是:20

上面實例中,Teacher類僅有一個直接關聯的類 -- GroupLeader。而Girl這個類就是出如今commond方法體內,所以不屬於與Teacher類直接關聯的類。
方法是類的一個行爲,類居然不知道本身的行爲與其餘類產生依賴關係,這是不容許的,違反了迪米特法則。架構

對程序進行簡單的修改,把 對 $girlList 的初始化移出 Teacher 類,同時在 GroupLeader 中增長對 Girl 的注入,避開 Teacher 類對陌生類 Girl 的訪問,下降系統間的耦合,提升系統的健壯性。
下面是改進後的代碼:app

  • Good
/**
 * 老師類
 * Class Teacher
 */
class Teacher {

    /**
     * 老師對班長髮布命令,清點女生數量
     * @param GroupLeader $groupLeader
     */
    public function command(GroupLeader $groupLeader)
    {
        // 告訴班長開始執行清點任務
        $groupLeader->countGirls();
    }
}

/**
 * 班長類
 * Class GroupLeader
 */
class GroupLeader {

    private $_girlList;

    /**
     * 傳遞全班的女生進來
     * GroupLeader constructor.
     * @param Girl[]|\ArrayIterator $girlList
     */
    public function __construct(\ArrayIterator $girlList)
    {
        $this->_girlList = $girlList;
    }

    //清查女生數量
    public function countGirls()
    {
        echo "女生數量是:", $this->_girlList->count(), "\n";
    }
}

/**
 * 女生類
 * Class Girl
 */
class Girl {

}

// 產生一個女生羣體
$girlList = new \ArrayIterator();
// 初始化女生
for($i = 0; $i < 20; $i++){
    $girlList->append(new Girl());
}

$teacher= new Teacher();
//老師發佈命令
$teacher->command(new GroupLeader($girlList)); // 女生數量是:20

關聯的類之間也要有距離

迪米特法則要求類「羞澀」一點,儘可能不要對外公佈太多的public方法和非靜態的public變量,儘可能內斂,多使用private、protected等訪問權限。編程語言

一個類公開的public屬性或方法越多,修改時涉及的面也就越大,變動引發的風險擴散也就越大。所以,爲了保持類間的距離,在設計時須要反覆衡量:是否還能夠再減小public方法和屬性,是否能夠修改成private、protected等訪問權限,是否能夠加上final關鍵字等。函數

實例場景:實現軟件安裝的過程,其中first方法定義第一步作什麼,second方法定義第二步作什麼,third方法定義第三步作什麼。

  • Bad
/**
 * 導向類
 * Class Wizard
 */
class Wizard {

    /**
     * 第一步
     * @return int
     */
    public function first()
    {
        echo "執行第一步安裝...\n";
        // 模擬用戶點是或取消
        return rand(0, 1);
    }

    /**
     * 第二步
     * @return int
     */
    public function second()
    {
        echo "執行第二步安裝...\n";
        // 模擬用戶點是或取消
        return rand(0, 1);
    }

    /**
     * 第三步
     * @return int
     */
    public function third()
    {
        echo "執行第三步安裝...\n";
        // 模擬用戶點是或取消
        return rand(0, 1);
    }
}

/**
 * 安裝軟件類
 * Class InstallSoftware
 */
class InstallSoftware {

    /**
     * 執行安裝軟件操做
     * @param Wizard $wizard
     */
    public function installWizard(Wizard $wizard)
    {
        $first = $wizard->first();
        //根據first返回的結果,看是否須要執行second
        if($first === 1){
            $second = $wizard->second();
            if($second === 1){
                $third = $wizard->third();
                if($third === 1){
                    echo "軟件安裝完成!\n";
                }
            }
        }
    }
}

// 實例化軟件安裝類
$invoker = new InstallSoftware();
// 開始安裝軟件
$invoker->installWizard(new Wizard()); // 運行結果和隨機數有關,每次的執行結果都不相同

Wizard類把太多的方法暴露給InstallSoftware類,二者的朋友關係太親密了,耦合關係變得異常牢固。若是要將Wizard類中的first方法返回值的類型由int改成boolean,就須要修改InstallSoftware類,從而把修改變動的風險擴散開了。所以,這樣的耦合是極度不合適的。

改進:在Wizard類中增長一個installWizard方法,對安裝過程進行封裝,同時把原有的三個public方法修改成private方法。

/**
 * 導向類
 * Class Wizard
 */
class Wizard {

    //第一步
    private function first()
    {
        echo "執行第1個方法...\n";
        // 模擬用戶點是或取消
        return rand(0, 1);
    }
    //第二步
    private function second()
    {
        echo "執行第2個方法...\n";
        // 模擬用戶點是或取消
        return rand(0, 1);
    }
    //第三個方法
    private function third()
    {
        echo "執行第3個方法...\n";
        // 模擬用戶點是或取消
        return rand(0, 1);
    }

    public function installWizard(){
        $first = $this->first();
        //根據first返回的結果,看是否須要執行second
        if($first === 1){
            $second = $this->second();
            if($second === 1){
                $third = $this->third();
                if($third === 1){
                    echo "軟件安裝完成!\n";
                }
            }
        }
    }
}

/**
 * 安裝軟件類
 * Class InstallSoftware
 */
class InstallSoftware {

    /**
     * 執行安裝軟件操做
     * @param Wizard $wizard
     */
    public function installWizard(Wizard $wizard)
    {
        $wizard->installWizard();
    }
}

// 實例化軟件安裝類
$invoker = new InstallSoftware();
// 開始安裝軟件
$invoker->installWizard(new Wizard()); // 運行結果和隨機數有關,每次的執行結果都不相同

代碼改進後,類間的耦合關係變弱了,結構也清晰了,變動引發的風險也變小了。

3. 最佳實踐

在實際應用中常常會出現這樣一個方法:放在本類中也能夠,放在其餘類中也沒有錯,那怎麼去衡量呢?
你能夠堅持這樣一個原則:若是一個方法放在本類中,既不增長類間關係,也對本類不產生負面影響,那就放置在本類中。

在實際應用中,若是一個類跳轉兩次以上才能訪問到另外一個類,就須要想辦法進行重構了。
由於一個系統的成功不只僅是一個標準或是原則就可以決定的,有很是多的外在因素決定,跳轉次數越多,系統越複雜,維護就越困難,因此只要跳轉不超過兩次都是能夠忍受的,這須要具體問題具體分析。

迪米特法則要求類間解耦,但解耦是有限度的,除非是計算機的最小單元——二進制的0和1。那纔是徹底解耦,在實際的項目中,須要適度地考慮這個原則,別爲了套用原則而作項目。
原則只是供參考,若是違背了這個原則,項目也未必會失敗,這就須要你們在採用原則時反覆度量,不遵循是不對的,嚴格執行就是「過猶不及」。

6、開閉原則

1. 開閉原則簡介

開閉原則的英文名稱是 Open-Close Principle,簡稱OCP。
開閉原則是面向對象設計中最基礎的設計原則,它指導咱們如何創建一個穩定、靈活的軟件系統。
開閉原則的英文定義是

Software entities like classes,modules and functions should be open for extension but closed for modifications.

一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。 其含義是說一個軟件實體應該經過擴展來實現變化,而不是經過修改已有的代碼來實現變化。

軟件實體包括如下幾個部分:

  • 項目或軟件產品中按照必定的邏輯規則劃分的模塊。
  • 抽象和類。
  • 方法。

一個軟件產品只要在生命期內,都會發生變化,既然變化是一個既定的事實,咱們就應該在設計時儘可能適應這些變化,以提升項目的穩定性和靈活性,真正實現「擁抱變化」。開閉原則告訴咱們應儘可能經過擴展軟件實體的行爲來實現變化,而不是經過修改已有的代碼來完成變化,它是爲軟件實體的將來事件而制定的對現行開發設計進行約束的一個原則。

2. 開閉原則的優勢

提升複用率

在面向對象的設計中,全部的邏輯都是從原子邏輯組合而來的,而不是在一個類中獨立實現一個業務邏輯。只有這樣代碼才能夠複用,粒度越小,被複用的可能性就越大。
複用能夠減小代碼量,避免相同的邏輯分散在多個角落,避免往後的維護人員爲了修改一個微小的缺陷或增長新功能而要在整個項目中處處查找相關的代碼。
那怎麼才能提升複用率呢?縮小邏輯粒度,直到一個邏輯不可再拆分爲止。

提升可維護性

一款軟件投產後,維護人員的工做不只僅是對數據進行維護,還可能要對程序進行擴展,維護人員最樂意作的事情就是擴展一個類,而不是修改一個類,甭管原有的代碼寫得多麼優秀仍是多麼糟糕,讓維護人員讀懂原有的代碼,而後再修改,是一件很痛苦的事情,不要讓他在原有的代碼海洋裏遊弋完畢後再修改,那是對維護人員的一種折磨和摧殘。

面向對象開發的要求

萬物皆對象,咱們須要把全部的事物都抽象成對象,而後針對對象進行操做,可是萬物皆運動,有運動就有變化,有變化就要有策略去應對。怎麼快速應對呢?這就須要在設計之初考慮到全部可能變化的因素,而後留下接口,等待「可能」轉變爲「現實」。

2. 變化的三種類型

邏輯變化

只變化一個邏輯,而不涉及其餘模塊,好比原有的一個算法是 a*b+c,如今須要修改成 a*b*c,能夠經過修改原有類中的方法的方式來完成,前提條件是全部依賴或關聯類都按照相同的邏輯處理。

子模塊變化

一個模塊變化,會對其餘的模塊產生影響,特別是一個低層次的模塊變化必然引發高層模塊的變化,所以在經過擴展完成變化時,高層次的模塊修改是必然的,剛剛的書籍打折處理就是相似的處理模塊,該部分的變化甚至會引發界面的變化。

視圖變化

可見視圖是提供給客戶使用的界面,該部分的變化通常會引發連鎖反應。若是僅僅是界面上按鈕、文字的從新排布卻是簡單,最司空見慣的是業務耦合變化,例如一個展現數據的列表,按照原有的需求是6列,忽然有一天要增長1列,並且這一列要跨N張表,處理M個邏輯才能展示出來,這樣的變化是比較恐怖的,但仍是能夠經過擴展來完成變化,這就要看咱們原有的設計是否靈活。

3. 開閉原則的使用

抽象約束

抽象是對一組事物的通用描述,沒有具體的實現,也就表示它能夠有很是多的可能性,能夠跟隨需求的變化而變化。所以,經過接口或抽象類能夠約束一組可能變化的行爲,而且可以實現對擴展開放,其包含三層含義:
第一,經過接口或抽象類約束擴展,對擴展進行邊界限定,不容許出如今接口或抽象類中不存在的public方法;
第二,參數類型、引用對象儘可能使用接口或者抽象類,而不是實現類;
第三,抽象層儘可能保持穩定,一旦肯定即不容許修改。

封裝變化

對變化的封裝包含兩層含義:
第一,將相同的變化封裝到一個接口或抽象類中;
第二,將不一樣的變化封裝到不一樣的接口或抽象類中,不該該有兩個不一樣的變化出如今同一個接口或抽象類中。
封裝變化,也就是受保護的變化(protected variations),找出預計有變化或不穩定的點,咱們爲這些變化點建立穩定的接口,準確地講是封裝可能發生的變化,一旦預測到或「第六感」發覺有變化,就能夠進行封裝。

4. Show me the code

書店售書場景

代碼使用PHP7.2語法編寫
  • 書籍接口
/**
 * Interface IBook
 * 書籍接口
 */
interface IBook {
    /**
     * 書籍名稱
     * @return mixed
     */
    public function getName() : string ;

    /**
     * 書籍價格
     * 這裏把價格定義爲int類型並非錯誤,
     * 在非金融類項目中對貨幣處理時,通常取2位精度,
     * 一般的設計方法是在運算過程當中擴大100倍,在須要展現時再縮小100倍,減小精度帶來的偏差。
     * @return mixed
     */
    public function getPrice() : int ;

    /**
     * 書籍做者
     * @return mixed
     */
    public function getAuthor() : string ;
}
  • 小說類
/**
 * 小說類
 * Class NovelBook
 */
class NovelBook implements IBook {

    /**
     * 書籍名稱
     * @var string $_name
     */
    private $_name;

    /**
     * 書籍價格
     * @var int $_price
     */
    private $_price;

    /**
     * 書籍做者
     * @var string $_author
     */
    private $_author;

    /**
     * 經過構造函數傳遞書籍信息
     * @param string $name
     * @param int $price
     * @param string $author
     */
    public function __construct(string $name, int $price, string $author)
    {
        $this->_name = $name;
        $this->_price = $price;
        $this->_author = $author;
    }

    /**
     * 獲取書籍名稱
     * @return string
     */
    public function getName() : string
    {
        return $this->_name;
    }

    /**
     * 獲取書籍價格
     * @return int
     */
    public function getPrice() : int
    {
        return $this->_price;
    }

    /**
     * 獲取書籍做者
     * @return string
     */
    public function getAuthor() : string
    {
        return $this->_author;
    }

}
  • 售書場景
// 產生一個書籍列表
$bookList = new ArrayIterator();

// 始化數據
$bookList->append(new NovelBook("天龍八部",3200,"金庸"));
$bookList->append(new NovelBook("巴黎聖母院",5600,"雨果"));

echo "------書店賣出去的書籍記錄以下:--------\n";
foreach($bookList as $book){
    $price = $book->getPrice() / 100;
    echo <<<TXT
書籍名稱: {$book->getName()}
書籍做者: {$book->getAuthor()}
書籍價格: {$price} 元
---\n
TXT;
}
------書店賣出去的書籍記錄以下:--------
書籍名稱: 天龍八部
書籍做者: 金庸
書籍價格: 32 元
---
書籍名稱: 巴黎聖母院
書籍做者: 雨果
書籍價格: 56 元
---

一段時間以後,書店決定對小說類書籍進行打折促銷:全部40元以上的書籍9折銷售,其餘的8折銷售。面對需求的變化,咱們有兩種解決方案。

  1. 修改實現類NovelBook

直接修改NovelBook類中的getPrice()方法實現打折處理。該方法在項目有明確的章程(團隊內約束)或優良的架構設計時,是一個很是優秀的方法,可是該方法仍是有缺陷的。例如採購書籍人員也是要看價格的,因爲該方法已經實現了打折處理價格,所以採購人員看到的也是打折後的價格,會因信息不對稱而出現決策失誤的狀況。

  1. 經過擴展實現變化

增長一個子類OffNovelBook,覆寫getPrice方法,高層次的模塊經過OffNovelBook類產生新的對象,完成業務變化對系統的最小化開發,修改少,風險也小。

  • 打折銷售的小說類
/**
 * 打折銷售的小說類
 * Class OffNovelBook
 */
class OffNovelBook extends NovelBook {
    /**
     * 覆寫獲取銷售價格方法
     *
     * @return int
     */
    public function getPrice() : int
    {
        //原價
        $originPrice = parent::getPrice();

        if($originPrice > 40){ //原價大於40元,則打9折
            $discountPrice = $originPrice * 90 / 100;
        }else{
            $discountPrice = $originPrice * 80 / 100;
        }
        return $discountPrice;
    }
}
  • 打折售書場景
// 產生一個書籍列表
$bookList = new ArrayIterator();

// 始化數據,實際項目中通常是由持久層完成
$bookList->append(new OffNovelBook("天龍八部",3200,"金庸"));
$bookList->append(new OffNovelBook("巴黎聖母院",5600,"雨果"));

echo "------書店賣出去的書籍記錄以下:------\n";
foreach($bookList as $book){
    $price = $book->getPrice() / 100;
    echo <<<TXT
書籍名稱: {$book->getName()}
書籍做者: {$book->getAuthor()}
書籍價格: {$price} 元
---\n
TXT;
}
------書店賣出去的書籍記錄以下:------
書籍名稱: 天龍八部
書籍做者: 金庸
書籍價格: 28.8 元
---
書籍名稱: 巴黎聖母院
書籍做者: 雨果
書籍價格: 50.4 元
---

又過了一段時間,書店新增長了計算機書籍,它不只包含書籍名稱、做者、價格等信息,還有一個獨特的屬性:面向的是什麼領域,也就是它的範圍,好比是和編程語言相關的,仍是和數據庫相關的,等等。

  • 增長一個IComputerBook接口,它繼承自IBook
/**
 * 計算機類書籍接口
 * Interface IComputerBook
 */
interface IComputerBook extends IBook
{
    /**
     * 計算機書籍增長一個範圍屬性
     * @return string
     */
    public function getScope() : string ;
}
  • 計算機書籍類
/**
 * 計算機書籍類
 * Class ComputerBook
 */
class ComputerBook implements IComputerBook
{
    /**
     * 書籍名稱
     * @var string $_name
     */
    private $_name;

    /**
     * 書籍價格
     * @var int $_price
     */
    private $_price;

    /**
     * 書籍做者
     * @var string $_author
     */
    private $_author;

    /**
     * 書籍範圍
     * @var string $_scope
     */
    private $_scope;

    /**
     * 經過構造函數傳遞書籍信息
     * ComputerBook constructor.
     * @param string $name
     * @param int $price
     * @param string $author
     * @param string $scope
     */
    public function __construct(string $name, int $price, string $author, string $scope)
    {
        $this->_name = $name;
        $this->_price = $price;
        $this->_author = $author;
        $this->_scope = $scope;
    }

    /**
     * 獲取書籍名稱
     * @return string
     */
    public function getName() : string
    {
        return $this->_name;
    }

    /**
     * 獲取書籍價格
     * @return int
     */
    public function getPrice() : int
    {
        return $this->_price;
    }

    /**
     * 獲取書籍做者
     * @return string
     */
    public function getAuthor() : string
    {
        return $this->_author;
    }

    /**
     * 獲取書籍範圍
     * @return string
     */
    public function getScope() : string
    {
        return $this->_scope;
    }
}
  • 增長計算機書籍銷售
//產生一個書籍列表
$bookList = new ArrayIterator();

// 始化數據,實際項目中通常是由持久層完成
$bookList->append(new OffNovelBook("天龍八部",3200,"金庸"));
$bookList->append(new OffNovelBook("巴黎聖母院",5600,"雨果"));
$bookList->append(new ComputerBook("高性能MySQL",4800,"Baron", '數據庫'));


echo "------書店賣出去的書籍記錄以下:------\n";
foreach($bookList as $book) {
    $price = $book->getPrice() / 100;
    echo <<<TXT
書籍名稱: {$book->getName()}
書籍做者: {$book->getAuthor()}
書籍價格: {$price} 元
---\n
TXT;
}
------書店賣出去的書籍記錄以下:------
書籍名稱: 天龍八部
書籍做者: 金庸
書籍價格: 28.8 元
---
書籍名稱: 巴黎聖母院
書籍做者: 雨果
書籍價格: 50.4 元
---
書籍名稱: 高性能MySQL
書籍做者: Baron
書籍價格: 48 元
---

開閉原則對擴展開放,對修改關閉,並不意味着不作任何修改,低層模塊的變動,必然要有高層模塊進行耦合,不然就是一個孤立無心義的代碼片斷。

參考文獻:《設計模式之禪》
相關文章
相關標籤/搜索