“策略模式”:告別if else

閱讀完本篇文章你將瞭解到什麼是策略模式,策略模式的優缺點,以及策略模式在源碼中的應用。

策略模式引入

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

譬如商場購物場景中,有些商品按原價賣,商場可能為瞭促銷而推出優惠活動,有些商品打九折,有些打八折,有些則是返現10元等。而優惠活動並不影響結算之外的其他過程,隻是在結算的時候需要根據優惠方案結算。

再比如不同的人出去旅遊出行的交通方式也不同,經濟條件好的會選擇高鐵飛機,而普通人可能會選擇綠皮火車。

在這裡插入圖片描述

富豪老王打算去西藏旅遊,老王定瞭豪華酒店,並且定瞭機票當天直達。而普通人老張也要去西藏旅遊,他打算選擇乘坐高鐵出行。而學生黨的我小汪肯定會選擇綠皮火車,主要是為瞭看路邊的風景,而不是因為窮。

下面我們用代碼來描述一下上訴場景:

public class Travel {
    private String vehicle;//出行方式
    private String name;

    public String getName() {
        return name;
    }

    public Travel(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setVehicle(String vehicle) {
        this.vehicle = vehicle;
    }

    public String getVehicle() {
        return vehicle;
    }

    public void TravelTool(){
        if(name.equals("小汪")){
            setVehicle("綠皮火車");
        }else if(name.equals("老張")){
            setVehicle("高鐵");
        }else if(name.equals("老王")){
            setVehicle("飛機");
        }
        System.out.println(name+"選擇坐"+getVehicle()+"去西藏旅遊");
    }

}

public class Test {
    public static void main(String[] args) {
        Travel travel1 = new Travel("小汪");
        Travel travel2 = new Travel("老王");
        Travel travel3 = new Travel("老張");
        travel1.TravelTool();
        travel2.TravelTool();
        travel3.TravelTool();

    }
}
小汪選擇坐綠皮火車去西藏旅遊
老王選擇坐飛機去西藏旅遊
老張選擇坐高鐵去西藏旅遊

以上代碼雖然完成瞭我們的需求,但是存在以下問題

Travel類的TravelTool方法非常龐大,它包含各種人的旅行實現代碼,在代碼中出現瞭較長的 if…else… 語句,假如日後小汪發達瞭也想體驗一下做飛機去西藏旅遊,那就要去修改TravelTool方法。違反瞭 “開閉原則”,系統的靈活性和可擴展性較差。

算法的復用性差,如果在另一個系統中需要重用某些算法,隻能通過對源代碼進行復制粘貼來重用,無法單獨重用其中的某個或某些算法。

策略模式

策略模式的介紹

策略模式(Strategy Pattern)中,定義算法族,分別封裝起來,讓他們之間可以互相替換,此模式讓算法的變化獨立於使用算法的客戶這算法體現瞭幾個設計原則,

第一、把變化的代碼從不變的代碼中分離出來;

第二、針對接口編程而不是具體類(定義瞭策略接口);

第三、多用組合/聚合,少用繼承(客戶通過組合方式使用策略)。

策略模式的原理類圖

在這裡插入圖片描述

角色分析

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

Strategy(抽象策略類):它為所支持的算法聲明瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是接口。環境類通過抽象策略類中聲明的方法在運行時調用具體策略類中實現的算法。

ConcreteStrategy(具體策略類):它實現瞭在抽象策略類中聲明的算法,在運行時,具體策略類將覆蓋在環境類中定義的抽象策略類對象,使用一種具體的算法實現某個業務處理。

我們下面用策略模式來改進一下上面旅行的代碼例子。

抽象策略類 Discount

public abstract class AbstractTravle {
    private String vehicle;
    private String name;

    public AbstractTravle(String vehicle, String name) {
        this.vehicle = vehicle;
        this.name = name;
    }

    public String getVehicle() {
        return vehicle;
    }

    public String getName() {
        return name;
    }

    public abstract void TravelTool();
}

ConcreteStrategy(具體策略類)

public class XiaoWang extends AbstractTravle{
    public XiaoWang(String vehicle, String name) {
        super(vehicle, name);
    }

    @Override
    public void TravelTool() {
        System.out.println(getName()+"選擇坐"+getVehicle()+"去西藏旅遊");
    }
}
public class LaoWang extends AbstractTravle{
    public LaoWang(String vehicle, String name) {
        super(vehicle, name);
    }

    @Override
    public void TravelTool() {
        System.out.println(getName()+"選擇坐"+getVehicle()+"去西藏旅遊");
    }
}
public class LaoZhang extends AbstractTravle{
    public LaoZhang(String vehicle, String name) {
        super(vehicle, name);
    }
    @Override
    public void TravelTool() {
        System.out.println(getName()+"選擇坐"+getVehicle()+"去西藏旅遊");
    }

}

環境類

public class Context {
    private AbstractTravle abstractTravle;

    public Context(AbstractTravle abstractTravle) {
        this.abstractTravle = abstractTravle;
    }
    public void TravelTool() {
        System.out.println(abstractTravle.getName()+"選擇坐"+abstractTravle.getVehicle()+"去西藏旅遊");
    }
}

策略模式總結

public class Test {
    public static void main(String[] args) {
        Context context1 = new Context(new LaoWang("飛機", "老王"));
        context1.TravelTool();
        Context context2 = new Context(new LaoZang("高鐵", "老張"));
        context2.TravelTool();
        Context context3 = new Context(new XiaoWang("綠皮火車", "小汪"));
        context3.TravelTool();
    }
}
老王選擇坐飛機去西藏旅遊
老張選擇坐高鐵去西藏旅遊
小汪選擇坐綠皮火車去西藏旅遊

策略模式的主要優點如下

1.模式提供瞭對 “開閉原則” 的完美支持,用戶可以在不修改原有系統的基礎上選擇算法或行為,也可以靈活地增加新的算法或行為。

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

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

4.模式可以避免多重條件選擇語句。多重條件選擇語句是硬編碼,不易維護。

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

策略模式的主要缺點如下

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

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

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

適用場景

1.系統需要動態地在幾種算法中選擇一種,那麼可以將這些算法封裝到一個個的具體算法類中,而這些具體算法類都是一個抽象算法類的子類。換言之,這些具體算法類均有統一的接口,根據 “裡氏代換原則” 和面向對象的多態性,客戶端可以選擇使用任何一個具體算法類,並隻需要維持一個數據類型是抽象算法類的對象。

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

3.望客戶端知道復雜的、與算法相關的數據結構,在具體策略類中封裝算法與相關的數據結構,可以提高算法的保密性與安全性。

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

Java Comparator 中的策略模式

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

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

    @Override
    public String toString() {
        return "{id=" + id + ", name='" + name + "'}";
    }
}
// 降序
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 接口充當瞭抽象策略角色,兩個比較器 DescSortor 和 AscSortor 則充當瞭具體策略角色,Collections 和 Arrays 則是環境角色

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 接口提供的部分實現類如下:

1.lResource:訪問網絡資源的實現類。

2.assPathResource:訪問類加載路徑裡資源的實現類。

3.leSystemResource:訪問文件系統裡資源的實現類。

4.rvletContextResource:訪問相對於ServletContext 路徑裡的資源的實現類:

5.putStreamResource:訪問輸入流資源的實現類。

6.teArrayResource:訪問字節數組資源的實現類。

7.itableResource:寫資源文件

類圖如下

在這裡插入圖片描述

 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 Bean 實例化中的策略模式

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

具體實例化Bean的過程中,Spring中角色分工很明確,創建對象的時候先通

<?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>

過 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 為實例化策略接口,扮演抽象策略角色,有兩種具體策略類,分別為

SimpleInstantiationStrategy CglibSubclassingInstantiationStrategy

在這裡插入圖片描述

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

總結

1、略類之間可以自由切換,由於策略類實現自同一個抽象,所以他們之間可以自由切換。

2、於擴展,增加一個新的策略對策略模式來說非常容易,基本上可以在不改變原有代碼的基礎上進行擴展。

3、使用多重條件,如果不使用策略模式,對於所有的算法,必須使用條件語句進行連接,通過條件判斷來決定使用哪一種算法,在上一篇文章中我們已經提到,使用多重條件判斷是非常不容易維護的。

以上就是java策略模式的詳細內內容。也請關註WalkonNet其它相關文章!

推薦閱讀: