設計原則之【裡式替換原則】

Gopher大威發表於2022-02-28

設計原則是指導我們程式碼設計的一些經驗總結,也就是“心法”;物件導向就是我們的“武器”;設計模式就是“招式”。

以心法為基礎,以武器運用招式應對複雜的程式設計問題。

實習生表妹上班又闖禍了

表妹:今天上班又闖禍了?

我:發生什麼事情啦?

表妹:我不小心改了後端介面名的大小寫,前端頁面報錯了


你看,這不就類似我們軟體開發中的裡式替換原則嘛。

子類物件能夠替換程式中父類物件出現的任何地方,並且保證原來程式的邏輯行為不變及正確性不被破壞。

如何理解“裡式替換原則”?

實際上,裡式替換原則還有一個更加能落地、更有指導意義的描述,那就是“按照協議來設計”。

子類在設計的時候,要遵守父類的行為約定(或者叫協議)。父類定義了函式的行為約定,那子類可以改變函式內部實現邏輯(重寫),但不能改變函式原有的行為約定。

這裡的行為約定包括:函式宣告要實現的功能對輸入、輸出、異常的約定;甚至包含註釋中所羅列的任何特殊說明

前後端協商好的介面文件,就相當於“協議”。前端和後端都分別按照這個協議獨立開發,具體的實現邏輯,是遞迴、動態規劃還是貪心,由開發者決定。

實際上,定義中父類和子類之間的關係,也可以替換成介面和實現類之間的關係。

比如,父類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賬號實現了透支提現的功能,也就是提現金額可以大於賬戶餘額。

那麼,這個子類的設計就違背了裡式替換原則。

實際上,你發現沒有,裡式替換原則是非常寬鬆的。判斷子類的設計實現是否違背了裡式替換原則,可以拿父類的單元測試去驗證子類的程式碼。

如果某些單元測試執行失敗,就有可能說明,子類的設計實現沒有完全遵守父類的約定,子類有可能違背了裡式替換原則。

總結

裡式替換原則就是子類完美繼承父類的設計初衷,並做了增強(增加自己特有的方法)。

大白話就是,可以青出於藍勝於藍,但是祖傳的東西不能變。

好啦,每個設計原則是否應用得當,應該根據具體的業務場景,具體分析。

參考

極客時間專欄《設計模式之美》

相關文章