設計模式之策略模式程式碼實戰
課程目標
- 瞭解程式碼重構
- 瞭解策略模式的定義、應用場景
- 瞭解JDK中策略模式的應用
- 瞭解設計原則(開閉原則、單一職責原則)
從一個真實需求案例開始
- 背景為某電商網站上線初期
v1版本
需求:顯示商品價格iPhone XR價格7100元
@ResponseBody public String getPrice() { return "7100.00" }
v2版本
需求:需要根據客戶型別,進行不同的折扣,新客戶不打折扣,針對老客戶打9折。
最簡單的 實現程式碼 版本1
if ("新客戶".equals(customType)) { System.out.println("抱歉!新客戶沒有折扣!"); return originalPrice; }else if ("老客戶".equals(customType)) { System.out.println("恭喜你!老客戶打9折!"); originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; }
V3版本
現在需求變更了,增加了針對VIP客戶打8折
if ("新客戶".equals(customType)) { System.out.println("抱歉!新客戶沒有折扣!"); return originalPrice; }else if ("老客戶".equals(customType)) { System.out.println("恭喜你!老客戶打9折!"); originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; }else if("VIP客戶".equals(customType)){ System.out.println("恭喜你!VIP客戶打8折!"); originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; }
V3.5版本 重構
尋找程式碼問題,出現了多重條件判斷語句,程式碼耦合嚴重,不便於維護
第一次重構
提取方法,將分支中的語句提取成單獨的方法,通過方法名降低維護難度
public BigDecimal getNewCustomerPrice(BigDecimal originalPrice) { return originalPrice; } public BigDecimal getOldCustomerPrice(BigDecimal originalPrice) { return originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); } public BigDecimal getVIPCustomerPrice(BigDecimal originalPrice) { return originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP); }
第二次重構
提取介面,重構實現類。將報價策略抽象成介面,針對不同的報價策略實現不同的具體類。
public interface ICustomerQuotation { public BigDecimal getQuotation(BigDecimal originalPrice); }
具體策略實現類
public class NewCustomerQuotation implements ICustomerQuotation { @Override public BigDecimal getQuotation(BigDecimal originalPrice) { return originalPrice; } } public class OldCustomerQuotation implements ICustomerQuotation { @Override public BigDecimal getQuotation(BigDecimal originalPrice) { return originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); } } public class VIPCustomerQuotation implements ICustomerQuotation { @Override public BigDecimal getQuotation(BigDecimal originalPrice) { return originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP); } }
客戶端呼叫
ICustomerQuotation quotation; if (Constant.NEW.equalsIgnoreCase(customerType)) { quotation = new NewCustomerQuotation(); } else if (Constant.OLD.equalsIgnoreCase(customerType)) { quotation = new OldCustomerQuotation(); } else if (Constant.VIP.equalsIgnoreCase(customerType)) { quotation = new VIPCustomerQuotation(); } else { quotation = new NewCustomerQuotation(); } BigDecimal price = quotation.getQuotation(originalPrice);
第三次重構
提取context,通過context持有具體策略,遮蔽客戶端對具體策略方法的依賴,通過context進行隔離。
public class CustomerQuotationContext { private ICustomerQuotation quotation; public CustomerQuotationContext(ICustomerQuotation quotation) { this.quotation = quotation; } public String getPrice(BigDecimal originalPrice) { System.out.println("進行一些前置處理"); BigDecimal price = quotation.getQuotation(originalPrice); System.out.println("進行一些後置處理"); return price.toPlainString(); } }
客戶端呼叫
@RequestMapping("/getMyQuotation") @ResponseBody public String getPrice(String customerType) { BigDecimal myPrice = originalPrice; ICustomerQuotation quotation; if (Constant.NEW.equalsIgnoreCase(customerType)) { quotation = new NewCustomerQuotation(); } else if (Constant.OLD.equalsIgnoreCase(customerType)) { quotation = new OldCustomerQuotation(); } else if (Constant.VIP.equalsIgnoreCase(customerType)) { quotation = new VIPCustomerQuotation(); } else { quotation = new NewCustomerQuotation(); } CustomerQuotationContext customerQuotationContext = new CustomerQuotationContext(quotation); return customerQuotationContext.getPrice(originalPrice); }
這就是策略模式
引入策略模式
定義
定義一系列的演算法,把它們一個個封裝起來,並且使它們可相互替換。
類圖
使用場景
- 多個類只有在演算法或行為上稍有不同的場景。
- 演算法需要自由切換的場景。
- 需要遮蔽演算法規則的場景。
電商案例需求變更
V4版本
增加MVP使用者,MVP使用者7折。
因為使用了策略模式,所以很容易擴充套件,簡單的實現一個針對MVP客戶的策略實現類即可。
public class MVPCustomerQuotation implements ICustomerQuotation { @Override public BigDecimal getQuotation(BigDecimal originalPrice) { return originalPrice.multiply(new BigDecimal(0.7)).setScale(2,BigDecimal.ROUND_HALF_UP); } }
V5版本
需求變更增加郵費,新客戶、老客戶郵費10.11元,VIP客戶9.11元,MVP客戶0.11元。
在策略介面中增加新方法 getShippingFee(),具體策略類進行實現。
public class MVPCustomerQuotation implements ICustomerQuotation { @Override public BigDecimal getQuotation(BigDecimal originalPrice) { return originalPrice.multiply(new BigDecimal(0.7)).setScale(2,BigDecimal.ROUND_HALF_UP); } @Override public String getCustomerType() { return Constant.MVP; } @Override public BigDecimal getShippingFee() { return new BigDecimal(0.11); } }
在context上下文中進行處理,對客戶端進行遮蔽
public String getPrice(BigDecimal originalPrice) { System.out.println("進行一些前置處理"); BigDecimal price = quotation.getQuotation(originalPrice); price = price.add(quotation.getShippingFee()).setScale(2, BigDecimal.ROUND_HALF_UP); System.out.println("進行一些後置處理"); return price.toPlainString(); }
電商案例繼續重構V6版本
如何進一步優化程式碼呢,能否減少if else判斷?
- 對策略介面進行改進,使用@PostConstruct對策略物件進行map管理
public interface ICustomerQuotation { public static Map<String, ICustomerQuotation> map = new ConcurrentHashMap(); public BigDecimal getQuotation(BigDecimal originalPrice); public BigDecimal getShippingFee(); public String getCustomerType(); @PostConstruct default public void init() { map.put(getCustomerType(), this); } }
- 通過spring繼續重構,具體實現策略增加@Service,通過spring進行管理
@Service public class NewCustomerQuotation implements ICustomerQuotation { @Override public BigDecimal getQuotation(BigDecimal originalPrice) { return originalPrice; } @Override public String getCustomerType() { return Constant.NEW; } @Override public BigDecimal getShippingFee() { return new BigDecimal(10.11); } }
- 簡化客戶端呼叫
@Controller public class QuotationController { public static final BigDecimal originalPrice = new BigDecimal(7100.00).setScale(2, BigDecimal.ROUND_HALF_UP); @RequestMapping("/getMyQuotation") @ResponseBody public String getPrice(String customerType) { BigDecimal myPrice = originalPrice; CustomerQuotationContext customerQuotationContext = new CustomerQuotationContext(customerType); return customerQuotationContext.getPrice(originalPrice); } }
策略模式其他應用案例
- 諸葛亮的錦囊妙計,每一個錦囊就是一個策略。三國劉備取西川時,謀士龐統給的上、中、下三個計策:
-
上策:挑選精兵,晝夜兼行直接偷襲成都,可以一舉而定,此為上計計也。
-
中策:楊懷、高沛是蜀中名將,手下有精銳部隊,而且據守關頭,我們可以裝作要回荊州,引他們輕騎來見,可就此將其擒殺,而後進兵成都,此為中計。
-
下策:退還白帝,連引荊州,慢慢進圖益州,此為下計。
這三個計策都是取西川的計策,也就是攻取西川這個問題的具體的策略演算法,劉備可以採用上策,可以採用中策,當然也可以採用下策,由此可見策略模式的各種具體的策略演算法都是平等的,可以相互替換。那誰來選擇具體採用哪種計策(演算法)?在這個故事中當然是劉備選擇了,也就是外部的客戶端選擇使用某個具體的演算法,然後把該演算法(計策)設定到上下文當中。還有一種情況就是客戶端不選擇具體的演算法,把這個事交給上下文,這相當於劉備說我不管有哪些攻取西川的計策,我只要結果(成功的拿下西川),具體怎麼攻佔(有哪些計策,怎麼選擇)由參謀部來決定(上下文)。
- 旅行的出遊方式,選擇騎自行車、坐汽車,每一種旅行方式都是一個策略。
策略模式在JDK中的應用
ThreadPoolExecutor
RejectedExecutionHandler 是一個策略介面,用在當執行緒池中沒有多餘的執行緒來執行任務,並且儲存任務的多列也滿了(指的是有界佇列),對仍在提交給執行緒池的任務的處理策略。
JAVA AWT 中的 LayoutManager
Collections排序中的應用 Comparator
我們如果需要控制某個類的次序,而該類本身不支援排序(即沒有實現Comparable介面);那麼可以建立一個該類的比較器來排序,這個比較器只需要實現Comparator介面即可。,通過實現Comparator類來新建一個比較器,然後通過該比較器來對類進行排序。Comparator 介面其實就是一種策略模式的實踐
事例程式碼:
抽象策略類 Comparator
public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }
具體策略類 SortComparator
public class SortComparator implements Comparator { @Override public int compare(Object o1, Object o2) { Student student1 = (Student) o1; Student student2 = (Student) o2; return student1.getAge() - student2.getAge(); } }
策略模式上下文 Collections
public class Client { public static void main(String[] args) { Student stu[] = { new Student("張三" ,23), new Student("李四" ,26) , new Student("王五" ,22)}; Arrays. sort(stu,new SortComparator()); System.out.println(Arrays.toString(stu)); List<Student> list = new ArrayList<>(3); list.add( new Student("zhangsan" ,31)); list.add( new Student("lisi" ,30)); list.add( new Student("wangwu" ,35)); Collections.sort(list,new SortComparator()); System.out.println(list); } }
策略模式優缺點
優點:
- 演算法可以自由切換。
- 避免使用多重條件判斷。
-
擴充套件性良好
(1) 策略模式提供了對“開閉原則”的完美支援,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。
(2) 演算法的使用就和演算法本身分開,符合“單一職責原則”;
(3) 策略模式提供了一種演算法的複用機制,由於將演算法單獨提取出來封裝在策略類中,因此不同的環境類可以方便地複用這些策略類。
缺點
- 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法。換言之,策略模式只適用於客戶端知道所有的演算法或行為的情況。
- 策略模式將造成系統產生很多具體策略類,任何細小的變化都將導致系統要增加一個新的具體策略類。
- 無法同時在客戶端使用多個策略類,也就是說,在使用策略模式時,客戶端每次只能使用一個策略類,不支援使用一個策略類完成部分功能後再使用另一個策略類來完成剩餘功能的情況。