問題的提出

閱讀別人程式碼的時候最討厭遇到的就是大段大段的if-else分支語句,一般來說讀到下面的時候就忘了上面在判斷什麼了。很多資料上都會講到使用策略模式來改進這種程式碼邏輯。

策略模式的類圖如下:

只需要按照這個圖寫程式碼就可以了。

策略模式程式碼的實現

藉助Spring框架我們能夠輕鬆的實現策略模式。

舉一個簡單的例子,我們去咖啡店買咖啡的時候,會根據自己的喜好和胃容量選擇大小杯。那麼我們就要實現一個CoffeeStategy:

package com.example.demo.strategy;

public interface CoffeeStrategy {
void offer();
}

接下來就是各種具體策略的實現了,以中杯咖啡為例:

package com.example.demo.strategy;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; @Component("MID")
@Slf4j
public class MidCoffee implements CoffeeStrategy {
@Override
public void offer() {
log.info("你的中杯咖啡");
}
}

用Component註解給這個類起一個名字叫做MID,這個在後面的應用上下文中有起效。現在就開始定義應用上下文類:

package com.example.demo.strategy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import java.util.Map; @Service
public class CoffeeContext {
@Autowired
private Map<String, CoffeeStrategy> coffeeStrategyMap; public void getCoffee(String size) {
this.coffeeStrategyMap.get(size).offer();
}
}

因為是使用了Spring框架,所有的Bean都被Spring自行管理,啟動之後,Map中會有兩個元素:{"MID":MidCoffee}和{"LARGE":LargeCoffee}。在具體的業務邏輯中,只需要引入應用上下文類,每次使用getCoffee方法就可以了。

比如這個Controller方法:

@GetMapping("/get")
public void getCoffee(@Param("size") String size) {
this.coffeeContext.getCoffee(size);
}

請求這個介面,我們能在後臺看到具體的日誌內容:

2021-09-30 22:46:32.550  INFO 15628 --- [nio-8099-exec-1] com.example.demo.strategy.LargeCoffee    : 您的大杯咖啡
2021-09-30 22:46:39.201 INFO 15628 --- [nio-8099-exec-7] com.example.demo.strategy.LargeCoffee : 您的大杯咖啡

進一步的思考

之前寫過Component中起的名字有奇效。如果我們沒有用Spring框架去實現策略模式,那麼我們的程式碼要如何編寫呢?

首先可以肯定的是策略介面和策略實現類是不需要變的。需要變的地方就是應用上下文了,因為不存在自動注入了。這段程式碼就會變成大致這樣:

package com.example.demo.strategy;

public class CoffeeContext {

    CoffeeStrategy coffeeStrategy;
public CoffeeContext(CoffeeStrategy coffeeStrategy) {
this.coffeeStrategy = coffeeStrategy;
} public void getCoffee() {
this.coffeeStrategy.offer();
}
}

這樣,在實際使用的時候,我需要先新建一個具體的實現類物件,然後將這個物件傳入策略應用上下文去。這種方式怎麼看著都沒有Spring的實現方式優雅。

CoffeeStrategy mid = new MidCoffee();
CoffeeContext context = new CoffeeContext(mid);
context.getCoffee();

在我實際改造程式碼的過程中我發現有些策略其實是一樣的,只是個別引數不同罷了。我對接的是各個業務供應商,有些供應商的介面邏輯式樣的,只是URL和USERNAME不一樣罷了。於是好幾個策略實現類的程式碼重複很嚴重,這個時候我使用了Java8開始提供的介面default方法。這種方法的好處就是能將這種一樣的邏輯提取到interface中,只要實現類不重寫,那麼就會預設使用default方法。

這樣改造之後,我的程式碼又精簡了很多。

心得體會

在我接手現在這個專案程式碼的時候,之前的程式設計師將程式碼寫的很直白,就是可以不用任何的設計,直接寫邏輯。這也沒錯,可是用IDEA的時候會各種提示重複程式碼啊之類的,讓人看著不開心。而且還有大量的if-else分支讓人摸不著頭腦。

在我大刀闊斧的改造之後,程式碼行數越來越少,但是可讀性卻越來越高。

此時我是比較理解GoF在設計模式這本書裡提到的一句話,大致意思就是開發一個面向物件的程式並不簡單。