設計原則是指導我們程式碼設計的一些經驗總結,也就是“心法”;物件導向就是我們的“武器”;設計模式就是“招式”。
以心法為基礎,以武器運用招式應對複雜的程式設計問題。
實習生表妹上班又闖禍了
表妹:今天上班又闖禍了?
我:發生什麼事情啦?
表妹:我不小心改了後端介面名的大小寫,前端頁面報錯了
你看,這不就類似我們軟體開發中的裡式替換原則嘛。
子類物件能夠替換程式中父類物件出現的任何地方,並且保證原來程式的邏輯行為不變及正確性不被破壞。
如何理解“裡式替換原則”?
實際上,裡式替換原則還有一個更加能落地、更有指導意義的描述,那就是“按照協議來設計”。
子類在設計的時候,要遵守父類的行為約定(或者叫協議)。父類定義了函式的行為約定,那子類可以改變函式內部實現邏輯(重寫),但不能改變函式原有的行為約定。
這裡的行為約定包括:函式宣告要實現的功能;對輸入、輸出、異常的約定;甚至包含註釋中所羅列的任何特殊說明。
前後端協商好的介面文件,就相當於“協議”。前端和後端都分別按照這個協議獨立開發,具體的實現邏輯,是遞迴、動態規劃還是貪心,由開發者決定。
實際上,定義中父類和子類之間的關係,也可以替換成介面和實現類之間的關係。
比如,父類Transporter使用org.apache.http庫中的HttpClient類傳輸網路資料。子類SecurityTransporter繼承父類Transporter,增加了額外的功能,支援傳輸appID和appToken安全認證資訊。
1 public class Transporter { 2 private HttpClient httpClient; 3 4 public Transporter(HttpClient httpClient) { 5 this.httpClient = httpClient; 6 } 7 8 public Response sendRequest(Request request) { 9 // ...use httpClient to send request 10 } 11 } 12 13 public class SecurityTransporter extends Transporter { 14 private String appID; 15 private String appToken; 16 17 public SecurityTransporter(HttpClient httpClient, String appID, String appToken) { 18 super(httpClient); 19 this.appID = appID; 20 this.appToken = appToken; 21 } 22 23 @Override 24 public Response sendRequest(Request request) { 25 if (StringUtils.isNotBlank(appID) && StringUtils.isNotBlank(appToken)) { 26 request.addPayload("app-id", appID); 27 request.addPayload("app-token", appToken); 28 } 29 return super.sendRequest(request); 30 } 31 } 32 33 public class Demo { 34 public void demoFunction(Transporter transporter) { 35 Request request = new Request(); 36 // ...省略設定request中資料值的程式碼... 37 Response response = transporter.sendRequest(request); 38 // ...省略其他邏輯... 39 } 40 } 41 42 // 裡式替換原則 43 Demo demo = new Demo(); 44 demo.demoFunction(new SecurityTransporter(/*省略引數*/););
在上面的程式碼中,子類SecurityTransporter的設計完全符合裡式替換原則,可以替換父類出現的任何位置,並且原來程式碼的邏輯行為(傳輸網路資料)不變且正確性也沒有被破壞。
你可能會問,上面的程式碼設計,不就是簡單利用了物件導向的多型特性嗎?
“裡式替換原則”就是多型嗎?
裡式替換原則,是實現開閉原則的重要方式之一,由於使用父類物件的地方可以使用子類物件,因此,在程式中儘量使用父類型別來對物件進行定義,而在執行時再確定其子類型別,用子類物件來替換父類物件。
那麼,“裡式替換原則”就是多型嗎?
還是剛才那個例子,不過需要對SecurityTransporter類中sendRequest()函式稍加改造一下。改造前,我們不校驗appID或者appToken是否設定;改造後,如果appID和appToken沒有設定,則直接丟擲NoAuthorizationRuntimeException未授權異常。改造前後的程式碼對比如下:
1 // 改造前: 2 public class SecurityTransporter extends Transporter { 3 // ...省略其他程式碼... 4 @override 5 public Response sendRequest(Request request) { 6 if (StringUtils.isNotBlank(appID) && StringUtils.isNotBlank(appToken)) { 7 request.addPayload("app-id", appID); 8 request.addPayload("app-token", appToken); 9 } 10 return super.sendRequest(request); 11 } 12 } 13 14 // 改造後: 15 public class SecurityTransporter extends Transporter { 16 // ...省略其他程式碼... 17 @override 18 public Response sendRequest(Request request) { 19 if (StringUtils.isBlank(appID) || StringUtils.isBlank(appToken)) { 20 throw new NoAuthorizationRuntimeException(...); 21 } 22 request.addPayload("add-id", appID); 23 request.addPayload("app-token", appToken); 24 return super.sendRequest(request); 25 } 26 }
你看,使用改造後的程式碼後,如果傳進demoFunction()函式的是父類Transporter物件,那demoFunction()函式並不會有異常丟擲,但如果傳遞給demoFunction()函式的是子類SecurityTransporter物件,那demoFunction()就有可能有異常丟擲。
儘管程式碼中丟擲的是執行時異常,我們可以不在程式碼中顯式地捕獲處理,但子類替換父類傳遞進demoFunction函式之後,整個程式的邏輯行為就發生了改變。
雖然從定義描述和程式碼實現上看,多型和裡式替換有點類似,但是它們關注的角度是不一樣的。
多型是物件導向程式設計的一大特性,也是物件導向程式語言的一種語法,是一種程式碼實現的思路。
而裡式替換是一種設計原則,是用來指導繼承關係中,子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程式的邏輯以及不破壞原有程式的正確性。
哪些程式碼明顯違背了“裡式替換原則”?
-
子類違背父類宣告要實現的功能
父類中提供的sortOrderByAmount()訂單排序函式,是按照金額從小到大來給訂單排序的,而子類重寫這個sortOrderByAmount()訂單排序函式之後,是按照建立日期來給訂單排序的。
那麼,這個子類的設計就違背了裡式替換原則。
-
子類違背父類對輸入、輸出、異常的約定
在父類中,某個函式約定:執行出錯的時候返回null;獲取資料為空的時候返回空集合。而子類過載函式之後,實現變了,執行出錯返回異常,獲取不到資料返回null。
那麼,這個子類的設計就違背了裡式替換原則。
-
子類違背父類註釋中所羅列的任何特殊說明
父類中定義的withdraw()提現函式的註釋是這麼寫的:“使用者的提現金額不得超過賬戶餘額...”,而子類重寫withdraw()函式之後,針對VIP賬號實現了透支提現的功能,也就是提現金額可以大於賬戶餘額。
那麼,這個子類的設計就違背了裡式替換原則。
實際上,你發現沒有,裡式替換原則是非常寬鬆的。判斷子類的設計實現是否違背了裡式替換原則,可以拿父類的單元測試去驗證子類的程式碼。
如果某些單元測試執行失敗,就有可能說明,子類的設計實現沒有完全遵守父類的約定,子類有可能違背了裡式替換原則。
總結
裡式替換原則就是子類完美繼承父類的設計初衷,並做了增強(增加自己特有的方法)。
大白話就是,可以青出於藍勝於藍,但是祖傳的東西不能變。
好啦,每個設計原則是否應用得當,應該根據具體的業務場景,具體分析。
參考
極客時間專欄《設計模式之美》