大白話式聊聊設計模式中的開閉原則
一、前言
在面向?qū)ο蟮能浖O計中,只有盡量降低各個模塊之間的耦合度,才能提高代碼的復用率,系統(tǒng)的可維護性、可擴展性才能提高。面向?qū)ο蟮能浖O計中,有23種經(jīng)典的設計模式,是一套前人代碼設計經(jīng)驗的總結(jié),如果把設計模式比作武功招式,那么設計原則就好比是內(nèi)功心法。常用的設計原則有七個,具體如下:
- 單一職責原則:專注降低類的復雜度,實現(xiàn)類要職責單一;
- 開放關閉原則:所有面向?qū)ο笤瓌t的核心,設計要對擴展開發(fā),對修改關閉;
- 里式替換原則:實現(xiàn)開放關閉原則的重要方式之一,設計不要破壞繼承關系;
- 依賴倒置原則:系統(tǒng)抽象化的具體實現(xiàn),要求面向接口編程,是面向?qū)ο笤O計的主要實現(xiàn)機制之一;
- 接口隔離原則:要求接口的方法盡量少,接口盡量細化;
- 迪米特法則:降低系統(tǒng)的耦合度,使一個模塊的修改盡量少的影響其他模塊,擴展會相對容易;
- 組合復用原則:在軟件設計中,盡量使用組合/聚合而不是繼承達到代碼復用的目的。
這些設計原則并不說我們一定要遵循他們來進行設計,而是根據(jù)我們的實際情況去怎么去選擇使用他們,來讓我們的程序做的更加的完善。
今天我們就來聊聊其中的單一職責原則和開閉原則實現(xiàn)理念,歡迎大家在評論區(qū)留言,一起探討。
二、單一職責原則
單一職責原則,簡單的說:就一個類而言,應該僅有一個引起它變化的原因,通俗的說,就是一個類只負責一項職責。
此原則的核心就是解耦和增強內(nèi)聚性
那么為什么要使用單一職責原則:
如果一個類承擔的職責過多,就等于把這些職責耦合在一起,一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。這種耦合會導致脆弱的設計。
這也是他的優(yōu)點,我們總結(jié)一下:
- 降低類的復雜度;
- 提高類的可讀性,提高系統(tǒng)的可維護性;
- 降低變更引起的風險(降低對其他功能的影響)。
我們來舉一些簡單的例子來說明一下這個單一職責原則
//我們用動物生活來做測試
class Animal{
public void breathe(String animal){
System.out.println(animal+"生活在陸地上");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("羊");
animal.breathe("牛");
animal.breathe("豬");
}
}
運行結(jié)果:
羊生活在陸地上
牛生活在陸地上
豬生活在陸地上
但是問題來了,動物并不是都生活在陸地上的,魚就是生活在水中的,修改時如果遵循單一職責原則,需要將 Animal 類細分為陸生動物類 Terrestrial,水生動物 Aquatic,代碼如下:
class Terrestrial{
public void breathe(String animal){
System.out.println(animal+"生活在陸地上");
}
}
class Aquatic{
public void breathe(String animal){
System.out.println(animal+"生活在水里");
}
}
public class Client{
public static void main(String[] args){
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe("羊");
terrestrial.breathe("牛");
terrestrial.breathe("豬");
Aquatic aquatic = new Aquatic();
aquatic.breathe("魚");
}
}
運行結(jié)果:
羊生活在陸地上
牛生活在陸地上
豬生活在陸地上
魚生活在水里
但是問題來了如果這樣修改花銷是很大的,除了將原來的類分解之外,還需要修改客戶端。而直接修改類 Animal 來達成目的雖然違背了單一職責原則,但花銷卻小的多,代碼如下:
class Animal{
public void breathe(String animal){
if("魚".equals(animal)){
System.out.println(animal+"生活在水中");
}else{
System.out.println(animal+"生活在陸地上");
}
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("羊");
animal.breathe("牛");
animal.breathe("豬");
animal.breathe("魚");
}
}
可以看到,這種修改方式要簡單的多。但是卻存在著隱患:有一天需要將魚分為生活在淡水中的魚和生活在海水的魚,則又需要修改 Animal 類的 breathe 方法,而對原有代碼的修改會對調(diào)用“豬”“?!薄把颉钡认嚓P功能帶來風險,也許某一天你會發(fā)現(xiàn)程序運行的結(jié)果變?yōu)椤芭I钤谒小绷恕?/p>
這種修改方式直接在代碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患卻是最大的。還有一種修改方式:
class Animal{
public void breathe(String animal){
System.out.println(animal+"生活在陸地上");
}
public void breathe2(String animal){
System.out.println(animal+"生活在水中");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
animal.breathe2("魚");
}
}
可以看到,這種修改方式?jīng)]有改動原來的方法,而是在類中新加了一個方法,這樣雖然也違背了單一職責原則,但在方法級別上卻是符合單一職責原則的,因為它并沒有動原來方法的代碼。
這三種方式各有優(yōu)缺點,那么在實際編程中,采用哪一中呢?其實這真的比較難說,需要根據(jù)實際情況來確定。我的原則是:只有邏輯足夠簡單,才可以在代碼級別上違反單一職責原則;只有類中方法數(shù)量足夠少,才可以在方法級別上違反單一職責原則;
例如本文所舉的這個例子,它太簡單了,它只有一個方法,所以,無論是在代碼級別上違反單一職責原則,還是在方法級別上違反,都不會造成太大的影響。實際應用中的類都要復雜的多,一旦發(fā)生職責擴散而需要修改類時,除非這個類本身非常簡單,否則還是遵循單一職責原則的好。
以上就是我所說的單一職責原則了,很多書中介紹的說它并不屬于面向?qū)ο笤O計原則中的一種,但是我認為它是,所以我就把他解釋出來了。
三、開放關閉原則
開放關閉原則又稱為開放封閉原則。
簡單的說:一個軟件實體應該對擴展開放,對修改關閉,這個原則也是說,在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展,換句話說,應當可以在不必修改源碼的情況下改變這個模塊的行為。
這句話其實剛開始看上去是有些矛盾的,接下來在我后邊文章解釋里面,我會把他解釋清楚一點。
我們先用個比較好玩的例子來說一下。
西游記大家都看過,玉帝招安孫悟空的時候的橋段,大家還有沒有印象?
當年大鬧天宮的時候美猴王對玉皇大帝做了個挑戰(zhàn),美猴王說:皇帝輪流做,明年到我家,只叫他搬出去,將天宮讓給我,對于這個挑戰(zhàn),太白金星給玉皇大帝提了個意見,我們把它招上來,給他個小官做,他不就不鬧事了?
換一句話說,不用興師動眾的,不破壞天庭的規(guī)矩這就是閉,但是收他為官,便是開,招安的方法便是符合開閉原則的,給他個‘弼馬溫’的官,便可以讓這個系統(tǒng)正常不受威脅,是吧,何樂而不為?我給大家畫個圖。
圖片
招安的方法的關鍵就是不允許更改現(xiàn)有的天庭秩序,但是允許將妖猴納入現(xiàn)有的秩序中,從而擴展了這個秩序,用面向?qū)ο蟮恼Z言來說:不允許更改的是系統(tǒng)的抽象層,而允許擴展的是系統(tǒng)的實現(xiàn)層。
我們寫一些簡單的代碼來進行一個完整的展示來進行一下理解。
//書店賣書
interface Books{
//書籍名稱
public Sting getName();
//書籍價格
public int getPrice();
//書籍作者
public String getAuthor();
}
public class NovelBook implements Books {
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;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getAuthor() {
return author;
}
}
以上的代碼是數(shù)據(jù)的實現(xiàn)類和書籍的類別;
下面我們將要開始對書籍進行一個售賣活動:
public class BookStore {
private final static ArrayList<Books> sBookList = new ArrayList<Books>();
static {
sBookList.add(new NovelBook("天龍八部", 4400, "金庸"));
sBookList.add(new NovelBook("射雕英雄傳", 7600, "金庸"));
sBookList.add(new NovelBook("鋼鐵是怎么煉成的", 7500, "保爾·柯查金"));
sBookList.add(new NovelBook("紅樓夢", 3300, "曹雪芹"));
}
public static void main(String[] args) throws IOException {
NumberFormat format = NumberFormat.getCurrencyInstance();
format.setMaximumFractionDigits(2);
System.out.println("----書店賣出去的書籍記錄如下---");
for (Books book : sBookList) {
System.out.println("書籍名稱:" + book.getName()
+ "\t書籍作者:" + book.getAuthor()
+ "\t書籍價格:" + format.format(book.getPrice() / 100.00) + "元");
}
}
}
運行結(jié)果如下:
D:\develop\JDK8\jdk1.8.0_181\bin\java.exe "-javaagent:D:\develop\IDEA\IntelliJ IDEA 2018.2.4\lib\idea_rt.jar=62787:D:\develop\IDEA\IntelliJ IDEA 2018.2.4\bin" -Dfile.encoding=UTF-8 -classpath D:\develop\JDK8\jdk1.8.0_181\jre\lib\charsets.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\deploy.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\dnsns.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\jaccess.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\localedata.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\nashorn.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunec.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\zipfs.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\javaws.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jce.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jfr.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jfxswt.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jsse.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\management-agent.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\plugin.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\resources.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\rt.jar;D:\develop\IDEA_Workspace\CloudCode\out\production\PattemMoudle com.yldyyn.test.BookStore
----書店賣出去的書籍記錄如下---
書籍名稱:天龍八部 書籍作者:金庸 書籍價格:¥44.00元
書籍名稱:射雕英雄傳 書籍作者:金庸 書籍價格:¥76.00元
書籍名稱:鋼鐵是怎么煉成的 書籍作者:保爾·柯查金 書籍價格:¥75.00元
書籍名稱:紅樓夢 書籍作者:曹雪芹 書籍價格:¥33.00元
Process finished with exit code 0
但是如果說現(xiàn)在書店賣書的時候要求打折出售,40以上的我們要7折售賣,40以下的我們打8折。
方法有三種,第一個辦法:修改接口。在 Books 上新增加一個方法 getOnSalePrice(),專門進行打折,所有實現(xiàn)類實現(xiàn)這個方法。但是這樣修改的后果就是實現(xiàn)類 NovelBook 要修改, BookStore 中的 main 方法也修改,同時 Books 作為接口應該是穩(wěn)定且可靠的,不應該經(jīng)常發(fā)生變化,否則接口做為契約的作用就失去了效能,其他不想打折的書籍也會因為實現(xiàn)了書籍的接口必須打折,因此該方案被否定。
第二個辦法:修改實現(xiàn)類。修改 NovelBook 類中的方法,直接在 getPrice() 中實現(xiàn)打折處理,這個應該是大家在項目中經(jīng)常使用的就是這樣辦法,通過 class 文件替換的方式可以完成部分業(yè)務(或是缺陷修復)變化,但是該方法還是有缺陷的,例如采購書籍人員也是要看價格的,由于該方法已經(jīng)實現(xiàn)了打折處理價格,因此采購人員看到的也是打折后的價格,這就產(chǎn)生了信息的蒙蔽效果,導致信息不對稱而出現(xiàn)決策失誤的情況。該方案也不是一個最優(yōu)的方案。
第三個辦法,通過擴展實現(xiàn)變化增加一個子類 OffNovelBook,覆寫 getPrice 方法,高層次的模塊(也就是 static 靜態(tài)模塊區(qū))通過 OffNovelBook 類產(chǎn)生新的對象,完成對業(yè)務變化開發(fā)任務。好辦法,風險也小。
public class OnSaleBook extends NovelBook {
public OnSaleBook(String name, int price, String author) {
super(name, price, author);
}
@Override
public String getName() {
return super.getName();
}
@Override
public int getPrice() {
int OnsalePrice = super.getPrice();
int salePrce = 0;
if (OnsalePrice >4000){
salePrce = OnsalePrice * 70/100;
}else{
salePrce = OnsalePrice * 80/100;
}
return salePrce;
}
@Override
public String getAuthor() {
return super.getAuthor();
}
}
上面的代碼是擴展出來的一個類,而不是在原來的類中進行的修改。
public class BookStore {
private final static ArrayList<Books> sBookList = new ArrayList<Books>();
static {
sBookList.add(new OnSaleBook("天龍八部", 4400, "金庸"));
sBookList.add(new OnSaleBook("射雕英雄傳", 7600, "金庸"));
sBookList.add(new OnSaleBook("鋼鐵是怎么煉成的", 7500, "保爾·柯查金"));
sBookList.add(new OnSaleBook("紅樓夢", 3300, "曹雪芹"));
}
public static void main(String[] args) throws IOException {
NumberFormat format = NumberFormat.getCurrencyInstance();
format.setMaximumFractionDigits(2);
System.out.println("----書店賣出去的書籍記錄如下---");
for (Books book : sBookList) {
System.out.println("書籍名稱:" + book.getName()
+ "\t書籍作者:" + book.getAuthor()
+ "\t書籍價格:" + format.format(book.getPrice() / 100.00) + "元");
}
}
}
結(jié)果展示:
----書店賣出去的書籍記錄如下---
書籍名稱:天龍八部 書籍作者:金庸 書籍價格:¥30.80元
書籍名稱:射雕英雄傳 書籍作者:金庸 書籍價格:¥53.20元
書籍名稱:鋼鐵是怎么煉成的 書籍作者:保爾·柯查金 書籍價格:¥52.50元
書籍名稱:紅樓夢 書籍作者:曹雪芹 書籍價格:¥26.40元
Process finished with exit code 0
在開閉原則中,抽象化是一個關鍵,解決問題的關鍵在于抽象化,在 JAVA 語言這種面向?qū)ο蟮恼Z言中,可以給系統(tǒng)定義出一個一勞永逸的,不再更改的抽象化的設計,此設計允許擁有無窮無盡的實現(xiàn)層被實現(xiàn)。
在 JAVA 語言中,可以給出一個或者多個抽象的 JAVA 類或者是JAVA接口,規(guī)定所有的具體類必須提供方法特征作為系統(tǒng)設計的抽象層,這個抽象層會遇見所有的可能出現(xiàn)的擴展,因此,在任何擴展情況下都不回去改變,這就讓系統(tǒng)的抽象層不需要修改,從而滿足開閉原則的第二條,對修改進行閉合。
同時,從抽象層里面導出一個或者多個新的具體類可以改變系統(tǒng)的行為,這樣就滿足了開閉原則的第一條。
盡管很多時候我們無法百分百的做到開閉原則,但是如果向著這個方向去努力,就能夠有部分的成功,這也是可以改善系統(tǒng)的結(jié)構(gòu)的。