更優雅地實現策略模式

寒煙濡雨發表於2022-03-28

一、為什麼講策略模式

策略模式,應該是工作中比較常用的設計模式,呼叫方自己選擇用哪一種策略完成對資料的操作,也就是“一個類的行為或其演算法可以在執行時更改”

我個人的理解是 將一些除了過程不同其他都一樣的函式封裝成策略,然後呼叫方自己去選擇想讓資料執行什麼過程策略。常見的例子為根據使用者分類推薦不同的排行榜(使用者關注點不一樣,推薦榜單就不一樣)

單例模式一樣,隨著時間發展,我不再推薦經典策略模式,更推薦簡單策略用列舉策略模式,複雜地用工廠策略模式。下面引入一個例子,我們的需求是:對一份股票資料列表,給出低價榜、高價榜、漲幅榜。這其中只有排序條件的區別,比較適合作為策略模式的例子

二、經典策略模式

資料DTO

@Data
public class Stock {

    // 股票交易程式碼
    private String code;

    // 現價
    private Double price;

    // 漲幅
    private Double rise;
}

抽象得到的策略介面

public interface Strategy {

    /**
     * 將股票列表排序
     *
     * @param source 源資料
     * @return 排序後的榜單
     */
    List<Stock> sort(List<Stock> source);
}

實現我們的策略類

/**
 * 高價榜
 */
public class HighPriceRank implements Strategy {

    @Override
    public List<Stock> sort(List<Stock> source) {
        return source.stream()
                .sorted(Comparator.comparing(Stock::getPrice).reversed())
                .collect(Collectors.toList());
    }
}

/**
 * 低價榜
 */
public class LowPriceRank implements Strategy {

    @Override
    public List<Stock> sort(List<Stock> source) {
        return source.stream()
                .sorted(Comparator.comparing(Stock::getPrice))
                .collect(Collectors.toList());
    }
}

/**
 * 高漲幅榜
 */
public class HighRiseRank implements Strategy {

    @Override
    public List<Stock> sort(List<Stock> source) {
        return source.stream()
                .sorted(Comparator.comparing(Stock::getRise).reversed())
                .collect(Collectors.toList());
    }
}

經典的Context類,

public class Context {
    private Strategy strategy;
    
    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public List<Stock> getRank(List<Stock> source) {
        return strategy.sort(source);
    }
}

於是 我們順禮成章地得到呼叫類--榜單例項RankServiceImpl

@Service
public class RankServiceImpl {

    /**
     * dataService.getSource() 提供原始的股票資料
     */
    @Resource
    private DataService dataService;

    /**
     * 前端傳入榜單型別, 返回排序完的榜單
     *
     * @param rankType 榜單型別
     * @return 榜單資料
     */
    public List<Stock> getRank(String rankType) {
        // 建立上下文
        Context context = new Context();
        // 這裡選擇策略
        switch (rankType) {
            case "HighPrice":
                context.setStrategy(new HighPriceRank());
                break;
            case "LowPrice":
                context.setStrategy(new LowPriceRank());
                break;
            case "HighRise":
                context.setStrategy(new HighRiseRank());
                break;
            default:
                throw new IllegalArgumentException("rankType not found");
        }
        // 然後執行策略
        return context.getRank(dataService.getSource());
    }
}

我們可以看到經典方法,建立了一個介面、三個策略類,還是比較囉嗦的。呼叫類的實現也待商榷,新增一個策略類還要修改榜單例項(可以用抽象工廠解決,但是複雜度又上升了)。加之我們有更好的選擇,所以此處不再推薦經典策略模式

三、基於列舉的策略模式

這裡對這種簡單的策略,推薦用列舉進行優化。列舉的本質是建立了一些靜態類的集合。

我下面直接給出例子,大家可以直觀感受一下

列舉策略類

public enum RankEnum {
    // 以下三個為策略例項
    HighPrice {
        @Override
        public List<Stock> sort(List<Stock> source) {
            return source.stream()
                    .sorted(Comparator.comparing(Stock::getPrice).reversed())
                    .collect(Collectors.toList());
        }
    },
    LowPrice {
        @Override
        public List<Stock> sort(List<Stock> source) {
            return source.stream()
                    .sorted(Comparator.comparing(Stock::getPrice))
                    .collect(Collectors.toList());
        }
    },
    HighRise {
        @Override
        public List<Stock> sort(List<Stock> source) {
            return source.stream()
                    .sorted(Comparator.comparing(Stock::getRise).reversed())
                    .collect(Collectors.toList());
        }
    };

    // 這裡定義了策略介面
    public abstract List<Stock> sort(List<Stock> source);
}

對應的呼叫類也得以優化,榜單例項RankServiceImpl

@Service
public class RankServiceImpl {

    /**
     * dataService.getSource() 提供原始的股票資料
     */
    @Resource
    private DataService dataService;

    /**
     * 前端傳入榜單型別, 返回排序完的榜單
     *
     * @param rankType 榜單型別 形似 RankEnum.HighPrice.name()
     * @return 榜單資料
     */
    public List<Stock> getRank(String rankType) {
        // 獲取策略,這裡如果未匹配會拋 IllegalArgumentException異常
        RankEnum rank = RankEnum.valueOf(rankType);
        // 然後執行策略
        return rank.sort(dataService.getSource());
    }
}

可以看到,如果策略簡單的話,基於列舉的策略模式優雅許多,呼叫方也做到了0修改,但正確地使用列舉策略模式需要額外考慮以下幾點。

  1. 列舉的策略類是公用且靜態,這意味著這個策略過程不能引入非靜態的部分,擴充套件性受限
  2. 策略模式的目標之一,是優秀的擴充套件性和可維護性,最好能新增或修改某一策略類時,對其他類是無改動的。而列舉策略如果過多或者過程複雜,維護是比較困難的,可維護性受限

四、基於工廠的策略模式

為了解決良好的擴充套件性和可維護性,我更推薦以下利用spring自帶beanFactory的優勢,實現一個基於工廠的策略模式。

策略類改動只是新增了@Service註解,並指定了Service的value屬性

/**
 * 高價榜
 * 注意申明 Service.value = HighPrice,他是我們的key,下同
 */
@Service("HighPrice")
public class HighPriceRank implements Strategy {

    @Override
    public List<Stock> sort(List<Stock> source) {
        return source.stream()
                .sorted(Comparator.comparing(Stock::getPrice).reversed())
                .collect(Collectors.toList());
    }
}

/**
 * 低價榜
 */
@Service("LowPrice")
public class LowPriceRank implements Strategy {

    @Override
    public List<Stock> sort(List<Stock> source) {
        return source.stream()
                .sorted(Comparator.comparing(Stock::getPrice))
                .collect(Collectors.toList());
    }
}

/**
 * 高漲幅榜
 */
@Service("HighRise")
public class HighRiseRank implements Strategy {

    @Override
    public List<Stock> sort(List<Stock> source) {
        return source.stream()
                .sorted(Comparator.comparing(Stock::getRise).reversed())
                .collect(Collectors.toList());
    }
}

呼叫類修改較大,接入藉助spring工廠特性,完成策略類

@Service
public class RankServiceImpl {

    /**
     * dataService.getSource() 提供原始的股票資料
     */
    @Resource
    private DataService dataService;
    /**
     * 利用註解@Resource和@Autowired特性,直接獲取所有策略類
     * key = @Service的value
     */
    @Resource
    private Map<String, Strategy> rankMap;

    /**
     * 前端傳入榜單型別, 返回排序完的榜單
     *
     * @param rankType 榜單型別 和Service註解的value屬性一致
     * @return 榜單資料
     */
    public List<Stock> getRank(String rankType) {
        // 判斷策略是否存在
        if (!rankMap.containsKey(rankType)) {
            throw new IllegalArgumentException("rankType not found");
        }
        // 獲得策略例項
        Strategy rank = rankMap.get(rankType);
        // 執行策略
        return rank.sort(dataService.getSource());
    }
}

若讀者使用的不是Spring,也可以找找對應框架的工廠模式實現,或者自己實現一個抽象工廠

工廠策略模式會比列舉策略模式囉嗦,但也更加靈活、易擴充套件性和易維護。故簡單策略推薦列舉策略模式,複雜策略才推薦工廠策略模式

相關文章