如何優雅的替換掉程式碼中的ifelse

薛定諤的雄貓發表於2020-05-01

場景

平時我們在寫程式碼時,需要針對不同情況處理不同的業務邏輯,用得最多的就是if和else。 但是如果情況太多,就會出現一大堆的“if else”,這就是為什麼很多遺留系統中,一個函式可能出現上千行的程式碼。當然你說可以通過抽取方法或者類來實現,每一個情況交給一個方法或者對應一個類來處理,但是這樣做只是看起來程式碼整潔了一些,還是有大量的”if else",後面有新的邏輯時,又要新增更多的“if else",沒有從根本上解決問題。

舉個例子,簡訊傳送業務的實現,一般公司會接入多個簡訊供應商,比如夢網、玄武、阿里雲等多個簡訊平臺(我們稱之為簡訊渠道),可能需要針對不同的簡訊型別或者簡訊平臺的穩定性來切換簡訊渠道:

  1. 比如阿里雲簡訊管控很嚴,帶營銷字樣的簡訊不讓傳送,則營銷類簡訊需要使用其他簡訊渠道來傳送;
  2. 也有可能某個簡訊平臺服務掛了暫時不可用,需要切換到另一個簡訊渠道;
  3. 某些簡訊平臺有優惠,則需要臨時切換到該簡訊渠道傳送簡訊;

程式碼實現

上面的業務場景簡單來說就是:針對不同的簡訊渠道來呼叫對應的簡訊平臺介面實現簡訊傳送。
簡訊渠道一般配置在檔案中,或者配置在資料庫中。

程式碼實現如下(注意下面所有的程式碼都不能直接執行,只是關鍵邏輯部分的示例程式碼):

爛程式碼示例

我們有一個簡訊傳送類:SmsSendService,裡面有一個send方法傳送簡訊
SmsSendService.java

public class SmsSendService{
	/**
 	 * @Param phoneNo 手機號
	 * @Param content 簡訊內容
	 */
	public void send(String phoneNo,String content){
		//從配置中讀取 簡訊渠道
		String channelType=config.getChannelType();
		
		//如果是簡訊渠道A,則呼叫渠道A的api傳送
		if(Objects.equals(channelType,"CHANNEL_A")){
			System.out.println("通過簡訊渠道A傳送簡訊");
		}
		//如果是簡訊渠道B,則呼叫渠道B的api傳送
		else if(Objects.equals(channelType,"CHANNEL_B")){
			System.out.println("通過簡訊渠道B傳送簡訊");
		}
	}
}

如果某天增加了一個簡訊渠道C,那麼接著追加一個”else if…"

//... 此處省略部分程式碼 ...

//從配置中讀取 簡訊渠道
String channelType=config.getChannelType();
//如果是簡訊渠道A,則呼叫渠道A的api傳送
if(Objects.equals(channelType,"CHANNEL_A")){
	System.out.println("通過簡訊渠道A傳送簡訊");
}
//如果是簡訊渠道B,則呼叫渠道B的api傳送
else if(Objects.equals(channelType,"CHANNEL_B")){
	System.out.println("通過簡訊渠道B傳送簡訊");
}
//ADD: 如果是簡訊渠道C,則呼叫渠道C的api傳送
else if(Objects.equals(channelType,"CHANNEL_C")){
	System.out.println("通過簡訊渠道C傳送簡訊");
}

//... 此處省略部分程式碼 ...

如果又加其他簡訊渠道了呢?你又寫一個“else if …" ?
顯然這種做法不可取,也不符合SOLID原則中的”開閉原則“ ——對擴充套件開放,對更改封閉。
這樣我們每次都需要修改原有程式碼(對更改沒有封閉),不斷的新增”if else"。

接下來我們把程式碼優化一下:

優化程式碼1


  1. 定義一個簡訊渠道的介面 SmsChannelService,所有的簡訊渠道API都實現該介面;

簡訊渠道介面 SmsChannelService.java

public interface SmsChannelService{
	//傳送簡訊
	void send(String phoneNo,String content);
}

簡訊渠道A SmsChannelServiceImplA.java

public class SmsChannelServiceImplA implements SmsChannelService {
	public void send(String phoneNo, String content) {
		System.out.println("通過簡訊渠道A傳送簡訊");
	}
}

簡訊渠道B SmsChannelServiceImplB.java

public class SmsChannelServiceImplB implements SmsChannelService {
	public void send(String phoneNo, String content) {
		System.out.println("通過簡訊渠道B傳送簡訊");
	}
}

  1. 通過工廠類來初始化所有簡訊渠道service

SmsChannelFactory.java

public class SmsChannelFactory {
	private Map<String,SmsChannelService> serviceMap;

	//初始化工廠,將所有的簡訊渠道Service放入Map中
	public SmsChannelFactory(){
		//渠道型別為 key , 對應的服務類為value :
		serviceMap=new HashMap<String, SmsChannelService>(2);
		serviceMap.put("CHANNEL_A",new SmsChannelServiceImplA());
		serviceMap.put("CHANNEL_B",new SmsChannelServiceImplB());
	}

	//根據簡訊渠道型別獲得對應渠道的Service
	public SmsChannelService buildService(String channelType){
		return serviceMap.get(channelType);
	}
}

  1. 在原來的SmsSendService中呼叫不同簡訊渠道的介面。

原來的 SmsSendService 類優化如下

public class SmsSendService {

	private SmsChannelFactory smsChannelFactory;

	public SmsSendService(){
		smsChannelFactory=new SmsChannelFactory();
	}

	public void send(String phoneNo,String content){
		//從配置中讀取 簡訊渠道
		String channelType=config.getChannelType();
		//獲取渠道型別對應的服務類
		SmsChannelService channelService=smsChannelFactory.buildService(channelType);
		//傳送簡訊
		channelService.send(phoneNo,content);
	}

}

這樣SmsSendService類非常簡潔,把“if else"幹掉了,
如果我要增加一個簡訊渠道C,無需再次更改 SmsSendService 類。
只需要增加一個類 SmsChannelServiceImplC 實現 SmsChannelService 介面,
然後在工廠類 SmsChannelFactory 中增加一行初始化 SmsChannelServiceImplC 的程式碼即可。

增加簡訊渠道C的實現 SmsChannelServiceImplC.java

public class SmsChannelServiceImplC implements SmsChannelService {
	public void send(String phoneNo, String content) {
		System.out.println("通過簡訊渠道C傳送簡訊");
	}
}

修改工廠類 SmsChannelFactory.java

public class SmsChannelFactory {
	private Map<String,SmsChannelService> serviceMap;

	//初始化 serviceMap ,將所有的簡訊渠道Service放入Map中
	public SmsChannelFactory(){
		//渠道型別為 key , 對應的服務類為value :
		serviceMap=new HashMap<String, SmsChannelService>(3);
		serviceMap.put("CHANNEL_A",new SmsChannelServiceImplA());
		serviceMap.put("CHANNEL_B",new SmsChannelServiceImplB());
		//ADD 增加一行 SmsChannelServiceImplC 的初始化程式碼 
		serviceMap.put("CHANNEL_C",new SmsChannelServiceImplC());
	}

	//根據渠道型別構建簡訊渠道Service
	public SmsChannelService buildService(String channelType){
		return serviceMap.get(channelType);
	}
}

“if else"是幹掉了,但還是得修改原來的類 SmsChannelFactory ,不滿足"開閉原則",有沒有更好得方式呢?

我們通過使用spring的依賴注入進一步優化程式碼:

優化程式碼2
  1. SmsChannelService 介面增加 getChannelType() 方法,這一步很關鍵。
public interface SmsChannelService {
	//傳送簡訊
	void send(String phoneNo,String content);
	//關鍵:增加getChannelType()方法,子類實現這個方法用於標識出渠道型別
	String getChannelType();
}

  1. 子類增加該方法的實現,並加上 @Service 註解,使其讓spring容器管理起來

SmsChannelServiceImplA.java

@Service
public class SmsChannelServiceImplA implements SmsChannelService {
	public void send(String phoneNo, String content) {
		System.out.println("通過簡訊渠道A傳送簡訊");
	}
	//關鍵:增加 getChannelType() 實現
	public String getChannelType() {
		return "CHANNEL_A";
	}
}

SmsChannelServiceImplB.java

@Service
public class SmsChannelServiceImplB implements SmsChannelService {
	public void send(String phoneNo, String content) {
		System.out.println("通過簡訊渠道B傳送簡訊");
	}
	//關鍵:增加 getChannelType() 實現
	public String getChannelType() {
		return "CHANNEL_B";
	}
}

  1. 修改 SmsChannelFactory 類: 這一步也很關鍵。

SmsChannelFactory.java

@Service
public class SmsChannelFactory {

	private Map<String,SmsChannelService> serviceMap;
	
	/*注入:通過spring容器將所有實現 SmsChannelService 介面的類的例項注入到 serviceList 中*/
	@Autowired
	private List<SmsChannelService> serviceList;

	/*通過 @PostConstruct 註解,在 SmsChannelFactory 例項化後,來初始化 serviceMap */
	@PostConstruct
	private void init(){
		if(CollectionUtils.isEmpty(serviceList)){
			return ;
		}
		serviceMap=new HashMap<String, SmsChannelService>(serviceList.size());
		//將 serviceList 轉換為 serviceMap
		for (SmsChannelService channelService : serviceList) {
			String channelType=channelService.getChannelType();
			//重複性校驗,避免不同實現類的 getChannelType() 方法返回同一個值。
			if(serviceMap.get(channelType)!=null){
				throw new RuntimeException("同一個簡訊渠道只能有一個實現類");
			}
			/*渠道型別為 key , 對應的服務類為value :
			與“優化程式碼1”中的通過手工設定“CHANNEL_A"、"CHANNEL_B"相比,
			這種方式更加自動化,後續在增加“CHANNEL_C"無需再改此處程式碼*/
			serviceMap.put(channelType,channelService);
		}
	}

	//根據渠道型別獲取對應簡訊渠道的Service
	public SmsChannelService buildService(String channelType){
		return serviceMap.get(channelType);
	}
}

  1. SmsSendService 加上 @Service 註解。通過 @Autowired 注入 SmsChannelFactory

SmsSendService.java

@Service
public class SmsSendService {

	@Autowired
	private SmsChannelFactory smsChannelFactory;

	public void send(String phoneNo,String content){
		//從配置中讀取簡訊渠道型別
		String channelType=config.getChannelType();
		//構建渠道型別對應的服務類
		SmsChannelService channelService=smsChannelFactory.buildService(channelType);
		//傳送簡訊
		channelService.send(phoneNo,content);
	}

}

這時,如果需要新增一個渠道C,那真的只需要新增一個 SmsChannelServiceImplC 即可,再也不用改原有程式碼,完全遵循“開閉原則”。

SmsChannelServiceImplC.java

@Service
public class SmsChannelServiceImplC implements SmsChannelService {
	public void send(String phoneNo, String content) {
		System.out.println("通過簡訊渠道C傳送簡訊");
	}

	public String getChannelType() {
		return "CHANNEL_C";
	}
}


總結

通過上述優化很好的去掉了 “if else" ,再也不會出現”又臭又長“像”衛生捲紙"一樣的程式碼了,而且完全遵循”開閉原則"。
spring是個好東西,關鍵看你怎麼用。











相關文章