設計模式 策略模式(strategy Pattern)詳細解讀

一、設計思想演進

1.1 預想場景

我們有一個會員業務,在售賣產品時價格是根據會員的類型來進行折扣,比如:普通會員不打折,VIP會員打8折,超級VIP會員7折。
現在做一個價格模塊的折扣計算功能,針對不同會員類型,進行折扣計算,代碼如下:

public class PriceCenter {
    /**
     * 對原價進行折扣計算
     * @param price 原價
     * @param memberType 會員類型
     * @return 計算折扣後價格
     */
    public BigDecimal discount(BigDecimal price, String memberType){
        if ("普通會員".equals(memberType)) {
            System.out.println("普通會員沒有折扣");
        }else if ("VIP會員".equals(memberType)) {
            System.out.println("vip會員打9折!");
            price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
        }else if("超級VIP會員".equals(memberType)){
            System.out.println("超級VIP會員打5折!");
            price = price.multiply(new BigDecimal("0.7")).setScale(2,RoundingMode.HALF_UP);
        }
        return price;
    }
}

一開始代碼邏輯很簡單,但我們把每種會員的折扣算法都放在了一個方法裏,隨着業務的增加代碼會越來越多,會十分臃腫。

1.2 優化

下面我們進行優化,把每種會員的折扣算法單獨封裝成一個方法,代碼如下:

public class PriceCenter {
    /**
     * 對原價進行折扣計算
     *
     * @param price      原價
     * @param memberType 會員類型
     * @return 計算折扣後價格
     */
    public BigDecimal discount(BigDecimal price, String memberType) {
        if ("普通會員".equals(memberType)) {
            price = memberNormalDiscount(price);
        } else if ("VIP會員".equals(memberType)) {
            price = memberVIPDiscount(price);
        } else if ("超級VIP會員".equals(memberType)) {
            price = memberSuperVIPDiscount(price);
        }
        return price;
    }

    private BigDecimal memberNormalDiscount(BigDecimal price) {
        System.out.println("普通會員沒有折扣");
        return price;
    }

    private BigDecimal memberVIPDiscount(BigDecimal price) {
        System.out.println("vip會員打9折!");
        price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
        return price;
    }

    private BigDecimal memberSuperVIPDiscount(BigDecimal price) {
        System.out.println("超級VIP會員打8折!");
        price = price.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_UP);
        return price;
    }
}

當我們想要修改某個算法的邏輯時只要,修改單獨的方法就可以了。
上面看似讓代碼變得優雅了,但還是存在一些問題:
問題一:每新增一個會員類型的時候,首先要添加一個該種客戶類型的報價算法方法,然後在方法discount中再加一個else if的分支,這違反了設計原則之一的開閉原則(open-closed-principle).

開閉原則:
對於擴展是開放的(Open for extension)。這意味着模塊的行爲是可以擴展的。當應用的需求改變時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行爲。也就是說,我們可以改變模塊的功能。
對於修改是關閉的(Closed for modification)。對模塊行爲進行擴展時,不必改動模塊的源代碼或者二進制代碼。

問題二:有的時候我們需要做額外的活動針對不同的節日,每個else if中的邏輯要做很大改動,活動過去之後又要恢復,如果剛入職的新手就會想直接改動原來else if種代碼,活動過去之後在改動回去,這樣其實很不好。

思考:每次執行discount方法時其實只會執行其中一個分支的代碼,是否有什麼辦法可以每次動態變化方法中的內容,於是我們就想到是否可以執行的時候調用固定的類的方法,具體的執行邏輯是否可以通過當一個參數傳到這個類中。

爲了解決上面的問題,這個時候策略模式就應運而生了

二、什麼是策略設計模式?

2.1 概念

其思想是針對一組算法,將每一種算法都封裝到具有共同接口的獨立的類中,從而是它們可以相互替換。策略模式可以讓算法在不影響客戶端的情況下發生變化。

2.2 類型

行爲型設計模式

2.3 UML結構

在這裏插入圖片描述

2.4 三類角色

  • 環境(Context):也叫上下文,它持有一個Strategy的引用。
  • 抽象策略(Strategy):這是一個抽象角色,通常由一個接口或抽象類實現。用來約束一系列具體的策略。
  • 具體策略(ConcreteStrategy):具體的策略實現。策略即算法。

2.5 一般通用實現*

策略接口:

//策略接口
public interface IStrategy {
    //定義的抽象算法方法 來約束具體的算法實現方法
    public void algorithmMethod();
}

具體的策略實現:

// 具體的策略實現A
public class ConcreteStrategyA implements IStrategy {
    //具體的算法實現
    @Override
    public void algorithmMethod() {
        System.out.println("我是具體的算法A");
    }
}
// 具體的策略實現B
public class ConcreteStrategyB implements IStrategy {
    //具體的算法實現
    @Override
    public void algorithmMethod() {
        System.out.println("我是具體的算法B");
    }
}

策略上下文

/**
 * 策略上下文
 */
public class StrategyContext {
    //持有一個策略實現的引用
    private IStrategy strategy;
    //使用構造器注入具體的策略類
    public StrategyContext(IStrategy strategy) {
        this.strategy = strategy;
    }

    //也可以通過set方法注入具體策略
    public void setStrategy(IStrategy strategy) {
        this.strategy = strategy;
    }

    public void contextMethod(){
        //調用策略實現的方法
        strategy.algorithmMethod();
    }
}

三、根據設計模式改造業務

折扣策略接口

public interface IDiscountStrategy {
    BigDecimal discount(BigDecimal price);
}

普通會員折扣策略

public class MemberNormalDiscount implements IDiscountStrategy {
    @Override
    public BigDecimal discount(BigDecimal price) {
        System.out.println("普通會員沒有折扣");
        return price;
    }
}

VIP會員折扣策略

public class MemberVIPDiscount implements IDiscountStrategy {
    @Override
    public BigDecimal discount(BigDecimal price) {
        System.out.println("vip會員打9折!");
        price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
        return price;
    }
}

超級VIP會員折扣策略

public class MemberSuperVIPDiscount implements IDiscountStrategy {
    @Override
    public BigDecimal discount(BigDecimal price) {
        System.out.println("超級VIP會員打8折!");
        price = price.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_UP);
        return price;
    }
}

折扣上下文

public class DiscountContext {

    private IDiscountStrategy discountStrategy;

    public DiscountContext(IDiscountStrategy strategy) {
        this.discountStrategy = strategy;
    }

    public void setDiscountStrategy(IDiscountStrategy iDiscountStrategy) {
        this.discountStrategy = iDiscountStrategy;
    }

    public BigDecimal discount(BigDecimal price){
        return discountStrategy.discount(price);
    }
}

客戶端

public class Client {
    public static void main(String[] args) {

        BigDecimal price1 = new BigDecimal(1000);
        MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
        DiscountContext discountContext = new DiscountContext(memberNormalDiscount);
        price1 = discountContext.discount(price1);
        System.out.println(price1);

        BigDecimal price2 = new BigDecimal(1000);
        discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
        price2 = discountContext.discount(price2);
        System.out.println(price2);
    }
}

同時測試兩種算法,結果如下:

普通會員沒有折扣
1000
超級VIP會員打8折!
800.00

下面我們來想一想,如果這時候推出了一個員工內部會員,買東西可享5折怎麼實現?
我們只要新增一個內部會員折扣策略就可以了,代碼如下:

public class MemberInnerDiscount implements IDiscountStrategy {
    @Override
    public BigDecimal discount(BigDecimal price) {
        System.out.println("內部會員打5折!");
        price = price.multiply(new BigDecimal("0.5")).setScale(2, RoundingMode.HALF_UP);
        return price;
    }
}
public class Client {
    public static void main(String[] args) {
        BigDecimal price1 = new BigDecimal(1000);
        MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
        DiscountContext discountContext = new DiscountContext(memberNormalDiscount);
        price1 = discountContext.discount(price1);
        System.out.println(price1);

        BigDecimal price2 = new BigDecimal(1000);
        discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
        price2 = discountContext.discount(price2);
        System.out.println(price2);

        BigDecimal price3 = new BigDecimal(1000);
        discountContext.setDiscountStrategy(new MemberInnerDiscount());
        price3 = discountContext.discount(price3);
        System.out.println(price3);
    }
}

四、if else如何解決?

在策略模式使用過程中我們發現,策略模式其實是通過context來解決執行具體邏輯可以根據需要隨時變化,也就是需要什麼算法就讓context持有。但客戶端中if else並沒有減少
我們將上面代碼改造的更貼近一些現實情況

public class Client {
    public static void main(String[] args) {
        //會員類型
        String memberType = "VIP";
        //原價
        BigDecimal price = new BigDecimal(1000);
        DiscountContext discountContext = new DiscountContext();
        if ("normal".equals(memberType)) {
            MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
            discountContext.setDiscountStrategy(new MemberNormalDiscount());
            price = discountContext.discount(price);
        } else if ("VIP".equals(memberType)) {
            BigDecimal price2 = new BigDecimal(1000);
            discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
            price = discountContext.discount(price);
        }else if ("superVIP".equals(memberType)) {
            discountContext.setDiscountStrategy(new MemberInnerDiscount());
            price = discountContext.discount(price);
        }
        System.out.println(price);
    }
}

4.1使用工廠方法

工廠方法

/**
 * 會員折扣策略工廠
 */
public class MemberDiscountFactory {
    private static final Map<String, IDiscountStrategy> map = new HashMap<>();
    static {
        map.put("normal", new MemberNormalDiscount());
        map.put("VIP", new MemberVIPDiscount());
        map.put("superVIP", new MemberSuperVIPDiscount());
    }
    public static IDiscountStrategy getDiscountStrategy(String memberType) {
        return map.get(memberType);
    }
}

客戶端

public class Client1 {
    public static void main(String[] args) {
        //這裏使用最簡單方式表示類型
        String memberType = "VIP";
        //原價
        BigDecimal price = new BigDecimal(1000);
        //使用工廠獲取具體策略
        IDiscountStrategy iDiscountStrategy = MemberDiscountFactory.getDiscountStrategy(memberType);
        DiscountContext discountContext = new DiscountContext(iDiscountStrategy);
        BigDecimal result = discountContext.discount(price);
        System.out.println("打折後的價格爲"+result);
    }
}

發現代碼優化後簡潔了很多,工廠方法只是一種解決方式,其主要的思路就是將各個策略放到容器中去來方便獲取。大家可以發散思路,比如枚舉,反射等等

思考:策略的選擇我們交給了客戶端,及時使用了工廠方法對客戶端也會覺得過於麻煩,我們是否可以將具體選擇哪些策略的權利交給Context上下文,這樣在折扣選擇的部分也可以徹底和客戶端解耦

五、Context在策略模式中的作用

5.1 作用: 讓客戶端和具體策略完全解耦

在沒有context的情況下, client直接持有策略接口, 如果具體策略改變, 需要更改client的代碼, 但是如果讓context持有IStrategy
那就不需要更改client

client在整個處理流程中只要知道他調了一個策略就行了, 不必要知道具體策略是什麼, 甚至策略的選擇都讓策略上下文去維護。
有很多策略模式的實現, 都是在context中維護了strategy list, 然後在裏面通過參數去選取對應的策略, 這塊"選取對應策略"放在context中比放在client中更合適

5.2 再次改造

Context上下文

public class DiscountContext1 {

    String memberType;

    private IDiscountStrategy discountStrategy;

    public DiscountContext1(String memberType) {
        this.memberType = memberType;
        this.discountStrategy = MemberDiscountFactory.getMedalService(memberType);
    }

    public String getMemberType() {
        return memberType;
    }

    public void setMemberType(String memberType) {
        this.memberType = memberType;
    }

    public IDiscountStrategy getDiscountStrategy() {
        return discountStrategy;
    }

    public BigDecimal discount(BigDecimal price){
        return discountStrategy.discount(price);
    }
}

客戶端

public class Client2 {
    public static void main(String[] args) {
        String memberType = "VIP";
        //原價
        BigDecimal price = new BigDecimal(1000);
        DiscountContext1 discountContext = new DiscountContext1(memberType);
        BigDecimal result = discountContext.discount(price);
        System.out.println("打折後的價格爲"+result);
    }
}

5.3 上下文當做參數傳入到具體策略中

具體的策略對象可以從上下文中獲取所需數據,可以將上下文當做參數傳入到具體策略中,具體策略通過回調上下文中的方法來獲取其所需要的數據。
**假設情景:**要求每個策略能展示會員類型,而且支持以後一些公用字段新增
修改策略接口

public interface IDiscountStrategy1 {
    BigDecimal discount(DiscountContext2 discountContext2);
}

修改一個具體策略

public class MemberNormalDiscount1 implements IDiscountStrategy1 {
    @Override
    public BigDecimal discount(DiscountContext2 discountContext2) {
        String memberType = discountContext2.getMemberType();
        BigDecimal price = discountContext2.getPrice();
        System.out.println(memberType + "沒有折扣");
        return price;
    }
}

修改Factory

public class MemberDiscountFactory1 {
    private static final Map<String, IDiscountStrategy1> map = new HashMap<>();
    static {
        map.put("normal", new MemberNormalDiscount1());
    }
    public static IDiscountStrategy1 getDiscountStrategy(String memberType) {
        return map.get(memberType);
    }
}

修改Context

public class DiscountContext2 {

    private String memberType;

    private BigDecimal price;

    private IDiscountStrategy1 discountStrategy;

    public DiscountContext2(String memberType) {
        this.memberType = memberType;
        this.discountStrategy = MemberDiscountFactory1.getDiscountStrategy(memberType);
    }

    public String getMemberType() {
        return memberType;
    }

    public void setMemberType(String memberType) {
        this.memberType = memberType;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public IDiscountStrategy1 getDiscountStrategy() {
        return discountStrategy;
    }

    public BigDecimal discount(BigDecimal price){
        this.price = price;
        return discountStrategy.discount(this);
    }
}

client

public class Client3 {
    public static void main(String[] args) {
        //這裏使用最簡單方式表示類型
        String memberType = "VIP";
        //原價
        BigDecimal price = new BigDecimal(1000);
        DiscountContext2 discountContext = new DiscountContext2(memberType);
        BigDecimal result = discountContext.discount(price);
        System.out.println("打折後的價格爲"+result);
    }
}

測試結果

normal沒有折扣
打折後的價格爲1000

六、策略模式在JDK中有哪些應用?

6.1 比較器Comparator

在Java的集合框架中,經常需要通過構造方法傳入一個比較器Comparator,或者創建比較器傳入Collections的靜態方法中作爲方法參數,進行比較排序等,使用的是策略模式。

在該比較架構中,Comparator就是一個抽象的策略;一個類實現該結構,並實現裏面的compare方法,該類成爲具體策略類;Collections類就是環境角色,他將集合的比較封裝成靜態方法對外提供api。

6.2 ThreadPoolExecutor中的四種拒絕策略

在創建線程池時,需要傳入拒絕策略,當創建新線程使當前運行的線程數超過maximumPoolSize時,將會使用傳入的拒絕策略進行處理。

  • AbortPolicy:直接拋出異常。
  • CallerRunsPolicy:只用調用者所在線程來運行任務。
  • DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
  • DiscardPolicy:不處理,丟棄掉。

這裏使用的就是策略模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章