菜鳥成長系列-策略模式

glmapper發表於2018-05-05

上次在模板方法模式中有提及到,模板方法模式通常不會單獨來試用,在一些實際的應用中會搭配其他的模式來使用,比如說今天要學習的策略模式。

一直我都很喜歡策略這個詞,有種莫名的高大上,對三國有了解的小夥伴肯定會知道,有的謀士是比較直接的,獻計就是獻計,有話當面說;但是也有的謀士就是比較喜歡搞一種神祕感,弄個小布袋子裡面塞個小布條(簡稱:錦囊);對於一件很棘手的事情,在交代下去的時候就會有這樣的囑咐:“此事關係重大,還望XXX(暱稱)務必處理妥帖;這裡有三個錦囊,如果XXXX,你就拆開第X個錦囊,然後XXXX”;有時候我就很不解,假如真在遇到事情的時候來看,那路上丟了怎麼辦?一摸口袋就懵逼了有木有?

扯遠了,不過意思就是這個意思,一個錦囊其實就是一種策略;然後它有一個總的背景(我們稱之為上下文環境),這個大背景下,每個不同的場景都會有一中策略來對應處理;

我們先以上面的列子為背景來擼一個小的例子,然後再去看一個spring中比較典型的策略模式使用,最後再來探討下策略模式的類圖,並以此來說明策略模式中的一些基本角色及其職責。

錦囊妙計

兵馬未動,糧草先行;但是這個運輸糧草到底是走水路還是走陸地呢?那這得看往哪運...

package com.glmapper.designmode.policy;
/**
 * @description: 大背景,運輸糧草
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: 磊叔
 * @date: 18/5/5
 */
public class TransportFood {
    //持有一個運輸策略的物件
    private TransportFoodStrategy strategy;
    /**
     * 建構函式,傳入一個具體策略物件
     * @param strategy    具體策略物件
     */
    public TransportFood(TransportFoodStrategy strategy){
        this.strategy = strategy;
    }
    /**
     * 策略方法
     */
    public void trasportFood(){

        strategy.trasport();
    }
}
複製程式碼

這個是我們的總體背景,就是運輸糧草;但是這個只是說要運輸糧草,但是並沒有說是怎麼運?這就得TransportFoodStrategy這個運輸策略有具體的運輸方案。

  • 運輸方案1:如果糧草是從武漢到南京,OK,那就走水運吧。
/**
 * @description: 運輸糧草的策略之水運運輸
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: 磊叔
 * @date: 18/5/5
 */
public class WaterTransportStrategy implements TransportFoodStrategy {
    @Override
    public void trasport() {
        System.out.println("用船,走水運");
    }
}
複製程式碼
  • 運輸方案2:如果從內蒙到北京;那就走陸運吧。
/**
 * @description: 運輸糧草的策略之陸地運輸
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: 磊叔
 * @date: 18/5/5
 */
public class LandTransportStrategy implements TransportFoodStrategy {
    @Override
    public void trasport() {
        System.out.println("用馬車,走陸運");
    }
}
複製程式碼

好了,來看下妙計使用:

/**
 * @description: 決策制定-客戶端
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: 磊叔
 * @date: 18/5/5
 */
public class Client {

    public static void main(String[] args) {

        TransportFoodStrategy strategy =
        getTransportFoodStrategy("內蒙到北京");

        TransportFood transportFood = new TransportFood(strategy);
        transportFood.trasportFood();
    }

    /**
     * 獲取運輸方案
     * @param lineType 運輸路線
     * @return
     */
    private static TransportFoodStrategy getTransportFoodStrategy(
    String lineType){
        if (lineType.equals("內蒙到北京")){
            return new LandTransportStrategy();
        }
        if (lineType.equals("武漢到南京")){
            return new WaterTransportStrategy();
        }
        return null;
    }
}

-> 用馬車,走陸運
複製程式碼

糧草運完了,真正的表演開始了...

Spring中典型的策略模式使用

我們知道spring載入資原始檔是通過ResourceLoader來搞定的。在ResourceLoader中提供了一個:

Resource getResource(String location);
複製程式碼

這個方法的註解中說道

//允許多個資源呼叫。
allowing for multiple {@link Resource#getInputStream()} calls.
複製程式碼

這裡就很赤裸裸了,他告訴了你要獲取資源,但是如果獲取資源呢?這就得看有哪些具體的獲取策略了。

菜鳥成長系列-策略模式

上圖就是Resource的具體子類實現,也就是一些具體的策略。我們比較常見的應該算是UrlResource(載入URL指定的資源)和ClasspathResource(載入類路徑中的資源)這兩個。再來看下這個getResource這個方法的實現:

getResource方法是在DefaultResourceLoader中具體實現的;DefaultResourceLoader是ResourceLoader的預設實現。

@Override
public Resource getResource(String location) {
	Assert.notNull(location, "Location must not be null");
	//首先使用ProtocolResolver來通過location引數建立Resource物件
	// spring4.3.x開始才有的
	for (ProtocolResolver protocolResolver : this.protocolResolvers) {
            Resource resource = protocolResolver.resolve(location,this);
            if (resource != null) {
        	return resource;
            }
	}
        //指定路徑的
	if (location.startsWith("/")) {
		return getResourceByPath(location);
	}
	//以classpath開頭的
	else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
		return new ClassPathResource(location.substring(
		CLASSPATH_URL_PREFIX.length()), getClassLoader());
	}
	//這裡是先嚐試解析是否是帶有網路協議的資源,
	//如果解析異常,則是在異常處理中使用了一種預設的機制。
	else {
		try {
			// Try to parse the location as a URL...
			URL url = new URL(location);
			return new UrlResource(url);
		}
		catch (MalformedURLException ex) {
			// No URL -> resolve as resource path.
			return getResourceByPath(location);
		}
	}
}
複製程式碼

關於ProtocolResolver

其實我們可以發現,這裡的location其實和我們上面那個例子中的lineType的作用是一樣的,根據這個來確定具體使用哪個策略方法。

  • 策略1:使用ProtocolResolver來通過location引數建立Resource物件,在ProtocolResolver中關於ProtocolResolver的解釋是:A resolution strategy for protocol-specific resource handles-協議專用資源控制程式碼的解析策略。
  • 策略2:返回給定路徑上資源的資源控制程式碼。
  • 策略3:以classpath:為字首的,這種location引數直接返回一個ClassPathResource物件,表示載入classes路徑下的資源;
  • 策略4:使用網路協議作為字首的,比如http、ftp等,這種直接返回一個UrlResource物件;
  • 策略5:無字首的,在預設實現中和第三種一樣是載入classes路徑下的資源,不同的是此處當作是ClassPathContextResource來處理的。

Spring中Resource的策(tao)略(lu)說完了,再回過頭來看下策略模式的一些具體理論知識。

策略模式

定義:策略模式屬於物件的行為模式。其用意是針對一組演算法,將每一個演算法封裝到具有共同介面的獨立的類中,從而使得它們可以相互替換。策略模式使得演算法可以在不影響到客戶端的情況下發生變化。

結合前面的例子分析和這段定義,可以知道,其實策略模式真的意圖不是如何實現策略演算法,它更在意的是如何組織這些演算法。

這也是策略模式的使用可以讓程式結構更靈活,具有更好的維護性和擴充套件性的重要因素。

類圖:

類圖
這個類圖畫的確實是有點醜,但是為了親手繪製一下,所以還請多多見諒!

類圖中的一些角色:

  • context:策略背景,也就是需要使用策略的主體;它持有一個strategy類的引用
  • strategy:抽象策略,這個角色給出了所有具體策略類所需的介面。所以通常是一個抽象類或者介面。
  • strategyPolicy:具體策略,它的作用就是包裝具體的演算法或者行為

那麼在實際的應用中,策略模式到底給我們帶來的好處是什麼,它能夠幫助我們解決什麼樣的問題呢?這個需要從模式本身的優缺點來看:

優點

  • 策略模式提供了管理相關的演算法族的辦法。策略類的等級結構定義了一個演算法或行為族。恰當使用繼承可以把公共的程式碼移到父類裡面,從而避免程式碼重複。

  • 策略模式可以避免使用多重條件(if-else)語句。通常對於一個背景主體,一般只會有一種策略演算法可供使用,使用多重條件句的話不易維護;因為它把採取哪一種演算法或採取哪一種行為的邏輯與演算法或行為的邏輯混合在一起了。

缺點

  • 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法類。換言之,策略模式只適用於客戶端知道演算法或行為的情況。

  • 由於策略模式把每個具體的策略實現都單獨封裝成為類,如果備選的策略很多的話,那麼物件的數目就會很可觀。--如果策略很多,通常會採用一些混合策略來避免策略類的不斷膨脹。

在瞭解其優缺點的情況下,我們就可以合理的將其放在一些適當的場景中來;如以下場景:

  • 如果在一個系統裡面有許多類,它們之間的區別僅在於它們的行為,那麼使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。
  • 一個系統需要動態地在幾種演算法中選擇一種。
  • 如果一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。

參考

相關文章