設計模式 | 策略模式及典型應用

小旋鋒發表於2019-02-28

本文的主要內容:

  • 介紹策略模式
  • 示例
    • 商場購物打折策略的實現
  • 策略模式總結
  • 原始碼分析策略模式的典型應用
    • Java Comparator 中的策略模式
    • Spring Resource 中的策略模式
    • Spring Bean 例項化中的策略模式

更多內容可訪問我的個人部落格:laijianfeng.org
關注【小旋鋒】微信公眾號,及時接收博文推送

關注【小旋鋒】微信公眾號

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用
設計模式 | 裝飾者模式及典型應用
設計模式 | 介面卡模式及典型應用
設計模式 | 享元模式及典型應用
設計模式 | 組合模式及典型應用
設計模式 | 模板方法模式及典型應用
設計模式 | 迭代器模式及典型應用


策略模式

在軟體開發中,我們也常常會遇到類似的情況,實現某一個功能有多條途徑,每一條途徑對應一種演算法,此時我們可以使用一種設計模式來實現靈活地選擇解決途徑,也能夠方便地增加新的解決途徑。

譬如商場購物場景中,有些商品按原價賣,商場可能為了促銷而推出優惠活動,有些商品打九折,有些打八折,有些則是返現10元等。

而優惠活動並不影響結算之外的其他過程,只是在結算的時候需要根據優惠方案結算

商場促銷場景

角色

Context(環境類):環境類是使用演算法的角色,它在解決某個問題(即實現某個方法)時可以採用多種策略。在環境類中維持一個對抽象策略類的引用例項,用於定義所採用的策略。

Strategy(抽象策略類):它為所支援的演算法宣告瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是介面。環境類通過抽象策略類中宣告的方法在執行時呼叫具體策略類中實現的演算法。

ConcreteStrategy(具體策略類):它實現了在抽象策略類中宣告的演算法,在執行時,具體策略類將覆蓋在環境類中定義的抽象策略類物件,使用一種具體的演算法實現某個業務處理。

示例

如果要寫出一個商場優惠場景的Demo可以很快的寫出來,譬如

import java.text.MessageFormat;

public class Shopping {
    private String goods;
    private double price;
    private double finalPrice;
    private String desc;

    public Shopping(String goods, double price) {
        this.goods = goods;
        this.price = price;
    }

    public double calculate(String discountType) {
        if ("dis9".equals(discountType)) {
            finalPrice = price * 0.9;
            desc = "打九折";
        } else if ("dis8".equals(discountType)) {
            finalPrice = price * 0.8;
            desc = "打八折";
        } else if ("cash10".equals(discountType)) {
            finalPrice = price >= 10 ? price - 10 : 0;
            desc = "返現10元";
        } else {
            finalPrice = price;
            desc = "不參與優惠活動";
        }
        System.out.println(MessageFormat.format("購買的物品:{0},原始價格:{1},{2},最終價格為:{3}", goods, price, desc, finalPrice));
        return finalPrice;
    }
}
複製程式碼

測試

public class Test {
    public static void main(String[] args) {
        Shopping shopping1 = new Shopping("書籍-深入理解Java虛擬機器", 54.00);
        shopping1.calculate("dis9"); // 九折

        Shopping shopping2 = new Shopping("Apple 妙控滑鼠", 588.00 );
        shopping2.calculate("dis8");

        Shopping shopping3 = new Shopping("戴爾U2417H顯示器", 1479.00);
        shopping3.calculate("cash10");

        Shopping shopping4 = new Shopping("索尼ILCE-6000L相機", 3599.00);
        shopping4.calculate(null);
    }
}
複製程式碼

以上程式碼當然完成了我們的需求,但是存在以下問題:

  • Shopping 類的 calculate() 方法非常龐大,它包含各種優惠演算法的實現程式碼,在程式碼中出現了較長的 if…else… 語句,不利於測試和維護。

  • 增加新的優惠演算法或者對原有打折演算法進行修改時必須修改 Shopping 類的原始碼,違反了 "開閉原則",系統的靈活性和可擴充套件性較差。

  • 演算法的複用性差,如果在另一個系統中需要重用某些優惠演算法,只能通過對原始碼進行復制貼上來重用,無法單獨重用其中的某個或某些演算法。

所以我們需要使用策略模式對 Shopping 類進行重構,將原本龐大的 Shopping 類的職責進行分解,將演算法的定義和使用分離。

抽象策略類 Discount,它是所有具體優惠演算法的父類,定義了一個 discount 抽象方法

import lombok.Data;

@Data
public abstract class Discount {
    protected double finalPrice;
    protected String desc;

    public Discount(String desc) {
        this.desc = desc;
    }

    abstract double discount(double price);
}
複製程式碼

四種具體策略類,繼承自抽象策略類 Discount,並在 discount 方法中實現具體的優惠演算法

public class Dis9Discount extends Discount {
    public Dis9Discount() {
        super("打九折");
    }

    @Override
    double discount(double price) {
        finalPrice = price * 0.9;
        return finalPrice;
    }
}

public class Dis8Discount extends Discount{
    public Dis8Discount() {
        super("打八折");
    }

    @Override
    double discount(double price) {
        finalPrice = price * 0.8;
        return finalPrice;
    }
}

public class Cash10Discount extends Discount {
    public Cash10Discount() {
        super("返現10元");
    }

    @Override
    public double discount(double price) {
        this.finalPrice = price >= 10 ? price - 10 : 0;
        return finalPrice;
    }
}

public class NoneDiscount extends Discount {
    public NoneDiscount() {
        super("不參與優惠活動");
    }

    @Override
    double discount(double price) {
        finalPrice = price;
        return finalPrice;
    }
}
複製程式碼

環境類 Shopping,維護了一個 Discount 引用

public class Shopping {
    private String goods;
    private double price;
    private Discount discount;

    public Shopping(String goods, double price, Discount discount) {
        this.goods = goods;
        this.price = price;
        this.discount = discount;
    }

    public double calculate() {
        double finalPrice = discount.discount(this.price);
        String desc = discount.getDesc();
        System.out.println(MessageFormat.format("購買的物品:{0},原始價格:{1},{2},最終價格為:{3}", goods, price, desc, finalPrice));
        return finalPrice;
    }
}
複製程式碼

測試

public class Test {
    public static void main(String[] args) {
        Shopping shopping1 = new Shopping("書籍-深入理解Java虛擬機器", 54.00, new Dis9Discount());
        shopping1.calculate();

        Shopping shopping2 = new Shopping("Apple 妙控滑鼠", 588.00, new Dis8Discount());
        shopping2.calculate();

        Shopping shopping3 = new Shopping("戴爾U2417H顯示器", 1479.00, new Cash10Discount());
        shopping3.calculate();

        Shopping shopping4 = new Shopping("索尼ILCE-6000L相機", 3599.00, new NoneDiscount());
        shopping4.calculate();
    }
}
複製程式碼

結果

購買的物品:書籍-深入理解Java虛擬機器,原始價格:54,打九折,最終價格為:48.6
購買的物品:Apple 妙控滑鼠,原始價格:588,打八折,最終價格為:470.4
購買的物品:戴爾U2417H顯示器,原始價格:1,479,返現10元,最終價格為:1,469
購買的物品:索尼ILCE-6000L相機,原始價格:3,599,不參與優惠活動,最終價格為:3,599
複製程式碼

可以看到,使用策略模式重構後,Shopping 類的 calculate 方法簡潔了很多,當需要更改優惠演算法的時候不需要再修改 Shopping 類的原始碼;要擴充套件出新的優惠演算法很方便,只需要繼承抽象策略類 Discount 並實現 calculate 方法即可;優惠演算法很容易重用。

畫出類圖如下

示例.策略模式類圖

策略模式總結

策略模式的主要優點如下:

  • 策略模式提供了對 "開閉原則" 的完美支援,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。

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

  • 策略模式提供了一種可以替換繼承關係的辦法。如果不使用策略模式而是通過繼承,這樣演算法的使用就 和演算法本身混在一起,不符合 "單一職責原則",而且使用繼承無法實現演算法或行為在程式執行時的動態切 換。

  • 使用策略模式可以避免多重條件選擇語句。多重條件選擇語句是硬編碼,不易維護。

  • 策略模式提供了一種演算法的複用機制,由於將演算法單獨提取出來封裝在策略類中,因此不同的環境類可以方便地複用這些策略類。

策略模式的主要缺點如下:

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

  • 策略模式將造成系統產生很多具體策略類,任何細小的變化都將導致系統要增加一個新的具體策略類。

  • 無法同時在客戶端使用多個策略類,也就是說,在使用策略模式時,客戶端每次只能使用一個策略類,不支援使用一個策略類完成部分功能後再使用另一個策略類來完成剩餘功能的情況。

適用場景

  • 一個系統需要動態地在幾種演算法中選擇一種,那麼可以將這些演算法封裝到一個個的具體演算法類中,而這些具體演算法類都是一個抽象演算法類的子類。換言之,這些具體演算法類均有統一的介面,根據 "里氏代換原則" 和麵向物件的多型性,客戶端可以選擇使用任何一個具體演算法類,並只需要維持一個資料型別是抽象演算法類的物件。

  • 一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重條件選擇語句來實現。此時,使用策略模式,把這些行為轉移到相應的具體策略類裡面,就可以避免使用難以維護的多重條件選擇語句。

  • 不希望客戶端知道複雜的、與演算法相關的資料結構,在具體策略類中封裝演算法與相關的資料結構,可以提高演算法的保密性與安全性。

原始碼分析策略模式的典型應用

Java Comparator 中的策略模式

java.util.Comparator 介面是比較器介面,可以通過 Collections.sort(List,Comparator)Arrays.sort(Object[],Comparator) 對集合和資料進行排序,下面為示例程式

一個學生類,有兩個屬性 idname

@Data
@AllArgsConstructor
public class Student {
    private Integer id;
    private String name;

    @Override
    public String toString() {
        return "{id=" + id + ", name='" + name + "'}";
    }
}
複製程式碼

實現兩個比較器,比較器實現了 Comparator 介面,一個升序,一個降序

// 降序
public class DescSortor implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o2.getId() - o1.getId();
    }
}

// 升序
public class AscSortor implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getId() - o2.getId();
    }
}
複製程式碼

通過 Arrays.sort() 對陣列進行排序

public class Test1 {
    public static void main(String[] args) {
        Student[] students = {
                new Student(3, "張三"),
                new Student(1, "李四"),
                new Student(4, "王五"),
                new Student(2, "趙六")
        };
        toString(students, "排序前");
        
        Arrays.sort(students, new AscSortor());
        toString(students, "升序後");
        
        Arrays.sort(students, new DescSortor());
        toString(students, "降序後");
    }

    public static void toString(Student[] students, String desc){
        for (int i = 0; i < students.length; i++) {
            System.out.print(desc + ": " +students[i].toString() + ", ");
        }
        System.out.println();
    }
}
複製程式碼

輸出

排序前: {id=3, name='張三'}, 排序前: {id=1, name='李四'}, 排序前: {id=4, name='王五'}, 排序前: {id=2, name='趙六'}, 
升序後: {id=1, name='李四'}, 升序後: {id=2, name='趙六'}, 升序後: {id=3, name='張三'}, 升序後: {id=4, name='王五'}, 
降序後: {id=4, name='王五'}, 降序後: {id=3, name='張三'}, 降序後: {id=2, name='趙六'}, 降序後: {id=1, name='李四'}, 
複製程式碼

通過 Collections.sort() 對集合List進行排序

public class Test2 {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(3, "張三"),
                new Student(1, "李四"),
                new Student(4, "王五"),
                new Student(2, "趙六")
        );
        toString(students, "排序前");
        
        Collections.sort(students, new AscSortor());
        toString(students, "升序後");

        Collections.sort(students, new DescSortor());
        toString(students, "降序後");
    }

    public static void toString(List<Student> students, String desc) {
        for (Student student : students) {
            System.out.print(desc + ": " + student.toString() + ", ");
        }
        System.out.println();
    }
}
複製程式碼

輸出

排序前: {id=3, name='張三'}, 排序前: {id=1, name='李四'}, 排序前: {id=4, name='王五'}, 排序前: {id=2, name='趙六'}, 
升序後: {id=1, name='李四'}, 升序後: {id=2, name='趙六'}, 升序後: {id=3, name='張三'}, 升序後: {id=4, name='王五'}, 
降序後: {id=4, name='王五'}, 降序後: {id=3, name='張三'}, 降序後: {id=2, name='趙六'}, 降序後: {id=1, name='李四'}, 
複製程式碼

我們向 Collections.sort()Arrays.sort() 分別傳入不同的比較器即可實現不同的排序效果(升序或降序)

這裡 Comparator 介面充當了抽象策略角色,兩個比較器 DescSortorAscSortor 則充當了具體策略角色,CollectionsArrays 則是環境角色

Spring Resource 中的策略模式

Spring 把所有能記錄資訊的載體,如各種型別的檔案、二進位制流等都稱為資源,譬如最常用的Spring配置檔案。

在 Sun 所提供的標準 API 裡,資源訪問通常由 java.NET.URL 和檔案 IO 來完成,尤其是當我們需要訪問來自網路的資源時,通常會選擇 URL 類。

URL 類可以處理一些常規的資源訪問問題,但依然不能很好地滿足所有底層資源訪問的需要,比如,暫時還無法從類載入路徑、或相對於 ServletContext 的路徑來訪問資源,雖然 Java 允許使用特定的 URL 字首註冊新的處理類(例如已有的 http: 字首的處理類),但是這樣做通常比較複雜,而且 URL 介面還缺少一些有用的功能,比如檢查所指向的資源是否存在等。

Spring 改進了 Java 資源訪問的策略,Spring 為資源訪問提供了一個 Resource 介面,該介面提供了更強的資源訪問能力,Spring 框架本身大量使用了 Resource 介面來訪問底層資源。

public interface Resource extends InputStreamSource {
    boolean exists();    // 返回 Resource 所指向的資源是否存在
    boolean isReadable();   // 資源內容是否可讀
    boolean isOpen();   // 返回資原始檔是否開啟
    URL getURL() throws IOException;
    URI getURI() throws IOException;
    File getFile() throws IOException;  // 返回資源對應的 File 物件
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String var1) throws IOException;
    String getFilename();
    String getDescription();    // 返回資源的描述資訊
}
複製程式碼

Resource 介面是 Spring 資源訪問策略的抽象,它本身並不提供任何資源訪問實現,具體的資源訪問由該介面的實現類完成——每個實現類代表一種資源訪問策略

Spring資源訪問介面Resource的實現類

Spring 為 Resource 介面提供的部分實現類如下:

  • UrlResource:訪問網路資源的實現類。
  • ClassPathResource:訪問類載入路徑裡資源的實現類。
  • FileSystemResource:訪問檔案系統裡資源的實現類。
  • ServletContextResource:訪問相對於 ServletContext 路徑裡的資源的實現類:
  • InputStreamResource:訪問輸入流資源的實現類。
  • ByteArrayResource:訪問位元組陣列資源的實現類。
  • WritableResource:寫資原始檔

這些 Resource 實現類,針對不同的的底層資源,提供了相應的資源訪問邏輯,並提供便捷的包裝,以利於客戶端程式的資源訪問。

它們之間的類關係如下所示:

Spring Resource 類圖

可以看到 AbstractResource 資源抽象類實現了 Resource 介面,為子類通用的操作提供了具體實現,非通用的操作留給子類實現,所以這裡也應用了模板方法模式。(只不過缺少了模板方法)

Resource 不僅可在 Spring 的專案中使用,也可直接作為資源訪問的工具類使用。意思是說:即使不使用 Spring 框架,也可以使用 Resource 作為工具類,用來代替 URL

譬如我們可以使用 UrlResource 訪問網路資源。

也可以通過其它協議訪問資源,file: 用於訪問檔案系統;http: 用於通過 HTTP 協議訪問資源;ftp: 用於通過 FTP 協議訪問資源等

public class Test {
    public static void main(String[] args) throws IOException {
        UrlResource ur = new UrlResource("http://image.laijianfeng.org/hello.txt");

        System.out.println("檔名:" + ur.getFilename());
        System.out.println("網路檔案URL:" + ur.getURL());
        System.out.println("是否存在:" + ur.exists());
        System.out.println("是否可讀:" + ur.isReadable());
        System.out.println("檔案長度:" + ur.contentLength());

        System.out.println("\n--------檔案內容----------\n");
        byte[] bytes = new byte[47];
        ur.getInputStream().read(bytes);
        System.out.println(new String(bytes));
    }
}
複製程式碼

輸出的內容如下,符合預期

檔名:hello.txt
網路檔案URL:http://image.laijianfeng.org/hello.txt
是否存在:true
是否可讀:true
檔案長度:47

--------檔案內容----------

hello world!
welcome to http://laijianfeng.org
複製程式碼

更多的示例可以參考:Spring 資源訪問剖析和策略模式應用

Spring Bean 例項化中的策略模式

Spring例項化Bean有三種方式:構造器例項化、靜態工廠例項化、例項工廠例項化

譬如通過構造器例項化bean的XML示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="person" class="com.demo.Person"></bean>
    
    <bean id="personWithParam" class="com.demo.Person">
        <constructor-arg name="name" value="小旋鋒"/>
    </bean>
    
    <bean id="personWirhParams" class="com.demo.Person">
            <constructor-arg name="name" value="小旋鋒"/>
            <constructor-arg name="age" value="22"/>
    </bean>
</beans>
複製程式碼

具體例項化Bean的過程中,Spring中角色分工很明確,建立物件的時候先通過 ConstructorResolver 找到對應的例項化方法和引數,再通過例項化策略 InstantiationStrategy 進行例項化,根據建立物件的三個分支( 工廠方法、有參構造方法、無參構造方法 ), InstantiationStrategy 提供了三個介面方法:

public interface InstantiationStrategy {
	// 預設構造方法
	Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) throws BeansException;

	// 指定構造方法
	Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner, Constructor<?> ctor,
			Object[] args) throws BeansException;

	// 指定工廠方法
	Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner, Object factoryBean,
			Method factoryMethod, Object[] args) throws BeansException;
}
複製程式碼

InstantiationStrategy 為例項化策略介面,扮演抽象策略角色,有兩種具體策略類,分別為 SimpleInstantiationStrategyCglibSubclassingInstantiationStrategy

Spring 例項化策略類圖

SimpleInstantiationStrategy 中對這三個方法做了簡單實現,如果工廠方法例項化直接用反射建立物件,如果是構造方法例項化的則判斷是否有 MethodOverrides,如果有無 MethodOverrides 也是直接用反射,如果有 MethodOverrides 就需要用 cglib 例項化物件,SimpleInstantiationStrategy 把通過 cglib 例項化的任務交給了它的子類 CglibSubclassingInstantiationStrategy

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析
Spring 資源訪問剖析和策略模式應用
Spring原始碼閱讀-例項化策略InstantiationStrategy
Spring學習之例項化bean的三種方式

相關文章