開閉原則的定義
開閉原則是java世界里最基礎(chǔ)的設(shè)計(jì)原則,它指導(dǎo)我們?nèi)绾谓⒁粋€(gè)穩(wěn)定,靈活的系統(tǒng)。開閉原則定義如下:
Software entities like classes,modules and functions should be open for extension but closed for modifications.
一個(gè)軟件實(shí)體如類,模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。
什么是開閉原則
開閉原則明確的告訴我們:軟件實(shí)現(xiàn)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉,其含義是說一個(gè)軟件實(shí)體應(yīng)該通過擴(kuò)展來實(shí)現(xiàn)變化,而不是通過修改已有的代碼來實(shí)現(xiàn)變化的。那什么是軟件實(shí)體呢?軟件實(shí)體包括以下幾個(gè)部分:
- 項(xiàng)目或軟件產(chǎn)品中按照一定的邏輯規(guī)則劃分的模塊
- 抽象和類
- 方法
一個(gè)軟件產(chǎn)品只要在生命周期內(nèi),都會(huì)發(fā)生變化,即然變化是一個(gè)事實(shí),我們就應(yīng)該在設(shè)計(jì)時(shí)盡量適應(yīng)這些變化,以提高項(xiàng)目的穩(wěn)定性和靈活性,真正實(shí)現(xiàn)“擁抱變化”。開閉原則告訴我們應(yīng)盡量通過擴(kuò)展軟件實(shí)體的行為來實(shí)現(xiàn)變化,而不是通過修改現(xiàn)有代碼來完成變化,它是為軟件實(shí)體的未來事件而制定的對(duì)現(xiàn)行開發(fā)設(shè)計(jì)進(jìn)行約束的一個(gè)原則。
我們舉例說明什么是開閉原則,以書店銷售書籍為例,其類圖如下:

書籍接口:
public interface IBook{
public String getName();
public String getPrice();
public String getAuthor();
}
小說類書籍:
public class NovelBook implements IBook{
private String name;
private int price;
private String author;
public NovelBook(String name,int price,String author){
this.name = name;
this.price = price;
this.author = author;
}
public String getAutor(){
return this.author;
}
public String getName(){
return this.name;
}
public int getPrice(){
return this.price;
}
}
Client類:
public class Client{
public static void main(Strings[] args){
IBook novel = new NovelBook("笑傲江湖",100,"金庸");
System.out.println("書籍名字:"+novel.getName()+"書籍作者:"+novel.getAuthor()+"書籍價(jià)格:"+novel.getPrice());
}
}
項(xiàng)目投產(chǎn)生,書籍正常銷售,但是我們經(jīng)常因?yàn)楦鞣N原因,要打折來銷售書籍,這是一個(gè)變化,我們要如何應(yīng)對(duì)這樣一個(gè)需求變化呢?
我們有下面三種方法可以解決此問題:
修改接口
在IBook接口中,增加一個(gè)方法getOffPrice(),專門用于進(jìn)行打折處理,所有的實(shí)現(xiàn)類實(shí)現(xiàn)此方法。但是這樣的一個(gè)修改方式,實(shí)現(xiàn)類NovelBook要修改,同時(shí)IBook接口應(yīng)該是穩(wěn)定且可靠,不應(yīng)該經(jīng)常發(fā)生改變,否則接口作為契約的作用就失去了。因此,此方案否定。修改實(shí)現(xiàn)類
修改NovelBook類的方法,直接在getPrice()方法中實(shí)現(xiàn)打折處理。此方法是有問題的,例如我們?nèi)绻鹓etPrice()方法中只需要讀取書籍的打折前的價(jià)格呢?這不是有問題嗎?當(dāng)然我們也可以再增加getOffPrice()方法,這也是可以實(shí)現(xiàn)其需求,但是這就有二個(gè)讀取價(jià)格的方法,因此,該方案也不是一個(gè)最優(yōu)方案。-
通過擴(kuò)展實(shí)現(xiàn)變化
我們可以增加一個(gè)子類OffNovelBook,覆寫getPrice方法。此方法修改少,對(duì)現(xiàn)有的代碼沒有影響,風(fēng)險(xiǎn)少,是個(gè)好辦法。下面是修改后的類圖:

打折類:
public class OffNovelBook extends NovelBook{
public OffNovelBook(String name,int price,String author){
super(name,price,author);
}
//覆寫價(jià)格方法,當(dāng)價(jià)格大于40,就打8析,其他價(jià)格就打9析
public int getPrice(){
if(this.price > 40){
return this.price * 0.8;
}else{
return this.price * 0.9;
}
}
}
現(xiàn)在打折銷售開發(fā)完成了,我們只是增加了一個(gè)OffNovelBook類,我們修改的代碼都是高層次的模塊,沒有修改底層模塊,代碼改變量少,可以有效的防止風(fēng)險(xiǎn)的擴(kuò)散。
我們可以把變化歸納為二種類型:
邏輯變化
只變化了一個(gè)邏輯,而不涉及其他模塊,比如一個(gè)算法是abc,現(xiàn)在需要修改為a+b+c,可以直接通過修改原有類中的方法的方式來完成,前提條件是所有依賴或關(guān)聯(lián)類都按照相同的邏輯處理子模塊變化
一人模塊變化,會(huì)對(duì)其它的模塊產(chǎn)生影響,特別是一個(gè)低層次的模塊變化必然引起高層模塊的變化,因此在通過擴(kuò)展完成變化。
為什么使用開閉原則
第一:開閉原則非常有名,只要是面向?qū)ο缶幊?,在開發(fā)時(shí)都會(huì)強(qiáng)調(diào)開閉原則
第二:開閉原則是最基礎(chǔ)的設(shè)計(jì)原則,其它的五個(gè)設(shè)計(jì)原則都是開閉原則的具體形態(tài),也就是說其它的五個(gè)設(shè)計(jì)原則是指導(dǎo)設(shè)計(jì)的工具和方法,而開閉原則才是其精神領(lǐng)袖。依照java語言的稱謂,開閉原則是抽象類,而其它的五個(gè)原則是具體的實(shí)現(xiàn)類。
第三:開閉原則可以提高復(fù)用性
在面向?qū)ο蟮脑O(shè)計(jì)中,所有的邏輯都是從原子邏輯組合而來,不是在一個(gè)類中獨(dú)立實(shí)現(xiàn)一個(gè)業(yè)務(wù)邏輯。只有這樣的代碼才可以復(fù)用,粒度越小,被復(fù)用的可能性越大。那為什么要復(fù)用呢?減少代碼的重復(fù),避免相同的邏輯分散在多個(gè)角落,減少維護(hù)人員的工作量。那怎么才能提高復(fù)用率呢?縮小邏輯粒度,直到一個(gè)邏輯不可以分為止。
第四:開閉原則可以提高維護(hù)性
一款軟件量產(chǎn)后,維護(hù)人員的工作不僅僅對(duì)數(shù)據(jù)進(jìn)行維護(hù),還可能要對(duì)程序進(jìn)行擴(kuò)展,維護(hù)人員最樂意的事是擴(kuò)展一個(gè)類,而不是修改一個(gè)類。讓維護(hù)人員讀懂原有代碼,再進(jìn)行修改,是一件非常痛苦的事情,不要讓他在原有的代碼海洋中游蕩后再修改,那是對(duì)維護(hù)人員的折磨和摧殘。
第五:面向?qū)ο箝_發(fā)的要求
萬物皆對(duì)象,我們要把所有的事物抽象成對(duì)象,然后針對(duì)對(duì)象進(jìn)行操作,但是萬物皆發(fā)展變化,有變化就要有策略去應(yīng)對(duì),怎么快速應(yīng)對(duì)呢?這就需要在設(shè)計(jì)之初考慮到所有可能變化的因素,然后留下接口,等待“可能”轉(zhuǎn)變?yōu)椤艾F(xiàn)實(shí)”。
如何使用開閉原則
第一:抽象約束
抽象是對(duì)一組事物的通用描述,沒有具體的實(shí)現(xiàn),也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此,通過接口或抽象類可以約束一組可能變化的行為,并且能夠?qū)崿F(xiàn)對(duì)擴(kuò)展開放,其包含三層含義:
- 通過接口或抽象類約束擴(kuò)散,對(duì)擴(kuò)展進(jìn)行邊界限定,不允許出現(xiàn)在接口或抽象類中不存在的public方法。
- 參數(shù)類型,引用對(duì)象盡量使用接口或抽象類,而不是實(shí)現(xiàn)類,這主要是實(shí)現(xiàn)里氏替換原則的一個(gè)要求
- 抽象層盡量保持穩(wěn)定,一旦確定就不要修改
第二:元數(shù)據(jù)(metadata)控件模塊行為
編程是一個(gè)很苦很累的活,那怎么才能減輕壓力呢?答案是盡量使用元數(shù)據(jù)來控制程序的行為,減少重復(fù)開發(fā)。什么是元數(shù)據(jù)?用來描述環(huán)境和數(shù)據(jù)的數(shù)據(jù),通俗的說就是配置參數(shù),參數(shù)可以從文件中獲得,也可以從數(shù)據(jù)庫(kù)中獲得。
第三:制定項(xiàng)目章程
在一個(gè)團(tuán)隊(duì)中,建立項(xiàng)目章程是非常重要的,因?yàn)檎鲁淌撬腥藛T都必須遵守的約定,對(duì)項(xiàng)目來說,約定優(yōu)于配置。這比通過接口或抽象類進(jìn)行約束效率更高,而擴(kuò)展性一點(diǎn)也沒有減少。
第四:封裝變化
對(duì)變化封裝包含兩層含義:
(1) 將相同的變化封裝到一個(gè)接口或抽象類中
(2) 將不同的變化封裝到不同的接口或抽象類中,不應(yīng)該有兩個(gè)不同的變化出現(xiàn)在同一個(gè)接口或抽象類中。
封裝變化,也就是受保護(hù)的變化,找出預(yù)計(jì)有變化或不穩(wěn)定的點(diǎn),我們?yōu)檫@些變化點(diǎn)創(chuàng)建穩(wěn)定的接口。