如需下載源碼,請訪問
https://github.com/fengchuanfang/Open_Closed_Principle
文章原創(chuàng),轉(zhuǎn)載請注明出處:
設(shè)計模式心法總則:開閉原則
開閉原則(Open-Closed Principle,OCP)是面向?qū)ο笤O(shè)計中“可復(fù)用設(shè)計”的基石,是設(shè)計模式最基本的法則。后面將要提到的五大設(shè)計原則和23種設(shè)計模式都可以看做是開閉原則的實現(xiàn)方法和手段。
1988年,勃蘭特·梅耶(Bertrand Meyer)在他的著作《面向?qū)ο筌浖?gòu)造(Object Oriented Software Construction)》中提出了開閉原則,它的原文是這樣:“Software entities should be open for extension,but closed for modification”。翻譯過來就是:“軟件實體應(yīng)當(dāng)對擴展開放,對修改關(guān)閉”。
說的通俗一點就是,已經(jīng)開發(fā)好的軟件實體(如類、模塊、函數(shù)),在升級迭代引入新功能時,不應(yīng)該修改已有的代碼,而是在已有代碼的基礎(chǔ)上,添加新代碼來實現(xiàn)。
每個公司的軟件產(chǎn)品只要還在運營,都避免不了升級迭代,優(yōu)化老功能,添加新功能,來滿足用戶不斷膨脹的需求。
軟件產(chǎn)品的變化無疑是個既定的事實,為了實現(xiàn)這種變化,我們既可以修改已有的代碼,也可以保持已有的代碼不變添加新代碼。
就像一個工廠,原本只有生產(chǎn)A產(chǎn)品的生產(chǎn)線1,現(xiàn)因業(yè)務(wù)拓展,需要生產(chǎn)B產(chǎn)品,既可以改裝一下生產(chǎn)線1,讓它即生產(chǎn)A產(chǎn)品又生產(chǎn)B產(chǎn)品;也可以保持生產(chǎn)線1不變,再開展一條生產(chǎn)線2,生產(chǎn)B產(chǎn)品。
開閉原則要求我們要盡可能的通過保持原有代碼不變添加新代碼而不是通過修改已有的代碼來實現(xiàn)軟件產(chǎn)品的變化。
開閉原則為我們在進行軟件開發(fā)設(shè)計時提供了一個準(zhǔn)則,要求我們設(shè)計開發(fā)出的軟件,在應(yīng)對未來可能出現(xiàn)的變化時,要具備良好的拓展性,要能通過添加新代碼而不是修改已有的代碼來實現(xiàn)這種可能的變化。
開閉原則要貫穿在軟件開發(fā)的各個層級中,要利用在每一個方法中,由方法組成的類中,由功能密切的類組成的模塊中,由相互依存的模塊組成的項目中。
下面通過一個示例讓我們來體驗一下開閉原則
一個工廠原本只有生產(chǎn)A產(chǎn)品的生產(chǎn)線1,現(xiàn)因業(yè)務(wù)拓展,需要生產(chǎn)B產(chǎn)品
設(shè)計一:
只有生產(chǎn)A產(chǎn)品的生產(chǎn)線的時候
工廠是這樣的:
public class Factory {
public String prodLine(){
return "產(chǎn)品A";
}
}
客戶是這樣的:
public class Client1 {
public static void main(String args[]) {
System.out.println("我訂購并收到產(chǎn)品:"+order());
}
private static String order(){
Factory factory = new Factory();
return factory.prodLine();
}
}
運行之后,可以滿足客戶1的需求:
我訂購并收到產(chǎn)品:產(chǎn)品A
現(xiàn)因業(yè)務(wù)拓展,需要加一條生產(chǎn)B產(chǎn)品的生產(chǎn)線
對于工廠,我們可以這么改:
public class Factory {
public String prodLine(int type) {
if(type == 0){
return "產(chǎn)品A";
}else{
return "產(chǎn)品B";
}
}
}
改完之后,我們就會發(fā)現(xiàn)客戶1已經(jīng)爆紅了,生產(chǎn)線方法prodLine加了個參數(shù)int type,所以客戶1就不能像原來那樣下訂單了,因為現(xiàn)在是混合型生產(chǎn)線,再下訂單時需要指明需要的產(chǎn)品,否則生產(chǎn)不了。
這種讓客戶調(diào)整自己適應(yīng)公司的改變,而公司只會增加客戶的負擔(dān)而不能給其帶來額外利潤的做法,在客戶至上,客戶利益大于一切的今天,無疑是十分愚蠢的。
客戶1看在與公司多年合作,友誼深厚的份上,委屈一下自己勉強做下改變,以后再下訂單的時候指定所需產(chǎn)品類型。
public class Client1 {
public static void main(String args[]) {
System.out.println("我訂購并收到產(chǎn)品:"+order());
}
private static String order(){
Factory factory = new Factory();
return factory.prodLine(0);
}
}
客戶1又可以正常向工廠訂購并收取產(chǎn)品A了
然后是客戶2,工廠經(jīng)過調(diào)整,已經(jīng)可以生產(chǎn)其所需要的產(chǎn)品B了,可以向工廠下單了
public class Client2 {
public static void main(String args[]) {
System.out.println("我訂購并收到產(chǎn)品:"+order());
}
private static String order(){
Factory factory = new Factory();
return factory.prodLine(1);
}
}
客戶2也可以正常向工廠訂購并收取產(chǎn)品B了
以后再有新的需求,依然可以改造原有的生產(chǎn)線prodLine,只需根據(jù)type不同多加幾個if…else…即可。
但是這樣做增加了生產(chǎn)線方法調(diào)用的復(fù)雜性,不僅需要改變原有客戶類Client1,而且對于之后新加進來客戶類在調(diào)用生產(chǎn)線方法時,都需要適應(yīng)這種復(fù)雜性,一旦參數(shù)傳錯,就收取不到自己所需的產(chǎn)品。隨著新客戶需求的不斷增加,生產(chǎn)線方法中的代碼也會暴增,最后就會變得極難維護。
設(shè)計二:
在工廠類Factory中保持原生產(chǎn)線prodLine不變,新加一個生產(chǎn)線prodLine1,生產(chǎn)產(chǎn)品B,如下:
public class Factory {
public String prodLine(){
return "產(chǎn)品A";
}
public String prodLine1(){
return "產(chǎn)品B";
}
}
這樣改完之后,客戶1不做任何更改,依然可以正常運行
只需添加客戶2:Client2
public class Client2 {
public static void main(String args[]) {
System.out.println("我訂購并收到產(chǎn)品:"+order());
}
private static String order(){
Factory factory = new Factory();
return factory.prodLine1();
}
}
客戶2也可以正常向工廠訂購并收取產(chǎn)品B了
這樣做可以保證客戶1不做任何的更改,不需要因工廠的改變而改變自己,同時也可以滿足客戶2的需求。
但這就是開閉原則了嗎,其實還遠著呢。
這樣做依然是有問題的,如果再有新的客戶需求,我們依然需要在工廠類中添加生產(chǎn)線,會引起工廠類的爆炸導(dǎo)致生產(chǎn)線方法過多,新的客戶類在調(diào)用的時候,需要在一大堆的生產(chǎn)線方法中選擇出自己需要的那個。而且,新增的生產(chǎn)線方法對原有的客戶類也是可見的,如果哪天客戶喝大了,一不留神就有可能下錯單,收錯貨。
設(shè)計三:到了開閉原則展示自己魅力的時候了
如果開始時,我們是這么設(shè)計的
先寫個工廠接口,預(yù)知未來可能的變化,需要生產(chǎn)線方法,如下:
public interface IFactory {
String prodLine();
}
滿足客戶1的工廠FactoryA實現(xiàn)IFactory,生產(chǎn)產(chǎn)品A,如下:
public class FactoryA implements IFactory{
@Override
public String prodLine() {
return "產(chǎn)品A";
}
}
對于客戶1中的代碼:
public class Client1 {
public static void main(String args[]) {
System.out.println("我訂購并收到產(chǎn)品:"+order());
}
private static String order(){
IFactory factory = new FactoryA();
return factory.prodLine();
}
}
客戶1中持有工廠接口IFactory的引用,指向其實現(xiàn)類,
運行之后,可以滿足客戶1的需求:
我訂購并收到產(chǎn)品:產(chǎn)品A
現(xiàn)在公司需要拓展業(yè)務(wù)滿足客戶2的需求,只需添加一個工廠接口IFactory的實現(xiàn)類,生產(chǎn)B產(chǎn)品,如下:
public class FactoryB implements IFactory {
@Override
public String prodLine() {
return "產(chǎn)品B";
}
}
客戶B中的代碼
public class Client2 {
public static void main(String args[]) {
System.out.println("我訂購并收到產(chǎn)品:"+order());
}
private static String order(){
IFactory factory = new FactoryB();
return factory.prodLine();
}
}
運行后,可以滿足客戶B的需求
我訂購并收到產(chǎn)品:產(chǎn)品B
這樣相當(dāng)于公司為每一位客戶,私人訂制了一個工廠,專門生產(chǎn)其所需要的產(chǎn)品,絲毫不受生產(chǎn)其他產(chǎn)品的影響,而且是保密生產(chǎn),客戶2所需的產(chǎn)品B,對于客戶1是不可見的,絕對的客戶利益至上。
以后再有新的需求,只需要增加工廠接口IFactory的實現(xiàn)類即可。
不需要改變原有的客戶類中的代碼,也不需要改變原有工廠類中的代碼,不會導(dǎo)致工廠類中因生產(chǎn)線方法過多而爆炸式增長變得難以維護和使用,新增的工廠線方法對于新的客戶類都是私人訂制的,避免新增客戶類時,面對一大堆工廠線方法眼花繚亂,原有的客戶類對于新的工廠線方法都是不可見的,可避免原有客戶類更改代碼時調(diào)錯方法的情況發(fā)生。
為什么要使用開閉原則?
1、保持軟件產(chǎn)品的穩(wěn)定性
開閉原則要求我們通過保持原有代碼不變添加新代碼來實現(xiàn)軟件的變化,因為不涉及原代碼的改動,這樣可以避免為實現(xiàn)新功能而改壞線上功能的情況,避免老用戶的流失。
2、不影響原有測試代碼的運行
軟件開發(fā)規(guī)范性好的團隊都會寫單元測試,如果某條單元測試所測試的功能單元發(fā)生了變化,則單元測試代碼也應(yīng)做相應(yīng)的斷言變更,否則就會導(dǎo)致單元測試運行紅條。如果每次軟件的變化,除了變更功能代碼之外,還得變更測試代碼,書寫測試代碼同樣需要消耗工時,這樣在項目中引入單元測試就成了累贅。開閉原則可以讓單元測試充分發(fā)揮作用而又不會成為后期軟件開發(fā)的累贅。
3、使代碼更具模塊化,易于維護
開閉原則可以讓代碼中的各功能,以及新舊功能獨立存在于不同的單元模塊中,一旦某個功能出現(xiàn)問題,可以很快地鎖定代碼位置作出修改,由于模塊間代碼獨立不相互調(diào)用,更改一個功能的代碼也不會引起其他功能的崩潰。
4、提高開發(fā)效率
在代碼開發(fā)中,有時候閱讀前人的代碼是件很頭疼的事,尤其項目開發(fā)周期比較長,可能三五年,再加上公司人員流動性大,原有代碼的開發(fā)人員早就另謀高就,而代碼寫的更是一團糟,自帶混淆,能走彎路不走直路。而現(xiàn)在需要在原有功能的基礎(chǔ)上開發(fā)新功能,如果開閉原則使用得當(dāng)?shù)脑?,我們是不需要看懂原有代碼實現(xiàn)細節(jié)便可以添加新代碼實現(xiàn)新功能(例如示例中,我們不需要知道A產(chǎn)品是怎么生產(chǎn)的,便可以開發(fā)生產(chǎn)B產(chǎn)品的功能),畢竟有時候閱讀一個功能的代碼,比自己重新實現(xiàn)這個功能用的時間還要長。