消除代碼壞味道之減少嵌套if的一些方式
【最近發(fā)生了一些事情,不過(guò)是,螞蟻緣槐夸大國(guó),蚍蜉撼樹(shù)談何易?!?br>
在實(shí)際的代碼開(kāi)發(fā)過(guò)程中,經(jīng)常能夠見(jiàn)到一個(gè)類(lèi)或者方法中存在大量的if-else代碼塊,一個(gè)復(fù)雜的邏輯幾乎沒(méi)有方法的抽象和提取,只能根據(jù)if判斷條件去理解業(yè)務(wù)邏輯。這是典型的代碼壞味道。
更有甚者else中還繼續(xù)嵌套if,甚至出現(xiàn)嵌套層級(jí)大于三層的情況,讓維護(hù)者很是頭痛,代碼如下所示。
public void handle(String orderId, Double actualPrice, Double couponPrice) {
if (StringUtils.isNotBlank(orderId)) {
if (actualPrice != null) {
if (couponPrice != null) {
// 執(zhí)行業(yè)務(wù)邏輯
doSomething();
}
}
} else {
// 其他邏輯
}
}
這是一個(gè)簡(jiǎn)單的訂單處理邏輯,當(dāng)訂單號(hào)orderId、實(shí)際金額actualPrice以及券金額couponPrice均不為空時(shí)執(zhí)行業(yè)務(wù)邏輯,否則執(zhí)行其他邏輯。
初學(xué)者常常喜歡將判斷屬性非空的邏輯通過(guò)嵌套if方式編寫(xiě),代碼閱讀體驗(yàn)比較差,如果邏輯復(fù)雜,則嵌套if代碼塊到處都是,無(wú)形中提高了理解成本。這里提供幾種優(yōu)化方式。
使用一層判斷減少嵌套if
首先介紹的方法是通過(guò)使用一層判斷減少嵌套if,具體的做法是對(duì)于嵌套if,通過(guò)條件運(yùn)算符對(duì)判斷條件進(jìn)行連接,如果判斷條件過(guò)長(zhǎng)則重構(gòu)提取為一個(gè)獨(dú)立的判斷方法,這樣代碼看起來(lái)就會(huì)比較精煉。
public void handle2(String orderId, Double actualPrice, Double couponPrice) {
if (isOrderParameterIllegal(orderId, actualPrice, couponPrice)) {
// 執(zhí)行業(yè)務(wù)邏輯
doSomething();
} else {
// 其他邏輯
}
}
private boolean isOrderParameterIllegal(String orderId, Double actualPrice, Double couponPrice) {
return StringUtils.isNotBlank(orderId)
&& actualPrice != null && couponPrice != null;
}
上述代碼中,將之前的嵌套if通過(guò)條件運(yùn)算符“&&”(AND)進(jìn)行連接,并將連接之后的復(fù)雜條件表達(dá)式單獨(dú)提取為一個(gè)方法,只有全部滿足才返回true,否則返回false。
這樣處理之后,主流程的代碼就看起來(lái)比較清晰,閱讀體驗(yàn)比較好。這種通過(guò)合并判斷條件,并將判斷條件單獨(dú)提取的方式在開(kāi)發(fā)中是一種常見(jiàn)的消除嵌套if帶來(lái)的壞味道的方式。
使用衛(wèi)語(yǔ)句減少嵌套if層級(jí)
除了合并判斷條件外,還可以通過(guò)使用“衛(wèi)語(yǔ)句”的方式減少嵌套if層級(jí)。
經(jīng)典軟件開(kāi)發(fā)著作《重構(gòu)》中是這樣定義“衛(wèi)語(yǔ)句”的:
?如果條件語(yǔ)句極其復(fù)雜,就應(yīng)該將條件語(yǔ)句拆解開(kāi),然后逐個(gè)檢查,并在條件為真時(shí)立刻從函數(shù)中返回,這樣的單獨(dú)檢查通常被稱之為“衛(wèi)語(yǔ)句”(guard clauses)。
?
簡(jiǎn)單的說(shuō)就是對(duì)復(fù)雜條件進(jìn)行逐個(gè)檢查,從反向邏輯進(jìn)行考慮,一旦不滿足條件就直接返回,將不滿足條件的情況考慮完全之后,直接執(zhí)行剩下的正常邏輯即可。還是通過(guò)章節(jié)開(kāi)頭的案例進(jìn)行直觀展示,代碼如下:
public void handle3(String orderId, Double actualPrice, Double couponPrice) {
if (StringUtils.isBlank(orderId)) {
// 一些善后邏輯
otherThing();
return;
}
if (actualPrice == null) {
// 一些善后邏輯
otherThing();
return;
}
if (couponPrice == null) {
// 一些善后邏輯
otherThing();
return;
}
// 執(zhí)行業(yè)務(wù)邏輯
doSomething();
}
通過(guò)代碼案例可以看到,衛(wèi)語(yǔ)句是對(duì)各種異常條件進(jìn)行先行判斷,參數(shù)一旦滿足這些異常情況則直接結(jié)束業(yè)務(wù)邏輯,執(zhí)行一些善后操作后就返回了。
當(dāng)所有考慮到的異常情況被校驗(yàn)之后,直接執(zhí)行正常的業(yè)務(wù)邏輯即可。這種代碼編寫(xiě)風(fēng)格直接消除了嵌套if,將代碼層級(jí)優(yōu)化為最多嵌套一層if,代碼閱讀難度大幅度降低,整體代碼風(fēng)格清新,條理,讓閱讀者心情暢快。這種風(fēng)格也是筆者比較推崇的,希望讀者朋友能夠吸收并加以運(yùn)用。
使用策略模式消除復(fù)雜if判斷
對(duì)于復(fù)雜場(chǎng)景的業(yè)務(wù)判斷,通過(guò)引入策略模式能夠完全消除if,在實(shí)際開(kāi)發(fā)場(chǎng)景中,這也是一種經(jīng)常使用到的編碼技巧。
關(guān)于策略模式暫時(shí)不展開(kāi)講解,有經(jīng)驗(yàn)的同學(xué)自然用過(guò),沒(méi)有學(xué)過(guò)的同學(xué)可以去找資料學(xué)一下,比如說(shuō)《設(shè)計(jì)模式之禪》。
假設(shè)有一個(gè)后端接口服務(wù),需要同時(shí)對(duì)App、微信小程序、支付寶小程序、PC網(wǎng)頁(yè)端提供服務(wù),此時(shí)有個(gè)需求要統(tǒng)計(jì)來(lái)自不同端的用戶行為,并針對(duì)不同來(lái)源的用戶進(jìn)行特定的操作,如圖所示。

如果采用傳統(tǒng)的if-else或者switch-case方式進(jìn)行編碼,代碼看起來(lái)如下所示。
public void handleByChannelType(ChannelType channelType) {
if (channelType == ChannelType.PHONE_APP) {
System.out.println("手機(jī)App渠道邏輯");
} else if (channelType == ChannelType.PHONE_H5) {
System.out.println("H5渠道邏輯");
} else if (channelType == ChannelType.MICRO_APPLET_WECHAT) {
System.out.println("微信小程序處理邏輯");
} else if (channelType == ChannelType.MICRO_APPLET_ALIPAY) {
System.out.println("支付寶小程序處理邏輯");
} else {
System.out.println("其他邏輯");
}
}
public void handleByChannelType2(ChannelType channelType) {
switch (channelType) {
case PHONE_H5:
System.out.println("手機(jī)App渠道邏輯");
break;
case PHONE_APP:
System.out.println("H5渠道邏輯");
break;
case MICRO_APPLET_WECHAT:
System.out.println("微信小程序處理邏輯");
break;
case MICRO_APPLET_ALIPAY:
System.out.println("支付寶小程序處理邏輯");
break;
default:
System.out.println("其他邏輯");
}
}
代碼中的渠道類(lèi)型會(huì)一直增加,每個(gè)渠道內(nèi)部的代碼邏輯也會(huì)逐步修改與增加,隨著代碼邏輯被不斷修改,這部分代碼勢(shì)必會(huì)變得越來(lái)越復(fù)雜,最終難以維護(hù)。這是if-else以及switch-case在應(yīng)對(duì)復(fù)雜業(yè)務(wù)場(chǎng)景時(shí)天然的不足之處。
那么此時(shí)如果使用策略模式進(jìn)行重構(gòu),則能夠很好的解決這個(gè)問(wèn)題。
(1)首先定義一個(gè)接口提供一個(gè)getChannelType()方法,供不同的渠道實(shí)現(xiàn)類(lèi)進(jìn)行實(shí)現(xiàn),返回實(shí)際的渠道類(lèi)型;同時(shí)接口提供了doSomething()方法,表示抽象的業(yè)務(wù)邏輯,不同的渠道實(shí)現(xiàn)類(lèi)實(shí)現(xiàn)該方法,對(duì)外提供不同的業(yè)務(wù)邏輯實(shí)現(xiàn);
public interface ChannelStrategy {
/**具體的業(yè)務(wù)邏輯*/
void doSomething();
/**獲取渠道類(lèi)型*/
ChannelType getChannelType();
}
(2)定義不同渠道的策略實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)接口ChannelStrategy,支付寶小程序渠道代碼如下:
public class MicroAppletAlipayStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("支付寶小程序處理邏輯");
}
@Override
public ChannelType getChannelType() {
return ChannelType.MICRO_APPLET_ALIPAY;
}
}
微信小程序代碼如下:
public class MicroAppletWechatStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("微信小程序處理邏輯");
}
@Override
public ChannelType getChannelType() {
return ChannelType.MICRO_APPLET_WECHAT;
}
}
手機(jī)App渠道的策略實(shí)現(xiàn)如下:
public class PhoneAppChannelStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("手機(jī)App渠道邏輯");
}
@Override
public ChannelType getChannelType() {
return ChannelType.PHONE_APP;
}
}
手機(jī)H5頁(yè)面的渠道策略如下:
public class PhoneH5ChannelStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("H5渠道邏輯");
}
@Override
public ChannelType getChannelType() {
return ChannelType.PHONE_H5;
}
}
(3)接著定義策略上下文,該上下文中包含一個(gè)Map,上下文定義為單例,在實(shí)例初始化階段加載不同的渠道邏輯實(shí)現(xiàn)到Map中;
public class ChannelStrategyContext {
private static final Map CHANNEL_STRATEGY_MAP = new ConcurrentHashMap<>();
private ChannelStrategyContext() {
ChannelStrategy phoneH5 = new PhoneAppChannelStrategyImpl();
ChannelStrategy phoneApp = new PhoneAppChannelStrategyImpl();
ChannelStrategy appletAlipay = new MicroAppletAlipayStrategyImpl();
ChannelStrategy appletWechat = new MicroAppletWechatStrategyImpl();
CHANNEL_STRATEGY_MAP.put(phoneH5.getChannelType(), phoneH5);
CHANNEL_STRATEGY_MAP.put(phoneApp.getChannelType(), phoneApp);
CHANNEL_STRATEGY_MAP.put(appletAlipay.getChannelType(), appletAlipay);
CHANNEL_STRATEGY_MAP.put(appletWechat.getChannelType(), appletWechat);
}
public static ChannelStrategyContext getInstance() {
return Holder.singleton;
}
public void domeSomething(ChannelType channelType) {
CHANNEL_STRATEGY_MAP.get(channelType).doSomething();
}
/**
* 內(nèi)部類(lèi),為單例提供
*/
private static class Holder {
private static ChannelStrategyContext singleton = new ChannelStrategyContext();
}
}
(4)接著在策略上下文中定義doSomething()方法,通過(guò)委托的方式根據(jù)方法參數(shù)中傳入的ChannelType從Map中篩選出對(duì)應(yīng)的渠道對(duì)象,并調(diào)用該渠道對(duì)象的doSomething()方法;
編寫(xiě)一段代碼測(cè)試一下通過(guò)策略模式改寫(xiě)后的邏輯,假設(shè)上游傳遞的ChannelType類(lèi)型為微信小程序MICRO_APPLET_WECHAT,則業(yè)務(wù)邏輯代碼如下所示:
public class Main {
public static void main(String[] args) {
ChannelType wechatApplet = ChannelType.MICRO_APPLET_WECHAT;
ChannelStrategyContext.getInstance().domeSomething(wechatApplet);
}
}
運(yùn)行測(cè)試代碼,觀察控制臺(tái)輸出如下:
微信小程序處理邏輯
Process finished with exit code 0
可以看到,相比于if-else或者switch-case,主流程的代碼非常簡(jiǎn)潔,只需要一行代碼就能根據(jù)ChannelType執(zhí)行到對(duì)應(yīng)的邏輯,代碼邏輯更容易被人所理解。一旦需要增加新的渠道,只需要增加一個(gè)新的ChannelStrategy實(shí)現(xiàn)類(lèi),并添加到ChannelStrategyContext上下文中。這體現(xiàn)出了代碼編寫(xiě)中的開(kāi)發(fā)封閉原則,即對(duì)主業(yè)務(wù)流程的修改是關(guān)閉的,對(duì)渠道的新增是開(kāi)放的。
相信有的讀者發(fā)現(xiàn),每次新增新的渠道實(shí)現(xiàn)都需要修改一下ChannelStrategyContext,還不夠友好,「有沒(méi)有一種方式能夠只增加渠道的實(shí)現(xiàn)就可以動(dòng)態(tài)的將新的渠道類(lèi)型注冊(cè)到ChannelStrategyContext中呢?」
Spring集合注入方式動(dòng)態(tài)裝載策略容器
其實(shí)是有的,可以通過(guò)基于Spring集合類(lèi)型注入的方式對(duì)接口所有實(shí)現(xiàn)類(lèi)批量注入從而避免手動(dòng)編寫(xiě)大量的put代碼,實(shí)現(xiàn)新增渠道實(shí)現(xiàn)類(lèi)不需要修改ChannelStrategyContext的目的。接下來(lái)對(duì)這種方式進(jìn)行講解,讀者可以根據(jù)自己的理解與喜好在實(shí)戰(zhàn)中加以應(yīng)用。
首先介紹Spring集合類(lèi)型批量注入接口實(shí)現(xiàn)類(lèi)的方式,首先將ChannelStrategy的接口實(shí)現(xiàn)類(lèi)均標(biāo)注為Spring的Bean,如使用@Service、@Component注解,代碼如下(以支付寶渠道策略實(shí)現(xiàn)類(lèi)為例)。
@Service
public class MicroAppletAlipayStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("支付寶小程序處理邏輯");
}
@Override
public ChannelType getChannelType() {
return ChannelType.MICRO_APPLET_ALIPAY;
}
}
接著編寫(xiě)Spring配置類(lèi),通過(guò)@ComponentScan注解配置掃描包,開(kāi)啟注解支持。配置類(lèi)代碼如下:
@Configuration
@ComponentScan(basePackages = {"com.snowalker.from.distributed.to.cloudnative.section11_4.channeldemo.spring_collection_inject"})
public class BeanConfig {
}
?接著編寫(xiě)ChannelStrategySpringContext策略上下文,通過(guò)@Autowired注入List< ChannelStrategy>集合,完整的ChannelStrategySpringContext代碼如下:
@Service
public class ChannelStrategySpringContext {
private static final Map CHANNEL_STRATEGY_MAP = new ConcurrentHashMap<>();
@Autowired
List channelStrategies;
@PostConstruct
public void init() {
channelStrategies.stream().forEach(channelStrategy -> {
CHANNEL_STRATEGY_MAP.put(channelStrategy.getChannelType(), channelStrategy);
});
}
public void domeSomething(ChannelType channelType) {
CHANNEL_STRATEGY_MAP.get(channelType).doSomething();
}
}
對(duì)比上面未使用Spring框架的原生Java實(shí)現(xiàn)的ChannelStrategyContext,此處的ChannelStrategySpringContext代碼量更少,邏輯更加簡(jiǎn)潔。
通過(guò)直接注入List
通過(guò)@PostContruct注解標(biāo)注的init()方法,將ChannelStrategy接口的所有實(shí)例解析出來(lái)并加載到Map中,方便在方法中根據(jù)具體的ChannelType獲取對(duì)應(yīng)的ChannelStrategy實(shí)現(xiàn)。
其他方法和原生Java實(shí)現(xiàn)相同,但是省略了內(nèi)部類(lèi)以及獲取單例方法,原因在于Spring中Bean默認(rèn)為單例,因此不需要再顯式書(shū)寫(xiě)單例相關(guān)的代碼。
為方便讀者理解,將這部分邏輯用一張流程圖展示如圖所示。

最后編寫(xiě)測(cè)試代碼進(jìn)行調(diào)用,依舊指定渠道類(lèi)型ChannelType為微信小程序,測(cè)試代碼如下:
public class Client {
public static void main(String[] args) {
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BeanConfig.class);
ChannelStrategySpringContext channelStrategySpringContext =
applicationContext.getBean("channelStrategySpringContext", ChannelStrategySpringContext.class);
channelStrategySpringContext
.domeSomething(ChannelType.MICRO_APPLET_WECHAT);
}
}
代碼邏輯為:先定義AnnotationConfigApplicationContext上下文,加載BeanConfig配置類(lèi),開(kāi)啟注解支持。然后從上下文中根據(jù)bean名稱獲取到ChannelStrategySpringContext的實(shí)例,調(diào)用ChannelStrategySpringContext的domeSomething(ChannelType channelType)方法,指定ChannelType為微信小程序ChannelType.MICRO_APPLET_WECHAT。代碼運(yùn)行結(jié)果如下:
微信小程序處理邏輯
Process finished with exit code 0
到此就對(duì)如何基于策略模式消除代碼中的if邏輯進(jìn)行了充分的講解,希望能夠?qū)ψx者提升代碼質(zhì)量,消除過(guò)多if帶來(lái)的壞味道有所幫助。
后記:本文是新書(shū)的第11章節(jié)的節(jié)選,主要是講解了開(kāi)發(fā)中常用的消除嵌套if的一些策略,更多內(nèi)容正在持續(xù)輸出中,敬請(qǐng)期待。
