策略模式、策略模式與Spring的碰撞

CodeBear發表於2020-06-30

策略模式是GoF23種設計模式中比較簡單的了,也是常用的設計模式之一,今天我們就來看看策略模式。

實際案例

我工作第三年的時候,重構旅遊路線的機票查詢模組,旅遊路線分為四種情況:

  • 如果A地-B地往返都可以直達,那麼查詢兩張機票(往返)
  • 如果A地-B地去程無法直達,需要中轉,但是返程可以直達,那麼查詢三張機票(去程兩張,返程一張)
  • 如果A地-B地去程可以直達,但是返程需要中轉,那麼查詢三張機票(去程一張,返程兩張)
  • 如果A地-B地往返都無法直達,那麼查詢四張機票(去程兩張,返程兩張)

在我重構前,程式碼差不多是這樣的:

        int type = 1;
        // 往返都可以直達
        if (type == 1) {
            // 查詢出兩張機票
            return;
        }

        // 去程無法直達,需要中轉,但是返程可以直達
        if (type == 2) {
            // 查詢出三張機票(去程兩張,返程一張)
            return;
        }
        // 去程可以直達,但是返程需要中轉
        if (type == 3) {
            // 查詢出三張機票(去程一張,返程兩張)
            return;
        }
        // 往返都無法直達
        else{
            // 查詢出四張機票(去程兩張,返程兩張)
            return;
        }

當時我還是菜雞(現在也是),也不懂什麼設計模式,就是感覺程式碼都寫在一個類中,實在是太長了,不夠清爽,不管是哪種型別的線路,最終都是返回機票集合,只是處理邏輯不同,可以提取一個介面出來,再開四個類去實現此介面,最後定義一個Map,Key是Type,Value是介面(實現類),根據Type決定呼叫哪個實現類,就像下面的醬紫:

public class Ticket {
    private String desc;

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

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "Ticket{" +
                "desc='" + desc + '\'' +
                '}';
    }
}
public interface QueryTicketService {
    List<Ticket> getTicketList();
}
public class QueryTicketAService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程機票"));
        list.add(new Ticket("返程機票"));
        return list;
    }
}
public class QueryTicketBService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一張機票"));
        list.add(new Ticket("去程第二張機票"));
        list.add(new Ticket("返程機票"));
        return list;
    }
}
public class QueryTicketCService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程機票"));
        list.add(new Ticket("返程第一張機票"));
        list.add(new Ticket("返程第二張機票"));
        return list;
    }
}
public class QueryTicketDService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一張機票"));
        list.add(new Ticket("去程第二張機票"));
        list.add(new Ticket("返程第一張機票"));
        list.add(new Ticket("返程第二張機票"));
        return list;
    }
}
public class Main {
    static Map<Integer, QueryTicketService> map = new HashMap<>();

    static {
        map.put(1, new QueryTicketAService());
        map.put(2, new QueryTicketBService());
        map.put(3, new QueryTicketCService());
        map.put(4, new QueryTicketDService());
    }

    public static void main(String[] args) {
        int type = 1;
        System.out.println(map.get(type).getTicketList());
    }
}

執行結果:

[Ticket{desc='去程機票'}, Ticket{desc='返程機票'}]

當初我也不知道什麼設計模式,就是感覺這樣寫完,程式碼清爽多了,後來才知道這就是策略模式的雛形了。

GoF23種設計模式真正應用廣泛的設計模式不多,但是策略模式絕對算其中之一了,你看,當初我都不懂這些,就寫出了策略模式的雛形。

原始的策略模式

如果我們遇到類似於上面的需求,第一反應肯定是用if else語句或者switch語句,根據不同的情況執行不同的程式碼,這樣做也沒什麼大問題,但是我們的專案會越來越複雜,這麼做的缺陷就慢慢的顯現了出來:如果現線上路新增了一個型別,需要中轉兩次,就又得加好幾個判斷的分支(去程中轉一次,返程中轉兩次;去程中轉兩次,返程中轉一次;去程直達,返程中轉兩次等等),想想就恐怖,這樣分支會越來越多,程式碼會越來越長,越來越難以維護,所以策略模式出現了。

當一個邏輯中,有很多if else語句或者switch語句,而且它們需要解決的問題是一樣的,就可以考慮策略模式。

最原始的策略模式有三個角色:

  • Strategy:抽象策略角色,對演算法、策略的抽象,定義每個演算法、策略所必需的方法,通常為介面。
  • ConcreteStrategy:具體策略角色,實現抽象策略角色,完成具體的演算法、策略。
  • Context:上下文環境角色,儲存了ConcreteStrategy,負責呼叫ConcreteStrategy。

而我上面的程式碼,就有了策略模式的味道,有了Strategy,也有了ConcreteStrategy,缺少的就是Context,如果用最原始的設計模式的寫法來實現,是醬紫的:

public class Context {
    static Map<Integer, QueryTicketStrategy> map = new HashMap<>();

    static {
        map.put(1, new QueryTicketAConcreteStrategy());
        map.put(2, new QueryTicketBConcreteStrategy());
        map.put(3, new QueryTicketCConcreteStrategy());
        map.put(4, new QueryTicketDConcreteStrategy());
    }

    public void getTicketList(int type) {
        System.out.println(map.get(type).getTicketList());
    }
}
public class Main {
    public static void main(String[] args) {
        Context context = new Context();
        context.getTicketList(1);
    }
}

執行結果:

[Ticket{desc='去程機票'}, Ticket{desc='返程機票'}]

在這裡,我把類名重新定義了下,讓人一眼就可以看出這裡使用了策略模式,這也是阿里推薦的命名方法。

策略模式是不是很簡單(我在學習設計模式的時候,甚至覺得它比單例、簡單工廠還要簡單),而且特別實用,下面我們來看看策略模式的UML圖:
image.png

JDK中的策略模式

既然策略模式那麼實用,那麼在JDK中有策略模式的應用嗎?當然有。JDK中定義的Comparator介面就是策略模式的一種實踐了:

public class SortLengthComparator implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return (o1.length() - o2.length() > 0) ? 1 : -1;
    }
}
public class Main {
    public static void main(String[] args) {
        List<String>list=new ArrayList<>();
        list.add("hello");
        list.add("world");
        list.add("codebear");
        list.add("balabala");
        list.add("java");
        list.sort(new SortLengthComparator());
        System.out.println(list);
    }
}

我定義了一個比較器,實現了Comparator介面,重寫了compare方法,實現了以比較字串長度來比較字串的功能。

執行結果:

[java, world, hello, balabala, codebear]

Comparator介面就是Strategy,我定義的SortLengthComparator就是ConcreteStrategy。

Comparator結合Lambda,會產生怎樣的火花

定義一個比較器,雖然不難,但是總覺得不夠簡潔,不夠方便,需要新建一個類,所以現在越來越多的人使用Lambda來進行排序,就像下面的醬紫:

        List<String>list=new ArrayList<>();
        list.add("hello");
        list.add("world");
        list.add("codebear");
        list.add("balabala");
        list.add("java");
        List<String> newList = list.stream().sorted((a, b) -> (a.length() - b.length() > 0) ? 1 : -1).collect(Collectors.toList());
        newList.forEach(System.out::println);

雖然底層還是用的Comparator,但是這樣的寫法清爽多了,如果比較的策略比較複雜,或者有多個地方都需要用到這個比較策略,還是用最原始的寫法更好一些。

策略模式與Spring的碰撞

現在我們已經知道了什麼是策略模式,如何使用策略模式,但是還有一個天大的問題,要知道,現在每個專案都在用Spring,如果你還是這麼寫的話:

public class Context {
    static Map<Integer, QueryTicketStrategy> map = new HashMap<>();

    static {
        map.put(1, new QueryTicketAConcreteStrategy());
        map.put(2, new QueryTicketBConcreteStrategy());
        map.put(3, new QueryTicketCConcreteStrategy());
        map.put(4, new QueryTicketDConcreteStrategy());
    }

    public void getTicketList(int type) {
        System.out.println(map.get(type).getTicketList());
    }
}

就意味著實現類裡面的依賴需要自己去維護,無法使用神奇的@Autowired註解,所以策略模式與Spring碰撞,策略模式必須發生一點改變,而這改變讓策略模式變得更加簡單,效能更好,也更加迷人。

寫法1

@Service
public class QueryTicketAConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程機票"));
        list.add(new Ticket("返程機票"));
        return list;
    }
}
@Service
public class QueryTicketDConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一張機票"));
        list.add(new Ticket("去程第二張機票"));
        list.add(new Ticket("返程第一張機票"));
        list.add(new Ticket("返程第二張機票"));
        return list;
    }
}
@Service
public class Context {

    @Autowired
    private QueryTicketStrategy queryTicketAConcreteStrategy;

    @Autowired
    private QueryTicketStrategy queryTicketDConcreteStrategy;

    private static Map<Integer, QueryTicketStrategy> map = new HashMap<>();

    @PostConstruct
    public void init() {
        map.put(1, queryTicketAConcreteStrategy);
        map.put(4, queryTicketAConcreteStrategy);
    }

    public void getTicketList(int type) {
        System.out.println(map.get(type).getTicketList());
    }
}
@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
        run.getBean(Context.class).getTicketList(1);
    }
}

執行結果:

[Ticket{desc='去程機票'}, Ticket{desc='返程機票'}]

原始的設計模式有一個缺點,不管是具體的策略實現類,還是上下文類,都不是單例模式,而我們的方法在大多數情況下是無狀態的,所以改成單例模式是非常合適的,而結合了Spring,我們完全不需要手寫單例模式,Spring就幫我們完成了。

寫法2(自認為最優雅)

不管是原始的策略模式,還是Spring與策略模式結合的第一種寫法,都沒有完全符合開閉原則,如果有新的策略引入,必須修改上下文類,往map裡面新增一組新的對映關係,而第二種寫法完美的解決了這個問題,而且讓策略模式變得非常優雅,下面直接放出程式碼:

@Service("1")
public class QueryTicketAConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程機票"));
        list.add(new Ticket("返程機票"));
        return list;
    }
}
@Service("4")
public class QueryTicketDConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一張機票"));
        list.add(new Ticket("去程第二張機票"));
        list.add(new Ticket("返程第一張機票"));
        list.add(new Ticket("返程第二張機票"));
        return list;
    }
}
@Service
public class Context {

    @Autowired
    private Map<String, QueryTicketStrategy> map = new HashMap<>();


    public void getTicketList(int type) {
        String typeStr = String.valueOf(type);
        System.out.println(map.get(typeStr).getTicketList());
    }
}
@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
        run.getBean(Context.class).getTicketList(1);
    }
}

執行結果:

[Ticket{desc='去程機票'}, Ticket{desc='返程機票'}]

這就是Spring和神奇、迷人之處了,竟然可以自動注入map,key就是beanName,value就是介面(具體的實現類)。

用這種寫法不但完成了天然的單例模式,而且真正的符合了開閉原則,引入新的策略,完全不需要修改任何一行舊程式碼,自認為這種寫法是最優雅、最迷人的。

END

相關文章