策略模式(學習筆記)
1. 意圖
定義一系列演算法,把它們一個個封裝起來,並且使它們可相互替換。本模式使得演算法可獨立於使用它的客戶而變化
2. 動機
假設打算為遊客們建立一款導遊程式。該程式的核心功能是提供美觀的地圖,以幫助使用者在任何城市中快速定位。使用者期待的程式新功能是自動路線規劃:他們希望輸入地址後就能在地圖上看到前往目的地的最快路線。程式的首個版本只能規劃公路路線。駕車旅行的人們對此非常滿意。但很顯然,並非所有人都會在度假時開車。因此在下次更新時添加了規劃步行路線的功能。此後,又添加了規劃公共交通路線的功能。而這只是個開始。不久後,又要為騎行者規劃路線。又過了一段時間,又要為遊覽城市中的所有景點規劃路線。儘管從商業角度來看,這款應用非常成功,但其技術部分卻讓你非常頭疼:每次新增新的路線規劃演算法後,導遊應用中主要類的體積就會增加一倍。隨著需求的不斷增加,你覺得自己沒法繼續維護這堆程式碼了
策略模式通過定義一些類來封裝不同功能背後的演算法,從而避免了一個類不斷膨脹以至於難以維護的問題。名為上下文的原始類必須包含一個成員變數來儲存對於每種策略的引用。上下文並不執行任務,而是將工作委派給已連線的策略物件。上下文不負責選擇符合任務需要的演算法——客戶端會將所需策略傳遞給上下文。實際上,上下文並不十分了解策略,它會通過同樣的通用介面與所有策略進行互動,而該介面只需暴露一個方法來觸發所選策略中封裝的演算法即可。因此,上下文可獨立於具體策略。這樣你就可在不修改上下文程式碼或其他策略的情況下新增新演算法或修改已有演算法了。在導遊應用中,每個路線規劃演算法都可被抽取到 buildRoute 方法的獨立類中。 該方法接收起點和終點作為引數,並返回路線中途點的集合。
3. 適用性
- 許多相關的類僅僅是行為有異。“策略”提供了一種用多個行為中的一個行為來配置一個類的方法
- 需要使用一個演算法的不同變體
- 演算法使用客戶不應該知道的資料。可使用策略模式以避免暴露覆雜的、與演算法相關的資料結構
- 一個類定義了多種行為,並且這些行為在這個類的操作中以多個條件語句的形式出現。將相關的條件分支移入它們各自的Strategy類中以代替這些條件語句
4. 結構
5. 效果
1. 可以在執行時切換物件內的演算法
2. 一個替代繼承的方法 如果直接生成一個Context類的子類,從而給它以不同的行為。這會將行為硬性編制到Context中,將演算法的實現與Context的實現混合起來,從而使Context難以理解、維護和擴充套件,而且還不能動態的改變演算法
3. 消除了一些條件語句
4. Strategy可以提供相同行為的不同實現
5. 客戶必須瞭解不同的Strategy以選擇合適的演算法
6.許多現代程式語言支援函式型別功能,允許你在一組匿名函式中實現不同版本的演算法。使用這些函式的方式就和使用策略物件時完全相同,無需藉助額外的類和介面來保持程式碼簡潔
7. 增加了物件的數目
8. Strategy和Context之間的通訊開銷 無論各個ConcreteStrategy實現的演算法是簡單還是複雜,它們都共享Strategy定義的介面。因此很可能某些ConcreteStrategy不會用到所有通過這個介面傳遞給它們的資訊,簡單的ConcreteStrategy可能不使用其中的任何資訊!這就意味著有時Context會建立和初始化一些永遠不會用到的引數。如果存在這個問題,需要在Strategy和Context之間進行更緊密的耦合
6. 程式碼實現
在本例中,策略模式被用於在電子商務應用中實現各種支付方法。客戶選中希望購買的商品後需要選擇一種支付方式:Paypal 或者信用卡。具體策略不僅會完成實際的支付工作,還會改變支付表單的行為,並在表單中提供相應的欄位來記錄支付資訊
strategies/PayStrategy.java: 通用的支付方法介面
package strategy.strategies; /** * @author GaoMing * @date 2021/7/26 - 20:57 * Common interface for all strategies. */ public interface PayStrategy { boolean pay(int paymentAmount); void collectPaymentDetails(); }
strategies/PayByPayPal.java: 使用 PayPal 支付
package strategy.strategies; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; /** * @author GaoMing * @date 2021/7/26 - 20:57 */ public class PayByPayPal implements PayStrategy{ private static final Map<String, String> DATA_BASE = new HashMap<>(); private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in)); private String email; private String password; private boolean signedIn; static { DATA_BASE.put("amanda1985", "[email protected]"); DATA_BASE.put("qwerty", "[email protected]"); } /** * Collect customer's data. */ @Override public void collectPaymentDetails() { try { while (!signedIn) { System.out.print("Enter the user's email: "); email = READER.readLine(); System.out.print("Enter the password: "); password = READER.readLine(); if (verify()) { System.out.println("Data verification has been successful."); } else { System.out.println("Wrong email or password!"); } } } catch (IOException ex) { ex.printStackTrace(); } } private boolean verify() { setSignedIn(email.equals(DATA_BASE.get(password))); return signedIn; } /** * Save customer data for future shopping attempts. */ @Override public boolean pay(int paymentAmount) { if (signedIn) { System.out.println("Paying " + paymentAmount + " using PayPal."); return true; } else { return false; } } private void setSignedIn(boolean signedIn) { this.signedIn = signedIn; } }
strategies/PayByCreditCard.java: 使用信用卡支付
package strategy.strategies; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * @author GaoMing * @date 2021/7/26 - 20:58 */ public class PayByCreditCard implements PayStrategy{ private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in)); private CreditCard card; /** * Collect credit card data. */ @Override public void collectPaymentDetails() { try { System.out.print("Enter the card number: "); String number = READER.readLine(); System.out.print("Enter the card expiration date 'mm/yy': "); String date = READER.readLine(); System.out.print("Enter the CVV code: "); String cvv = READER.readLine(); card = new CreditCard(number, date, cvv); // Validate credit card number... } catch (IOException ex) { ex.printStackTrace(); } } /** * After card validation we can charge customer's credit card. */ @Override public boolean pay(int paymentAmount) { if (cardIsPresent()) { System.out.println("Paying " + paymentAmount + " using Credit Card."); card.setAmount(card.getAmount() - paymentAmount); return true; } else { return false; } } private boolean cardIsPresent() { return card != null; } }
strategies/CreditCard.java: 信用卡類
package strategy.strategies; /** * @author GaoMing * @date 2021/7/26 - 20:58 */ public class CreditCard { private int amount; private String number; private String date; private String cvv; CreditCard(String number, String date, String cvv) { this.amount = 100_000; this.number = number; this.date = date; this.cvv = cvv; } public void setAmount(int amount) { this.amount = amount; } public int getAmount() { return amount; } }
context/Order.java: 訂單類
package strategy.context; import strategy.strategies.PayStrategy; /** * @author GaoMing * @date 2021/7/26 - 20:59 * Order class. Doesn't know the concrete payment method (strategy) user has * picked. It uses common strategy interface to delegate collecting payment data * to strategy object. It can be used to save order to database. * */ public class Order { private int totalCost = 0; private boolean isClosed = false; public void processOrder(PayStrategy strategy) { strategy.collectPaymentDetails(); // Here we could collect and store payment data from the strategy. } public void setTotalCost(int cost) { this.totalCost += cost; } public int getTotalCost() { return totalCost; } public boolean isClosed() { return isClosed; } public void setClosed() { isClosed = true; } }
Demo.java: 客戶端程式碼
package strategy; import strategy.context.Order; import strategy.strategies.PayByCreditCard; import strategy.strategies.PayByPayPal; import strategy.strategies.PayStrategy; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; /** * @author GaoMing * @date 2021/7/26 - 20:56 */ public class Demo { private static Map<Integer, Integer> priceOnProducts = new HashMap<>(); private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); private static Order order = new Order(); private static PayStrategy strategy; static { priceOnProducts.put(1, 2200); priceOnProducts.put(2, 1850); priceOnProducts.put(3, 1100); priceOnProducts.put(4, 890); } public static void main(String[] args) throws IOException { while (!order.isClosed()) { int cost; String continueChoice; do { System.out.print("Please, select a product:" + "\n" + "1 - Mother board" + "\n" + "2 - CPU" + "\n" + "3 - HDD" + "\n" + "4 - Memory" + "\n"); int choice = Integer.parseInt(reader.readLine()); cost = priceOnProducts.get(choice); System.out.print("Count: "); int count = Integer.parseInt(reader.readLine()); order.setTotalCost(cost * count); System.out.print("Do you wish to continue selecting products? Y/N: "); continueChoice = reader.readLine(); } while (continueChoice.equalsIgnoreCase("Y")); if (strategy == null) { System.out.println("Please, select a payment method:" + "\n" + "1 - PalPay" + "\n" + "2 - Credit Card"); String paymentMethod = reader.readLine(); // Client creates different strategies based on input from user, // application configuration, etc. if (paymentMethod.equals("1")) { strategy = new PayByPayPal(); } else { strategy = new PayByCreditCard(); } } // Order object delegates gathering payment data to strategy object, // since only strategies know what data they need to process a // payment. order.processOrder(strategy); System.out.print("Pay " + order.getTotalCost() + " units or Continue shopping? P/C: "); String proceed = reader.readLine(); if (proceed.equalsIgnoreCase("P")) { // Finally, strategy handles the payment. if (strategy.pay(order.getTotalCost())) { System.out.println("Payment has been successful."); } else { System.out.println("FAIL! Please, check your data."); } order.setClosed(); } } } }
執行結果
Please, select a product: 1 - Mother board 2 - CPU 3 - HDD 4 - Memory 1 Count: 2 Do you wish to continue selecting products? Y/N: y Please, select a product: 1 - Mother board 2 - CPU 3 - HDD 4 - Memory 2 Count: 1 Do you wish to continue selecting products? Y/N: n Please, select a payment method: 1 - PalPay 2 - Credit Card 1 Enter the user's email: [email protected] Enter the password: qwerty Wrong email or password! Enter user email: [email protected] Enter password: amanda1985 Data verification has been successful. Pay 6250 units or Continue shopping? P/C: p Paying 6250 using PayPal. Payment has been successful.
7. 與其他模式的關係
- 橋接模式、狀態模式和策略模式(在某種程度上包括介面卡模式)模式的介面非常相似。實際上,它們都基於組合模式——即將工作委派給其他物件,不過也各自解決了不同的問題。模式並不只是以特定方式組織程式碼的配方,你還可以使用它們來和其他開發者討論模式所解決的問題
-
命令模式和策略看上去很像,因為兩者都能通過某些行為來引數化物件。但是,它們的意圖有非常大的不同:
- 命令模式將任何操作轉換為物件。 操作的引數將成為物件的成員變數。可以通過轉換來延遲操作的執行、將操作放入佇列、儲存歷史命令或者向遠端服務傳送命令等
- 策略模式通常用於描述完成某件事的不同方式,讓你能夠在同一個上下文類中切換演算法 - 裝飾模式可讓你更改物件的外表,策略則讓你能夠改變其本質
- 模板方法模式基於繼承機制:它允許你通過擴充套件子類中的部分內容來改變部分演算法。策略基於組合機制:你可以通過對相應行為提供不同的策略來改變物件的部分行為。模板方法在類層次上運作,因此它是靜態的。策略在物件層次上運作,因此允許在執行時切換行為
- 狀態可被視為策略的擴充套件。兩者都基於組合機制:它們都通過將部分工作委派給“幫手”物件來改變其在不同情景下的行為。策略使得這些物件相互之間完全獨立,它們不知道其他物件的存在。但狀態模式沒有限制具體狀態之間的依賴,且允許它們自行改變在不同情景下的狀態
8. 已知應用
策略模式在Java程式碼中很常見。它經常在各種框架中使用,能在不擴充套件類的情況下向使用者提供改變其行為的方式
Java 8 開始支援 lambda 方法,它可作為一種替代策略模式的簡單方式
一些核心 Java 程式庫中策略模式的示例:
對 java.util.Comparator#compare() 的呼叫來自 Collections#sort()
javax.servlet.http.HttpServlet:service()方法,還有所有接受 HttpServletRequest和 HttpServletResponse物件作為引數的 doXXX()方法
javax.servlet.Filter#doFilter()
識別方法:策略模式可以通過允許巢狀物件完成實際工作的方法以及允許將該物件替換為不同物件的設定器來識別。