策略模式是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圖:
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就是介面(具體的實現類)。
用這種寫法不但完成了天然的單例模式,而且真正的符合了開閉原則,引入新的策略,完全不需要修改任何一行舊程式碼,自認為這種寫法是最優雅、最迷人的。