摘要:六開閉原則開閉原則簡介開閉原則的英文名稱是,簡稱。開閉原則是面向對象設計中最基礎的設計原則,它指導我們如何建立一個穩定靈活的軟件系統。
面向對象基本原則(3)- 最少知道原則與開閉原則
面向對象基本原則(1)- 單一職責原則與接口隔離原則
面向對象基本原則(2)- 里式代換原則與依賴倒置原則
面向對象基本原則(3)- 最少知道原則與開閉原則
最少知識原則(Least KnowledgePrinciple,LKP)也稱為迪米特法則(Law of Demeter,LoD)。雖然名字不同,但描述的是同一個規則:一個對象應該對其他對象有最少的了解。
通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,你(被耦合或調用的類)的內部是如何復雜都和我沒關系,那是你的事情,我就知道你提供的這么多public方法,我就調用這么多,其他的我一概不關心。
2. 最少知道原則實現 只與直接關聯的類交流每個對象都必然會與其他對象有耦合關系,耦合關系的類型有很多,例如組合、聚合、依賴等。
出現在成員變量、方法的輸入輸出參數中的類稱為直接關聯的類,而出現在方法體內部的類不屬于直接關聯的類。
下面舉例說明如何才能做到只與直接關聯的類交流。
場景:老師想讓班長清點女生的數量
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(), " "; } } /** * 女生類 * Class Girl */ class Girl { } $teacher= new Teacher(); //老師發布命令 $teacher->command(new GroupLeader()); // 女生數量是:20
上面實例中,Teacher類僅有一個直接關聯的類 -- GroupLeader。而Girl這個類就是出現在commond方法體內,因此不屬于與Teacher類直接關聯的類。
方法是類的一個行為,類竟然不知道自己的行為與其他類產生依賴關系,這是不允許的,違反了迪米特法則。
對程序進行簡單的修改,把 對 $girlList 的初始化移出 Teacher 類,同時在 GroupLeader 中增加對 Girl 的注入,避開 Teacher 類對陌生類 Girl 的訪問,降低系統間的耦合,提高系統的健壯性。
下面是改進后的代碼:
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(), " "; } } /** * 女生類 * 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 "執行第一步安裝... "; // 模擬用戶點是或取消 return rand(0, 1); } /** * 第二步 * @return int */ public function second() { echo "執行第二步安裝... "; // 模擬用戶點是或取消 return rand(0, 1); } /** * 第三步 * @return int */ public function third() { echo "執行第三步安裝... "; // 模擬用戶點是或取消 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 "軟件安裝完成! "; } } } } } // 實例化軟件安裝類 $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個方法... "; // 模擬用戶點是或取消 return rand(0, 1); } //第二步 private function second() { echo "執行第2個方法... "; // 模擬用戶點是或取消 return rand(0, 1); } //第三個方法 private function third() { echo "執行第3個方法... "; // 模擬用戶點是或取消 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 "軟件安裝完成! "; } } } } } /** * 安裝軟件類 * Class InstallSoftware */ class InstallSoftware { /** * 執行安裝軟件操作 * @param Wizard $wizard */ public function installWizard(Wizard $wizard) { $wizard->installWizard(); } } // 實例化軟件安裝類 $invoker = new InstallSoftware(); // 開始安裝軟件 $invoker->installWizard(new Wizard()); // 運行結果和隨機數有關,每次的執行結果都不相同
代碼改進后,類間的耦合關系變弱了,結構也清晰了,變更引起的風險也變小了。
3. 最佳實踐在實際應用中經常會出現這樣一個方法:放在本類中也可以,放在其他類中也沒有錯,那怎么去衡量呢?
你可以堅持這樣一個原則:如果一個方法放在本類中,既不增加類間關系,也對本類不產生負面影響,那就放置在本類中。
在實際應用中,如果一個類跳轉兩次以上才能訪問到另一個類,就需要想辦法進行重構了。
因為一個系統的成功不僅僅是一個標準或是原則就能夠決定的,有非常多的外在因素決定,跳轉次數越多,系統越復雜,維護就越困難,所以只要跳轉不超過兩次都是可以忍受的,這需要具體問題具體分析。
迪米特法則要求類間解耦,但解耦是有限度的,除非是計算機的最小單元——二進制的0和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),找出預計有變化或不穩定的點,我們為這些變化點創建穩定的接口,準確地講是封裝可能發生的變化,一旦預測到或“第六感”發覺有變化,就可以進行封裝。
代碼使用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 "------書店賣出去的書籍記錄如下:-------- "; foreach($bookList as $book){ $price = $book->getPrice() / 100; echo <<getName()} 書籍作者: {$book->getAuthor()} 書籍價格: {$price} 元 --- TXT; }
------書店賣出去的書籍記錄如下:-------- 書籍名稱: 天龍八部 書籍作者: 金庸 書籍價格: 32 元 --- 書籍名稱: 巴黎圣母院 書籍作者: 雨果 書籍價格: 56 元 ---
一段時間之后,書店決定對小說類書籍進行打折促銷:所有40元以上的書籍9折銷售,其他的8折銷售。面對需求的變化,我們有兩種解決方案。
修改實現類NovelBook
直接修改NovelBook類中的getPrice()方法實現打折處理。該方法在項目有明確的章程(團隊內約束)或優良的架構設計時,是一個非常優秀的方法,但是該方法還是有缺陷的。例如采購書籍人員也是要看價格的,由于該方法已經實現了打折處理價格,因此采購人員看到的也是打折后的價格,會因信息不對稱而出現決策失誤的情況。
通過擴展實現變化
增加一個子類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 "------書店賣出去的書籍記錄如下:------ "; foreach($bookList as $book){ $price = $book->getPrice() / 100; echo <<getName()} 書籍作者: {$book->getAuthor()} 書籍價格: {$price} 元 --- 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 "------書店賣出去的書籍記錄如下:------ "; foreach($bookList as $book) { $price = $book->getPrice() / 100; echo <<getName()} 書籍作者: {$book->getAuthor()} 書籍價格: {$price} 元 --- TXT; }
------書店賣出去的書籍記錄如下:------ 書籍名稱: 天龍八部 書籍作者: 金庸 書籍價格: 28.8 元 --- 書籍名稱: 巴黎圣母院 書籍作者: 雨果 書籍價格: 50.4 元 --- 書籍名稱: 高性能MySQL 書籍作者: Baron 書籍價格: 48 元 ---
開閉原則對擴展開放,對修改關閉,并不意味著不做任何修改,低層模塊的變更,必然要有高層模塊進行耦合,否則就是一個孤立無意義的代碼片段。
參考文獻:《設計模式之禪》
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/31601.html
摘要:面向對象基本原則單一職責原則與接口隔離原則面向對象基本原則單一職責原則與接口隔離原則面向對象基本原則里式代換原則與依賴倒置原則面向對象基本原則最少知道原則與開閉原則一單一職責原則單一職責原則簡介單一職責原則的英文名稱是,簡稱。 面向對象基本原則(1)- 單一職責原則與接口隔離原則 面向對象基本原則(1)- 單一職責原則與接口隔離原則面向對象基本原則(2)- 里式代換原則與依賴倒置原則面...
摘要:四依賴倒置原則依賴倒置原則簡介依賴倒置原則的英文名稱是,簡稱。依賴倒置原則的表現其實就是面向接口編程。依賴倒置原則的優點減少類間的耦合性,提高系統的穩定性。結合里氏替換原則使用接口負責定義屬性和方法,并且聲明與其他對象的依賴關系。 面向對象基本原則(2)- 里式代換原則與依賴倒置原則 面向對象基本原則(1)- 單一職責原則與接口隔離原則面向對象基本原則(2)- 里式代換原則與依賴倒置原...
摘要:單一職責原則開閉原則里氏替換原則依賴倒置原則接口隔離原則迪米特法則組合聚合復用原則單一職責原則高內聚低耦合定義不要存在多于一個導致類變更的原因。建議接口一定要做到單一職責,類的設計盡量做到只有一個原因引起變化。使用繼承時遵循里氏替換原則。 單一職責原則 開閉原則 里氏替換原則 依賴倒置原則 接口隔離原則 迪米特法則 組合/聚合復用原則 單一職責原則(Single Responsi...
摘要:依賴倒置原則是個設計原則中最難以實現的原則,它是實現開閉原則的重要途徑,依賴倒置原則沒有實現,就別想實現對擴展開放,對修改關閉。 1、單一職能原則(Single Responsibility Principle, SRP) 定義 There should never be more than one reason for a class to change.應該有且僅有一個原因引起類的...
摘要:里氏替換原則里氏代換原則面向對象設計的基本原則之一。里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。里氏代換原則是對開閉原則的補充。而基類與子類的繼承關系就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規范。 showImg(https://segmentfault.com/img/bVbuXAu?w=640&h=361); 本文為本次系列文章的第一篇,接下...
閱讀 1227·2021-09-26 09:55
閱讀 3195·2019-08-30 15:55
閱讀 968·2019-08-30 15:53
閱讀 2297·2019-08-30 13:59
閱讀 2381·2019-08-29 13:08
閱讀 1110·2019-08-29 12:19
閱讀 3305·2019-08-26 13:41
閱讀 421·2019-08-26 13:24