這篇是軟件設(shè)計原則系列文章的第一篇钩蚊,之前寫過一篇博客里面介紹了七種設(shè)計原則,但是將七種原則容納到一篇文章之中總感覺哪里不對:說的太多文章就會變得冗長影響閱讀體驗言疗,說的太少總感覺有的話沒說完就進(jìn)行下一項了冰木,于是乎本人突發(fā)奇想為何不分開來寫妻熊?更于是乎就有了“軟件設(shè)計原則系列文章”饼丘,本文是個開篇趁桃。廢話不多說了,Let's come to the point!
軟件設(shè)計一共有七大原則镇辉,在這七大原則之上還有一個總體原則屡穗,也就是我們常說的“高內(nèi)聚贴捡,低耦合”忽肛。它們之間就好比白雪公主和七個小矮人的關(guān)系,那它們叫什么名字呢烂斋?讓我們先來認(rèn)識一下屹逛,它們分別是:開閉原則(OCP)、依賴倒置原則(DIP)汛骂、單一職責(zé)原則(SRP)罕模、接口隔離原則(ISP)、迪米特原則(LoD)帘瞭、里氏替換原則(LSP)和合成復(fù)用原則(CARP)淑掌。今天的主角是這七個矮人中的姚明,它就是開閉原則(OCP蝶念,Open Close Principle)抛腕。
門一開一閉,眼一閉一睜媒殉,這也算個原則担敌?
沒錯,但這里的開閉不是指門廷蓉,而是對擴(kuò)展開放對修改關(guān)閉全封,用擴(kuò)展而不是修改來適應(yīng)需求的變化。有句話說的好——唯一不變的是變化本身桃犬,應(yīng)用到軟件界就是需求是一定會變化的刹悴,即使原來的不變新的東西也會加進(jìn)來,如果你想一勞永逸攒暇,開發(fā)的東西永遠(yuǎn)不會變土匀,那只有一種可能——這個系統(tǒng)已經(jīng)沒人在用了。怎么證明一個軟件或者系統(tǒng)還活著扯饶?我們來看一下身邊的例子:Win10是不是會不定時地更新升級恒削?Android和IOS等常用的手機(jī)系統(tǒng)是不是會經(jīng)常提示你升級?我們經(jīng)常玩兒的游戲是不是會經(jīng)常打補(bǔ)丁……那什么樣的代碼不需要維護(hù)了呢尾序?在我上大學(xué)的時候最火的手機(jī)品牌之一——諾基亞钓丰,當(dāng)時它安裝的系統(tǒng)塞班現(xiàn)在已經(jīng)被淘汰,這個代碼已經(jīng)不需要維護(hù)了(壞了每币,暴露年齡了)⌒。現(xiàn)在是2020年的3月份,就在兩個月前的1月14日,微軟官方宣布Win 7操作系統(tǒng)退役梦鉴,從此陪伴了大家10年之久的Windows 7正式退出歷史舞臺李茫,它的代碼也不需要維護(hù)了,更早的像什么Windows XP啊肥橙、Windows 98啊就更不用說了(再一次暴露年齡??)魄宏,估計再過十幾年的學(xué)生都不一定聽說過這些操作系統(tǒng)。說了這么多存筏,其實想表達(dá)的就一點:既然變化是一定要發(fā)生的宠互,那我們應(yīng)該應(yīng)用什么策略以及采取什么措施來應(yīng)對變化,我覺得這是所有軟件設(shè)計者包括開發(fā)者應(yīng)該考慮的問題椭坚。哈哈予跌,值得慶幸的是早已有前輩為我們考慮好了這些事情,他們總結(jié)的經(jīng)驗經(jīng)過了大量工程實踐的考驗并被公認(rèn)為行之有效的方法善茎,從而逐漸形成原則沉淀下來為后人所用券册,這就是軟件設(shè)計的原則。
我們在討論本篇文章的主角——OCP之前再來廢一些話垂涯,那就是如果不遵守這七大原則能不能行烁焙?就問行!不集币!行考阱!答案是可以的,沒問題鞠苟,代碼照樣能運行乞榨!那你還扯個哩個兒愣~~散了,散了当娱,都散了吃既,各回各家吧,博主現(xiàn)場翻車跨细!~~嘖嘖嘖……我是不是在這扯皮一會兒就知道了鹦倚,下面用實際的代碼來感受一下用OCP和不用OCP的差別。
我們假設(shè)一個場景:小明買車冀惭,今天小明買了一輛奔馳震叙,開著奔馳去兜風(fēng)。
public class Benz {
public void run() {
System.out.println("Benz run...");
}
}
public class Person {
private String name;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void driveACar(Benz benz) {
System.out.println(getName());
benz.run();
}
}
public class OCP {
public static void main(String[] args) {
Person xiaoMing = new Person("小明");
Benz benz = new Benz();
xiaoMing.driveACar(benz);
}
}
main方法模擬的是客戶端程序散休,Person模擬的是服務(wù)端程序媒楼,這樣看起來沒什么問題,確實也沒什么問題戚丸。然后需求變了:小明換車了划址,把奔馳換成了寶馬,然后開著寶馬去兜風(fēng)。你會說那還不簡單夺颤,改一下不就行了嗎痢缎?
public class BMW {// 增加一個寶馬類
public void run() {
System.out.println("BMW run...");
}
}
public class Person {
private String name;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void driveACar(Benz benz) {
System.out.println(getName());
benz.run();
}
public void driveACar(BMW bmw) {// 增加開寶馬的方法
System.out.println(getName());
bmw.run();
}
}
public class OCP {
public static void main(String[] args) {
Person xiaoMing = new Person("小明");
// Benz benz = new Benz();
// xiaoMing.driveACar(benz);
BMW bmw = new BMW();// 修改客戶端代碼:注釋掉原有調(diào)用開奔馳的方法,新增調(diào)用開寶馬的方法
xiaoMing.driveACar(bmw);
}
}
我們來看一下這一次的需求改動需要做什么工作:首先服務(wù)端代碼Person要增加一個開寶馬的方法世澜,因為原有開奔馳的方法不能傳入寶馬對象所以只能新增一個独旷;其次,客戶端也要改變調(diào)用的代碼宜狐,因為原先調(diào)用開奔馳的方法已不滿足新的需求势告。所以,Look一下抚恒,一個小小的需求改動會牽扯到從服務(wù)端到客戶端整體的改造,所謂牽一發(fā)而動全身络拌。也許你又會問:這樣不行嗎俭驮?在本例中我只是用很簡單的代碼來舉栗子,實際項目里面代碼可能會很復(fù)雜春贸,所以需求變化可能會改很多代碼混萝,工作量比較大;另一方面萍恕,改造原有代碼意味著引入風(fēng)險逸嘀,因為原有代碼可能是經(jīng)過了很長時間的“磨合”,功能已經(jīng)非常穩(wěn)定了允粤,你去改造它能保證百分百不引入bug嗎崭倘?很有可能你改造A功能的代碼,但是卻無意間影響了原有的B功能类垫,導(dǎo)致原有B功能不可用或出現(xiàn)問題司光。所以,面對新的需求我們不嚴(yán)格要求絕對不能改動原有代碼悉患,但我們應(yīng)該做到的是盡可能少的改動残家。拿本例來說,如果小明再換一輛奧迪呢售躁?是不是Person又得加一個開奧迪的方法坞淮?如果我們將這些具體品牌的車抽象出一個更高層的概念——汽車類,那么你需要開什么車就傳入子類(或?qū)崿F(xiàn)類)不就可以了嗎陪捷?下面我們就按照這一思路來對上述代碼做改造:
public interface ICar {
/**
* 將具體的車抽象出來形成一個更高層的概念回窘,可以是一個接口或者抽象類,
* 它里面有一個run方法揩局,所有具體品牌的車都繼承或者實現(xiàn)這個更高層的類毫玖。
*/
void run();
}
public class Benz implements ICar {
public void run() {
System.out.println("Benz run...");
}
}
public class BMW implements ICar{
public void run() {
System.out.println("BMW run...");
}
}
public class Person {
private String name;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void driveACar(ICar car) {
System.out.println(getName());
car.run();
}
}
public class OCP {
private static ICar car;
static {
// 模擬Spring注入ICar實例,需要不同品牌的車就注入不同品牌的實例即可
car = new Benz();
}
public static void main(String[] args) {
Person person = new Person("小明");
person.driveACar(car);
}
}
這樣服務(wù)提供端Person就提供一個driveACar方法即可,入?yún)⑹且粋€接口付枫,所有具體品牌的車都實現(xiàn)ICar接口烹玉,這樣需要什么車就傳入相關(guān)的實現(xiàn)類即可,在客戶端需要什么車就注入一個汽車實例即可阐滩,所以面對小明不斷地?fù)Q車二打,只需要擴(kuò)展ICar的實現(xiàn)類即可,客戶端(main方法)和服務(wù)提供方(Person)都不需要改代碼掂榔,這就是“對擴(kuò)展開放继效,對修改關(guān)閉”,其實面向抽象(接口)編程就是這個道理装获。如果再放開一點瑞信,可以把Person也抽象成接口,這樣不僅小明可以開奔馳穴豫,張三凡简、李四都可以開,需要不同的人開車就在客戶端注入不同個人的實例精肃,這一點就不在這里用代碼演示了秤涩。
所以,到這里讀者應(yīng)該明白什么是OCP了以及遵循OCP原則會帶來哪些好處司抱,本文也接近尾聲了筐眷。最后說一下為什么在文章開頭處我用“白雪公主和七個小矮人”來做比喻,目的是為了讓讀者記住“7”這個數(shù)字习柠,可能你看完這個系列文章之后過一段時間會忘記一共有幾大原則匀谣,今后哪天如果被面試官問到只要你腦海記住有七個小矮人就可以了。