設計原則是指導我們程式碼設計的一些經驗總結,也就是“心法”;物件導向就是我們的“武器”;設計模式就是“招式”。
以心法為基礎,以武器運用招式應對複雜的程式設計問題。
表妹今天上班又忘記打卡了
表妹:哥啊,我真的是一點記性都沒有
我:發生什麼事啦?
表妹:今天上班又忘記打卡了,又是白打工的一天,做什麼事都提不起勁來。
你看,傳統的上下班打卡制,這種模式將按時上下班作為考核指標之一,雖然強化了企業的管理,但是卻限制了員工的時間自由,每個員工的情況和工作狀態都不同,強制的上班時間容易導致員工為了應付打卡而打卡,實則工作效率卻不高。
按時上下班其實不是老闆希望達到的目的,老闆希望的是,所有員工的績效達標,最終企業能夠盈利,而上下班打卡制只不過是為了達到這一目標的其中一個方法而已。
明確了將績效作為考核指標。那麼,績效至少達標,這個是不可以修改的,在這個基礎上,員工的上下班時間是可以自由安排的,這樣就可以提高員工的生產效率了。這就是彈性上班制,對業績成效的修改關閉,而對時間制度擴充套件的開放。
你看,這不就是我們軟體開發中的開放-封閉原則嘛。
是說軟體實體(類、模組、函式等)應該可以擴充套件,但是不可以修改。
這是一條最難理解和掌握,但是又最有用的設計原則。
之所以說難理解,是因為,“怎樣的程式碼改動才被定義為擴充套件?怎樣的程式碼改動才被定義為修改?怎樣才算滿足開閉原則?修改程式碼就一定違反了開閉原則嗎?”等問題。
之所以說難掌握,是因為,“如何做到對擴充套件開放,修改封閉?,如何在專案中靈活地應用開閉原則,在保證擴充套件性的同時又不影響程式碼的可讀性?”等問題。
之所以說最有用,是因為,擴充套件性是程式碼質量最重要的衡量標準之一。在23種經典設計模式之中,大部分設計模式都是為了解決程式碼的擴充套件性問題而存在的。
如何理解“對擴充套件開放、修改關閉”?
比如,書店銷售圖書。
圖書有三個屬性:書名、價格和作者。IBook是獲取圖書三個屬性的介面,如下所示:
1 public interface IBook { 2 // 圖書的名稱 3 public String getName(); 4 5 // 圖書的售價 6 public int getPrice(); 7 8 // 圖書的作者 9 public String getAuthor(); 10 }
小說類圖書NovelBook是一個具體的實現類,如下所示:
1 public class NovelBook implements IBook { 2 // 圖書的名稱 3 private String name; 4 5 // 圖書的價格 6 private int price; 7 8 // 圖書的作者 9 private String author; 10 11 // 通過建構函式傳遞書籍資料 12 public NovelBook(String _name,int _price,String _author){ 13 this.name = _name; 14 this.price = _price; 15 this.author = _author; 16 } 17 18 // 獲得作者是誰 19 public String getAuthor() { 20 return this.author; 21 } 22 23 // 獲得書名 24 public String getName() { 25 return this.name; 26 } 27 28 // 獲得圖書的價格 29 public int getPrice() { 30 return this.price; 31 } 32 }
接下來,我們看一下,書店是如何銷售圖書的:
1 public class BookStore { 2 private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); 3 4 // 靜態模組初始化,專案中一般是從持久層初始化產生 5 static{ 6 bookList.add(new NovelBook("天龍八部",3200,"金庸")); 7 bookList.add(new NovelBook("巴黎聖母院",5600,"雨果")); 8 bookList.add(new NovelBook("悲慘世界",3500,"雨果")); 9 bookList.add(new NovelBook("平凡的世界",4300,"路遙")); 10 } 11 12 //模擬書店買書 13 public static void main(String[] args) { 14 NumberFormat formatter = NumberFormat.getCurrencyInstance(); 15 formatter.setMaximumFractionDigits(2); 16 System.out.println("------------書店中的小說類圖書記錄如下:---------------------"); 17 for(IBook book:bookList){ 18 System.out.println("書籍名稱:" + book.getName()+"\t書籍作者:" + 19 book.getAuthor()+ "\t書籍價格:" + formatter.format(book.getPrice()/100.0)+"元"); 20 } 21 } 22 }
注:在BookStore中宣告瞭一個靜態模組,實現了資料的初始化,這部分應該是從持久層產生的,由持久層工具進行管理。
執行結果如下:
------------書店中的小說類圖書記錄如下:--------------------- 書籍名稱:天龍八部 書籍作者:金庸 書籍價格:¥32.00元 書籍名稱:巴黎聖母院 書籍作者:雨果 書籍價格:¥56.00元 書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:¥35.00元 書籍名稱:平凡的世界 書籍作者:路遙 書籍價格:¥43.00元
但是,最近書店的小說類圖書銷量下滑很嚴重,所以,書店希望通過打折來刺激消費:所有40元及以上的小說類圖書8折銷售,40元以下的按9折銷售。
對於已經投產的專案來說,這就是一個變化,那麼,我們應該怎麼應對呢?
有三種方法可以解決這個問題:
修改介面
在IBook上新增一個getOffPrice()的方法,專門進行打折處理。
首先,IBook作為介面應該是穩定可靠的,不應該經常發生變化,否則介面作為契約的作用就失去了意義。
其次,修改了介面,NovelBook實現類也要做相應的修改,這樣,為了實現這個需求,改動的面積是比較大的。
修改實現類
修改NovelBook實現類中getPrice()的方法,這樣,改動的面積相對比較小了,僅僅侷限在NovelBook實現類中。但是這樣的話,使用者就無法獲得圖書的原價了。
通過擴充套件實現變化
增加一個子類OffNovelBook,複寫getPrice()方法,高層次的模組(也就是static靜態模組區)通過OffNovelBook類產生新的物件。如下所示:
public class OffNovelBook extends NovelBook { public OffNovelBook(String _name,int _price,String _author){ super(_name,_price,_author); } // 覆寫銷售價格 @Override public int getPrice(){ // 原價 int selfPrice = super.getPrice(); int offPrice=0; if(selfPrice < 4000){ // 原價低於40元,則打9折 offPrice = selfPrice * 90 /100; }else{ offPrice = selfPrice * 80 /100; } return offPrice; } }
你看,僅僅擴充套件一個子類並複寫getPrice()方法,就可以完成新增的業務。接下來看一下BookStore類的修改:
public class BookStore { private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); // 靜態模組初始化,專案中一般是從持久層初始化產生 static{ // 換成打折的小說 bookList.add(new OffNovelBook("天龍八部",3200,"金庸")); bookList.add(new OffNovelBook("巴黎聖母院",5600,"雨果")); bookList.add(new OffNovelBook("悲慘世界",3500,"雨果")); bookList.add(new OffNovelBook("平凡的世界",4300,"路遙")); } // 模擬書店買書 public static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); System.out.println("------------書店中的小說類圖書記錄如下:---------------------"); for(IBook book:bookList){ System.out.println("書籍名稱:" + book.getName()+"\t書籍作者:" + book.getAuthor()+ "\t書籍價格:" + formatter.format(book.getPrice()/100.0)+"元"); } } }
上面只修改了靜態模組初始化部分,其他部分沒有修改。執行結果如下:
------------書店中的小說類圖書記錄如下:--------------------- 書籍名稱:天龍八部 書籍作者:金庸 書籍價格:¥25.60元 書籍名稱:巴黎聖母院 書籍作者:雨果 書籍價格:¥50.40元 書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:¥28.00元 書籍名稱:平凡的世界 書籍作者:路遙 書籍價格:¥38.70元
上面這個例子,通過一處擴充套件,一處修改,實現了打折的新需求。可能有同學就會問:“這不還是修改了程式碼嗎?”
修改程式碼就意味著違反了開閉原則嗎?
BookStore類確實修改了,這部分屬於高層次的模組。在業務規則改變的情況下,高層模組必須有部分改變以適應新業務。新增一個新功能,不可能任何模組、類、方法的程式碼都不“修改”,這個是做不到的。類需要建立、組裝、並且做一些初始化操作,才能構建成可執行的程式,這部分程式碼的修改是在所難免的。
我們要做的是,儘量讓修改操作更集中、更少、更上層,儘量讓最核心、最複雜的那部分邏輯程式碼滿足開閉原則。
如何做到“對擴充套件開放、修改關閉”?
實際上,開閉原則講的就是程式碼的擴充套件性問題,是判斷一段程式碼是否易擴充套件的“金標準”。
在講具體的方法論之前,我們先來看一些更加偏向頂層的指導思想。為了儘量寫出擴充套件性好的程式碼,我們要時刻具備擴充套件意識、抽象意識、封裝意識。這些“潛意識”可能比任何開發技巧都重要。
擴充套件意識:在寫程式碼的時候,我們要多花點時間往前多思考一下,這段程式碼未來可能有哪些需求變更、如何設計程式碼結構,事先留好擴充套件點,以便在未來需求變更的時候,不需要改動程式碼整體結構、做到最小程式碼改動的情況下,新的程式碼能夠靈活地插入到擴充套件點上,做到“對擴充套件開放、對修改關閉”。
抽象意識:提供抽象化的不可變介面,給上層系統使用。當具體的實現發生變化的時候,我們只需要基於相同的抽象介面,擴充套件一個新的實現,替換老的實現即可,上游系統的程式碼幾乎不需要修改。
封裝意識:在識別出程式碼可變部分和不可變部分之後,我們要將可變部分封裝起來,隔離變化。
在眾多的設計原則、思想、模式中,最常用來提高程式碼擴充套件性的方法有:多型、依賴注入、基於介面而非實現程式設計,以及大部分的設計模式(比如:裝飾、策略、模板、責任鏈、狀態等)。
設計模式這一塊,我們另外再分享。今天重點學習一下,如何利用多型、依賴注入、基於介面而非實現程式設計,來實現“對擴充套件開放、對修改關閉”。
假如,我們現在要開發一個通過Kafka來傳送非同步訊息。對於這樣一個功能的開發,我們要學會將其抽象成一組跟具體訊息佇列(Kafka)無關的非同步訊息介面。所有上層系統都依賴這組抽象的介面程式設計,並且通過依賴注入的方式來呼叫。當我們要替換新的訊息佇列的時候,比如將Kafka替換成RocketMQ,可以很方便地拔掉老的訊息佇列實現,插入新的訊息佇列實現。
// 這一部分體現了抽象意識 public interface MessageQueue { //...} public class KafkaMessageQueue implements MessageQueue { //...} public class RocketMQMessageQueue implements MessageQueue { //...} public interface MessageFromatter { //...} public class JsonMessageFromatter implements MessageFromatter { //...} public class ProtoBufMessageFromatter implements MessageFromatter { //...} public class Demo { private MessageQueue msgQueue; // 基於介面而非實現程式設計 public Demo(MessageQueue msgQueue) { // 依賴注入 this.msgQueue = msgQueue; } // msgFormatter:多型、依賴注入 public void sendNotification(Notification notification, MessageFormatter msg) { //.. } }
當然,開閉原則也不是免費的,有時候,程式碼的擴充套件性會跟可讀性衝突。這個時候,我們就需要在兩者之間做一個權衡。總之,沒有一個放之四海而皆準的參考標準,全憑實際的應用場景來決定。
如何預留擴充套件點?
前面我們提到,寫出支援“對擴充套件開放、對修改關閉”的程式碼的關鍵是預留擴充套件點,那麼問題是,應該如何才能識別出所有可能的擴充套件點呢?
如果開發業務導向的系統,比如電商系統、物流系統、金融系統等,要想識別儘可能多的擴充套件點,就需要對業務本身有足夠多的瞭解。
如果開發通用、偏底層的框架、類庫、元件等,就需要了解它們會被如何使用,日後可能會新增什麼功能。
“唯一不變的就是變化本身”,儘管我們對業務系統、框架功能有足夠多的瞭解,也不能識別出所有的擴充套件點。即便我們能夠識別出所有的擴充套件點,為這些地方做預留擴充套件點的設計,成本都是很大的,這就叫做“過度設計”。
合理的做法,應該是對於一些比較確定的,短期內可能就會擴充套件,或者需要改動對程式碼結構影響比較大的情況,或者實現成本不高的擴充套件點,在編寫程式碼的時候,我們就可以事先做預留擴充套件點設計。但是對於一些不確定未來是否要支援的需求,或者實現起來比較複雜的擴充套件點,可以通過重構程式碼的方式來支援擴充套件的需求。
好啦,設計原則是否應用得當,應該根據具體的業務場景,具體分析。
總結
對擴充套件開放,是為了應付變化(需求);
對修改封閉,是為了保證已有程式碼的穩定性;
最終結果是為了讓系統更有彈性!
參考
《大話設計模式》
極客時間專欄《設計模式之美》
https://blog.csdn.net/sinat_20645961/article/details/48239347