摘要:複雜的條件邏輯是程式設計中最難理解的東西之一。
本文分享自華為雲社群《簡單易懂的多型重構實踐》,作者:JavaEdge 。
1 動機
複雜的條件邏輯是程式設計中最難理解的東西之一,因此我一直在尋求給條件邏輯新增結構。很多時候,我發現可以將條件邏輯拆分到不同的場景(或者叫高階用例),從而拆解複雜的條件邏輯。這種拆分有時用條件邏輯本身的結構就足以表達,但使用類和多型能把邏輯的拆分表述得更清晰。
2 常見場景
2.1 構造一組型別,每個型別處理各自的一種條件邏輯
例如,我會注意到,圖書、音樂、食品的處理方式不同,這是因為它們分屬不同型別的商品。
最明顯的徵兆就是有好幾個函式都有基於型別程式碼的switch語句。若果真如此,我就可以針對switch語句中的每種分支邏輯建立一個類,用多型來承載各個型別特有的行為,從而去除重複的分支邏輯。
2.2 有一個基礎邏輯,在其上又有一些變體
基礎邏輯可能是最常用的,也可能是最簡單的。我可把基礎邏輯放進超類,這樣我可以首先理解這部分邏輯,暫時不管各種變體,然後我可以把每種變體邏輯單獨放進一個子類,其中的程式碼著重強調與基礎邏輯的差異。
多型是物件導向程式設計的關鍵特性之一,但也很容易被濫用。有人爭論說所有條件邏輯都應該用多型取代,我不贊同這種觀點。我的大部分條件邏輯只用到了基本的條件語句——if/else和switch/case,無需勞師動眾地引入多型。但如果發現如前所述的複雜條件邏輯,多型是改善這種情況的有力工具。
做法
- 如果現有的類尚不具備多型行為,就用工廠函式建立之,令工廠函式返回恰當的物件例項。
- 在呼叫方程式碼中使用工廠函式獲得物件例項。
- 將帶有條件邏輯的函式移到超類中。
- 如果條件邏輯還未提煉至獨立的函式,首先對其使用【提煉函式】
- 任選一個子類,在其中建立一個函式,使之覆寫超類中容納條件表示式的那個函式。將與該子類相關的條件表示式分支複製到新函式中,並對它進行適當調整。
- 重複上述過程,處理其他條件分支。
在超類函式中保留預設情況的邏輯。或者,如果超類應該是抽象的,就把該函式宣告為abstract,或在其中直接丟擲異常,表明計算責任都在子類中。
案例
有群鳥,想知道這些鳥飛得有多快,以及它們的羽毛是啥樣:
package com.javaedge.refactor.condition; import lombok.Getter; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; /** * @author JavaEdge * @date 2022/4/10 */ @Getter public class Bird { private Objects plumages; private String name; private String type; private int numberOfCoconuts; private boolean isNailed; private int voltage; public Map plumages(List<Bird> birds) { return birds.stream().collect(Collectors.toMap(Bird::getPlumages, Function.identity())); } public Map speeds(List<Bird> birds) { return birds.stream().collect(Collectors.toMap(Bird::getName, airSpeedVelocity(b))); } public String plumage(Bird bird) { switch (bird.type) { case "EuropeanSwallow": return "average"; case "AfricanSwallow": return (bird.numberOfCoconuts > 2) ? "tired" : "average"; case "NorwegianBlueParrot": return (bird.voltage > 100) ? "scorched" : "beautiful"; default: return "unknown"; } } public Integer airSpeedVelocity(Bird bird) { switch (bird.type) { case "EuropeanSwallow": return 35; case "AfricanSwallow": return 40 - 2 * bird.numberOfCoconuts; case "NorwegianBlueParrot": return (bird.isNailed) ? 0 : 10 + bird.voltage / 10; default: return null; } } }
有兩個不同的操作,其行為都隨著“鳥的型別”發生變化,因此可以建立出對應的類,用多型來處理各型別特有的行為。
先對airSpeedVelocity和plumage兩個函式使用【函式組合成類】。
package com.javaedge.refactor.condition; import lombok.Getter; /** * @author JavaEdge * @date 2022/4/10 */ @Getter public class Test { private Bird bird; public String plumages() { return bird.getPlumage(); } public int speeds() { return bird.airSpeedVelocity(); } } package com.javaedge.refactor.condition; import lombok.Getter; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; /** * @author JavaEdge * @date 2022/4/10 */ @Getter @NoArgsConstructor public class Bird { private Objects plumages; private String name; private String type; private int numberOfCoconuts; private boolean isNailed; private int voltage; Bird Bird(Bird bird) { return new Bird(); } public String getPlumage() { switch (this.type) { case "EuropeanSwallow": return "average"; case "AfricanSwallow": return (this.numberOfCoconuts > 2) ? "tired" : "average"; case "NorwegianBlueParrot": return (this.voltage > 100) ? "scorched" : "beautiful"; default: return "unknown"; } } public Integer airSpeedVelocity() { switch (this.type) { case "EuropeanSwallow": return 35; case "AfricanSwallow": return 40 - 2 * this.numberOfCoconuts; case "NorwegianBlueParrot": return (this.isNailed) ? 0 : 10 + this.voltage / 10; default: return null; } } }
然後針對每種鳥建立一個子類,用一個工廠函式來例項化合適的子類物件。
package com.javaedge.refactor.condition; import lombok.Getter; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; /** * @author JavaEdge * @date 2022/4/10 */ @Getter @NoArgsConstructor public class Bird { private Objects plumages; private String name; private String type; private int numberOfCoconuts; private boolean isNailed; private int voltage; public Bird(Bird bird) { } public String getPlumage() { switch (this.type) { case "EuropeanSwallow": return "average"; case "AfricanSwallow": return (this.numberOfCoconuts > 2) ? "tired" : "average"; case "NorwegianBlueParrot": return (this.voltage > 100) ? "scorched" : "beautiful"; default: return "unknown"; } } public Integer airSpeedVelocity() { switch (this.type) { case "EuropeanSwallow": return 35; case "AfricanSwallow": return 40 - 2 * this.numberOfCoconuts; case "NorwegianBlueParrot": return (this.isNailed) ? 0 : 10 + this.voltage / 10; default: return null; } } public String plumage(Bird bird) { return createBird(bird).getPlumage(); } public Integer airSpeedVelocity(Bird bird) { return createBird(bird).airSpeedVelocity(); } public Bird createBird(Bird bird) { switch (bird.type) { case "EuropeanSwallow": return new EuropeanSwallow(bird); case "AfricanSwallow": return new AfricanSwallow(bird); case "NorwegianBlueParrot": return new NorwegianBlueParrot(bird); default: return new Bird(bird); } } } class EuropeanSwallow extends Bird { public EuropeanSwallow(Bird bird) { } } class AfricanSwallow extends Bird { public AfricanSwallow(Bird bird) { } } class NorwegianBlueParrot extends Bird { public NorwegianBlueParrot(Bird bird) { } }
現在我已經有了需要的類結構,可以處理兩個條件邏輯了。先從plumage函式開始,我從switch語句中選一個分支,在適當的子類中覆寫這個邏輯。
class EuropeanSwallow extends Bird { public EuropeanSwallow(Bird bird) { } @Override public String getPlumage() { return "average"; } @Override public Integer airSpeedVelocity() { return 35; } } class AfricanSwallow extends Bird { private int numberOfCoconuts; public AfricanSwallow(Bird bird) { } @Override public String getPlumage() { return (this.numberOfCoconuts > 2) ? "tired" : "average"; } @Override public Integer airSpeedVelocity() { return 40 - 2 * this.numberOfCoconuts; } } class NorwegianBlueParrot extends Bird { private int voltage; private boolean isNailed; public NorwegianBlueParrot(Bird bird) { } @Override public String getPlumage() { return (this.voltage > 100) ? "scorched" : "beautiful"; } @Override public Integer airSpeedVelocity() { return (this.isNailed) ? 0 : 10 + this.voltage / 10; } }
父類函式保留下來處理預設情況。
public String getPlumage() { return "unknown"; }