1. 程式人生 > >[Code Design] 六大設計原則之`開閉原則`

[Code Design] 六大設計原則之`開閉原則`

目錄

六大設計原則之開閉原則

定義

開閉原則是java世界裡最基礎的設計原則,它指導我們如何建立一個穩定,靈活的系統。開閉原則定義如下:

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

一個軟體實體如類,模組和函式應該對擴充套件開放,對修改關閉。

軟體實體應該是可以擴充套件的,但不能因修改而改變它在抽象層次上的確定性。

什麼是開閉原則

開閉原則明確的告訴我們:軟體實現應該對擴充套件開放,對修改關閉,其含義是說一個軟體實體應該通過擴充套件來實現變化,而不是通過修改已有的程式碼來實現變化的。那什麼是軟體實體呢?軟體實體包括以下幾個部分:

  • 專案或軟體產品中按照一定的邏輯規則劃分的模組
  • 抽象和類
  • 方法

一個軟體產品只要在生命週期內,都會發生變化,即然變化是一個事實,我們就應該在設計時儘量適應這些變化,以提高專案的穩定性和靈活性,真正實現“擁抱變化”。開閉原則告訴我們應儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改現有程式碼來完成變化,它是為軟體實體的未來事件而制定的對現行開發設計進行約束的一個原則。

我們舉例說明什麼是開閉原則,以書店銷售書籍為例,其類圖如下:

輸入圖片說明

書籍介面

/*
 * @ProjectName: 程式設計學習
 * @Copyright:   2018 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2018/11/28 22:34
 * @email:       [email protected]
 * @description: 本內容僅限於程式設計技術學習使用,轉發請註明出處.
 */
package
com.example.chapter3.design.ocp; /** * <p> * * </p> * * @author xiazhaoyang * @version V1.0 * @date 2018/11/28 22:34 * @modificationHistory=========================邏輯或功能性重大變更記錄 * @modify By: {修改人} 2018/11/28 * @modify reason: {方法名}:{原因} * ... */ public interface IBook { /** * 出售 * @return 收益 */ double doSell(); }

小說類書籍

/*
 * @ProjectName: 程式設計學習
 * @Copyright:   2018 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2018/11/28 22:42
 * @email:       [email protected]
 * @description: 本內容僅限於程式設計技術學習使用,轉發請註明出處.
 */
package com.example.chapter3.design.ocp;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version V1.0
 * @date 2018/11/28 22:42
 * @modificationHistory=========================邏輯或功能性重大變更記錄
 * @modify By: {修改人} 2018/11/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
@Data
@AllArgsConstructor
public class NovelBook implements IBook {

    private double price;

    private String name;

    private String author;

    @Override
    public double doSell() {
        return getPrice();
    }

    public static void main(String[] args) {
        System.out.println(new NovelBook(10.5,"天龍八部","金庸").doSell());
        //10.5
    }
}

專案投產生,書籍正常銷售,但是我們經常因為各種原因,要打折來銷售書籍,這是一個變化,我們要如何應對這樣一個需求變化呢?

我們有下面三種方法可以解決此問題:

  • 修改介面
    在IBook介面中,增加一個方法doDiscountSell(),專門用於進行打折處理,所有的實現類實現此方法。但是這樣的一個修改方式,實現類NovelBook要修改,同時IBook介面應該是穩定且可靠,不應該經常發生改變,否則介面作為契約的作用就失去了。因此,此方案否定。

  • 修改實現類
    修改NovelBook類的方法,直接在doSell()方法中實現打折處理。此方法是有問題的,例如我們如果doSell()方法中只需要讀取書籍的打折前的價格呢?這不是有問題嗎?當然我們也可以再增加doDiscountSell()方法,這也是可以實現其需求,但是這就有二個讀取價格的方法,因此,該方案也不是一個最優方案。

  • 通過擴充套件實現變化
    我們可以增加一個子類DiscountNovelBook,覆寫doSell方法。此方法修改少,對現有的程式碼沒有影響,風險少,是個好辦法。

下面是修改後的類圖:

輸入圖片說明

打折類

/*
 * @ProjectName: 程式設計學習
 * @Copyright:   2018 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2018/11/28 22:44
 * @email:       [email protected]
 * @description: 本內容僅限於程式設計技術學習使用,轉發請註明出處.
 */
package com.example.chapter3.design.ocp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version V1.0
 * @date 2018/11/28 22:44
 * @modificationHistory=========================邏輯或功能性重大變更記錄
 * @modify By: {修改人} 2018/11/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class DiscountNovelBook extends NovelBook {

    private double discount;

    public DiscountNovelBook(double discount) {
        super(12.5,"天龍八部","金庸");
        this.discount = discount;
    }

    @Override
    public double doSell() {
        return super.doSell() * discount;
    }

    public static void main(String[] args) {
        DiscountNovelBook discountNovelBook = new DiscountNovelBook(0.5);
        System.out.println(discountNovelBook.doSell());//6.25
    }
}

現在打折銷售開發完成了,我們只是增加了一個DiscountNovelBook類,我們修改的程式碼都是高層次的模組,沒有修改底層模組,程式碼改變數少,可以有效的防止風險的擴散。
我們可以把變化歸納為二種型別:

  • 邏輯變化
    只變化了一個邏輯,而不涉及其他模組,比如一個演算法是abc,現在需要修改為a+b+c,可以直接通過修改原有類中的方法的方式來完成,前提條件是所有依賴或關聯類都按照相同的邏輯處理

  • 子模組變化
    一個模組變化,會對其它的模組產生影響,特別是一個低層次的模組變化必然引起高層模組的變化,因此在通過擴充套件完成變化。

為什麼使用開閉原則

  • 第一:開閉原則非常有名,只要是面向物件程式設計,在開發時都會強調開閉原則

  • 第二:開閉原則是最基礎的設計原則,其它的五個設計原則都是開閉原則的具體形態,也就是說其它的五個設計原則是指導設計的工具和方法,而開閉原則才是其精神領袖。依照java語言的稱謂,開閉原則是抽象類,而其它的五個原則是具體的實現類。

  • 第三:開閉原則可以提高複用性

    • 在面向物件的設計中,所有的邏輯都是從原子邏輯組合而來,不是在一個類中獨立實現一個業務邏輯。只有這樣的程式碼才可以複用,粒度越小,被複用的可能性越大。那為什麼要複用呢?減少程式碼的重複,避免相同的邏輯分散在多個角落,減少維護人員的工作量。那怎麼才能提高複用率呢?縮小邏輯粒度,直到一個邏輯不可以分為止。

    • 保持軟體產品的穩定性,開閉原則要求我們通過保持原有程式碼不變新增新程式碼來實現軟體的變化,因為不涉及原始碼的改動,這樣可以避免為實現新功能而改壞線上功能的情況,避免老使用者的流失。

  • 第四:開閉原則可以提高維護性

    • 一款軟體量產後,維護人員的工作不僅僅對資料進行維護,還可能要對程式進行擴充套件,維護人員最樂意的事是擴充套件一個類,而不是修改一個類。讓維護人員讀懂原有程式碼,再進行修改,是一件非常痛苦的事情,不要讓他在原有的程式碼海洋中游蕩後再修改,那是對維護人員的折磨和摧殘。

    • 使程式碼更具模組化,易於維護。開閉原則可以讓程式碼中的各功能,以及新舊功能獨立存在於不同的單元模組中,一旦某個功能出現問題,可以很快地鎖定程式碼位置作出修改,由於模組間程式碼獨立不相互呼叫,更改一個功能的程式碼也不會引起其他功能的崩潰。

    • 不影響原有測試程式碼的執行。軟體開發規範性好的團隊都會寫單元測試,如果某條單元測試所測試的功能單元發生了變化,則單元測試程式碼也應做相應的斷言變更,否則就會導致單元測試執行紅條。如果每次軟體的變化,除了變更功能程式碼之外,還得變更測試程式碼,書寫測試程式碼同樣需要消耗工時,這樣在專案中引入單元測試就成了累贅。開閉原則可以讓單元測試充分發揮作用而又不會成為後期軟體開發的累贅。

  • 第五:面向物件開發的要求

    • 萬物皆物件,我們要把所有的事物抽象成物件,然後針對物件進行操作,但是萬物皆發展變化,有變化就要有策略去應對,怎麼快速應對呢?這就需要在設計之初考慮到所有可能變化的因素,然後留下介面,等待“可能”轉變為“現實”。

    • 提高開發效率。在程式碼開發中,有時候閱讀前人的程式碼是件很頭疼的事,尤其專案開發週期比較長,可能三五年,再加上公司人員流動性大,原有程式碼的開發人員早就另謀高就,而程式碼寫的更是一團糟,自帶混淆,能走彎路不走直路。而現在需要在原有功能的基礎上開發新功能,如果開閉原則使用得當的話,我們是不需要看懂原有程式碼實現細節便可以新增新程式碼實現新功能(例如示例中,我們不需要知道A產品是怎麼生產的,便可以開發生產B產品的功能),畢竟有時候閱讀一個功能的程式碼,比自己重新實現這個功能用的時間還要長。

如何使用開閉原則

  • 第一:抽象約束

抽象是對一組事物的通用描述,沒有具體的實現,也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此,通過介面或抽象類可以約束一組可能變化的行為,並且能夠實現對擴充套件開放,其包含三層含義:

  • 通過介面或抽象類約束擴散,對擴充套件進行邊界限定,不允許出現在介面或抽象類中不存在的public方法。

  • 引數型別,引用物件儘量使用介面或抽象類,而不是實現類,這主要是實現里氏替換原則的一個要求

  • 抽象層儘量保持穩定,一旦確定就不要修改

  • 第二:元資料(metadata)控制元件模組行為

程式設計是一個很苦很累的活,那怎麼才能減輕壓力呢?答案是儘量使用元資料來控制程式的行為,減少重複開發。什麼是元資料?用來描述環境和資料的資料,通俗的說就是配置引數,引數可以從檔案中獲得,也可以從資料庫中獲得。

  • 第三:制定專案章程

在一個團隊中,建立專案章程是非常重要的,因為章程是所有人員都必須遵守的約定,對專案來說,約定優於配置。這比通過介面或抽象類進行約束效率更高,而擴充套件性一點也沒有減少。

  • 第四:封裝變化

對變化封裝包含兩層含義:
(1)將相同的變化封裝到一個介面或抽象類中
(2)將不同的變化封裝到不同的介面或抽象類中,不應該有兩個不同的變化出現在同一個介面或抽象類中。
封裝變化,也就是受保護的變化,找出預計有變化或不穩定的點,我們為這些變化點建立穩定的介面。

注意事項

  • 開閉原則要求我們要儘可能的通過保持原有程式碼不變新增新程式碼而不是通過修改已有的程式碼來實現軟體產品的變化。

問題由來

  • 在軟體的生命週期內,因為變化、升級和維護等原因需要對軟體原有程式碼進行修改時,可能會給舊程式碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有程式碼經過重新測試。

解決方案

當軟體需要變化時,儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來實現變化。

開閉原則是面向物件設計中最基礎的設計原則,它指導我們如何建立穩定靈活的系統。開閉原則可能是設計模式六項原則中定義最模糊的一個了,它只告訴我們對擴充套件開放,對修改關閉,可是到底如何才能做到對擴充套件開放,對修改關閉,並沒有明確的告訴我們。以前,如果有人告訴我"你進行設計的時候一定要遵守開閉原則",我會覺的他什麼都沒說,但貌似又什麼都說了。因為開閉原則真的太虛了。

其實開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴充套件細節。因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟體架構的穩定。而軟體中易變的細節,我們用從抽象派生的實現類來進行擴充套件,當軟體需要發生變化時,我們只需要根據需求重新派生一個實現類來擴充套件就可以了。當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性才行。

總結

至此開閉原則的示例就基本完了。細心的朋友可能覺察到了,以面向抽象程式設計的方式去達到對擴充套件開放的目的,這和我們的依賴倒置原則(高層模組不應該依賴低層模組,兩者都應該依賴其抽象)怎麼這麼像。可能有朋友就要不滿了,不就是一個面向抽象程式設計,玩這麼多花樣幹嘛,將簡單的問題複雜化。博主的理解是這些設計原則沒有絕對的界限,它們只是從不同的側重點去約束我們的軟體架構,使其能使用各種不同的需求。

說到這裡,再簡要介紹下其他5項原則,它們主要是告訴我們用抽象構建框架,用實現擴充套件細節的注意事項而已:單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向介面程式設計;介面隔離原則告訴我們在設計介面的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴充套件開放,對修改關閉。

最後說明一下如何去遵守這六個原則。對這六個原則的遵守並不是是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是一樣,制定這六個原則的目的並不是要我們刻板的遵守他們,而需要根據實際情況靈活運用。對他們的遵守程度只要在一個合理的範圍內,就算是良好的設計。

REFRENCES

  1. 六大設計原則之開閉原則
  2. 小話設計模式原則之:開閉原則OCP
  3. 設計模式心法總則:開閉原則
  4. 極客學院 開閉原則

更多

掃碼關注或搜尋架構探險之道獲取最新文章,不積跬步無以至千里,堅持每週一更,堅持技術分享^_^