二十三種設計模式 - 策略模式
策略模式簡介
模式動機
- 完成一項任務,往往可以有多種不同的方式,每一種方式稱為一個策略,我們可以根據環境或者條件的不同選擇不同的策略來完成該項任務。
- 在軟件開發中也常常遇到類似的情況,實現某一個功能有多個途徑,此時可以使用一種設計模式來使得系統可以靈活地選擇解決途徑,也能夠方便地增加新的解決途徑。
- 在軟件系統中,有許多算法可以實現某一功能,如查找、排序等,一種常用的方法是硬編碼(Hard Coding)在一個類中,如需要提供多種查找算法,可以將這些算法寫到一個類中,在該類中提供多個方法,每一個方法對應一個具體的查找算法;當然也可以將這些查找算法封裝在一個統一的方法中,通過if…else…等條件判斷語句來進行選擇。這兩種實現方法我們都可以稱之為硬編碼,如果需要增加一種新的查找算法,需要修改封裝算法類的源代碼;更換查找算法,也需要修改客戶端調用代碼。在這個算法類中封裝了大量查找算法,該類代碼將較復雜,維護較為困難。
- 除了提供專門的查找算法類之外,還可以在客戶端程序中直接包含算法代碼,這種做法更不可取,將導致客戶端程序龐大而且難以維護,如果存在大量可供選擇的算法時問題將變得更加嚴重。
- 為了解決這些問題,可以定義一些獨立的類來封裝不同的算法,每一個類封裝一個具體的算法,在這里,每一個封裝算法的類我們都可以稱之為策略(Strategy),為了保證這些策略的一致性,一般會用一個抽象的策略類來做算法的定義,而具體每種算法則對應于一個具體策略類。
模式定義
策略模式:屬于對象的行為模式。其用意是針對一組算法,將每一個算法封裝到具有共同接口的獨立的類中,從而使得它們可以相互替換。
策略模式:使得算法可以在不影響到客戶端的情況下發生變化。
策略模式:是對算法的包裝,是把使用算法的責任和算法本身分割開來,委派給不同的對象管理。策略模式通常把一個系列的算法包裝到一系列的策略類里面,作為一個抽象策略類的子類。用一句話來說,就是:“準備一組算法,并將每一個算法封裝起來,使得它們可以互換”。
策略模式結構
結構圖
時序圖
參與者
策略模式參與者:
Context:環境角色,持有一個Strategy的引用。
Strategy:抽象策略角色,這是一個抽象角色,通常由一個接口或抽象類實現。此角色給出所有的具體策略類所需的接口。
ConcreteStrategy:具體策略角色,包裝了相關的算法或行為。
環境角色類
public class Context {
//持有一個具體策略的對象
private Strategy strategy;
/**
* 構造函數,傳入一個具體策略對象
* @param strategy 具體策略對象
*/
public Context(Strategy strategy){
this.strategy = strategy;
}
/**
* 策略方法
*/
public void contextInterface(){
strategy.strategyInterface();
}
}
抽象策略類
public interface Strategy {
/**
* 策略方法
*/
public void strategyInterface();
}
具體策略類
public class ConcreteStrategyA implements Strategy {
@Override
public void strategyInterface() {
//相關的業務
}
}
public class ConcreteStrategyB implements Strategy {
@Override
public void strategyInterface() {
//相關的業務
}
}
public class ConcreteStrategyC implements Strategy {
@Override
public void strategyInterface() {
//相關的業務
}
}
策略模式實現
收銀軟件項目
現在需要給商場做收銀軟件,收銀員根據客戶購買產品的單價和數量,向客戶打印小票。
這個實現很簡單,一個類就可以搞定了
package com.lance.market.basic;
import java.util.Scanner;
public class Cash {
public String list = "";
public Double totalPrice = 0.00;
public void buttonOK() {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入單價:");
String price = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入數量:");
String num = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入折扣:");
String discount = scanner.nextLine();
Double account = Double.parseDouble(price) * Integer.parseInt(num) * Double.parseDouble(discount) / 10;
list += "單價:" + price + ",數量:" + num + ",折扣:" + discount + "\n";
totalPrice += account;
}
public static void main(String[] args) {
Cash cash = new Cash();
boolean flag = true;
while (flag) {
cash.buttonOK();
if (cash.totalPrice > 10) {
flag = false;
}
}
System.out.println("=============");
System.out.println("清單:\n" + cash.list);
System.out.println("總價:" + cash.totalPrice);
}
}
但是,用面向對象的角度思考,這個類將前端輸入和業務邏輯混合在一塊了,不利于維護,擴展,復用,也不靈活。
假如,現在商場在做活動,所有商品打折,7折,過一段時間,商場又搞活動,所有商品5折,國慶節活動,所有商品滿200減50。
按照上面的方式寫代碼,那么每次都要寫一遍,如何將其復用起來,并且每次增加新的活動的時候,又不會影響到原來的活動。
其實我們剛才所說的簡單工廠設計模式,也可應用在這里。
首先,有一個工廠類,在這個工廠類里面,根據類型,依賴于不同的現金收費方式。
先寫一個抽象的現金收費方式類(可理解為Product)
package com.lance.market.factory.product;
public abstract class CashFee {
public abstract double acceptCash(double money);
}
然后定義現金收費方式的實現類,分別是:正常收費,折扣類,返現類
package com.lance.market.factory.product;
public class NormalCashFee extends CashFee {
@Override
public double acceptCash(double money) {
return money;
}
}
package com.lance.market.factory.product;
public class DiscountCashFee extends CashFee {
private double discount = 0.00;
public DiscountCashFee(double discount) {
this.discount = discount / 10 ;
}
@Override
public double acceptCash(double money) {
return this.discount * money;
}
public double getDiscount() {
return discount;
}
public void setDiscount(double discount) {
this.discount = discount;
}
}
package com.lance.market.factory.product;
public class ReturnCashFee extends CashFee {
private double baseCash;
private double returnCash;
public ReturnCashFee(double baseCash, double returnCash) {
this.baseCash = baseCash;
this.returnCash = returnCash;
}
public double getBaseCash() {
return baseCash;
}
public void setBaseCash(double baseCash) {
this.baseCash = baseCash;
}
public double getReturnCash() {
return returnCash;
}
public void setReturnCash(double returnCash) {
this.returnCash = returnCash;
}
@Override
public double acceptCash(double money) {
return money - Math.floor(money / baseCash) * returnCash;
}
}
再定義一個工廠類,用來生產各種各樣的現金收費方式
package com.lance.market.factory;
import com.lance.market.factory.product.CashFee;
import com.lance.market.factory.product.DiscountCashFee;
import com.lance.market.factory.product.NormalCashFee;
import com.lance.market.factory.product.ReturnCashFee;
import java.util.Scanner;
public class CashFeeFactory {
public static CashFee createCashFee(int type, double discount, double basePrice, double returnPrice) {
CashFee cashFee = null;
switch (type) {
case 1:
cashFee = new NormalCashFee();
break;
case 2:
cashFee = new DiscountCashFee(discount);
break;
case 3:
cashFee = new ReturnCashFee(basePrice, returnPrice);
break;
}
return cashFee;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入單價:");
String price = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入數量:");
String num = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入折扣類型(1 無折扣 2 打折 3 滿減):");
String type = scanner.nextLine();
double discount = 0.0d;
double basePrice = 0;
double returnPrice = 0;
if ("2".equals(type)) {
scanner = new Scanner(System.in);
System.out.println("輸入折扣:");
discount = Double.parseDouble(scanner.nextLine());
}
if ("3".equals(type)) {
scanner = new Scanner(System.in);
System.out.println("基礎金額:");
basePrice = Double.parseDouble(scanner.nextLine());
scanner = new Scanner(System.in);
System.out.println("返還現金:");
returnPrice = Double.parseDouble(scanner.nextLine());
}
Double money = Double.parseDouble(price) * Integer.parseInt(num);
CashFee cashFee = CashFeeFactory.createCashFee(Integer.parseInt(type), discount, basePrice, returnPrice);
System.out.println("總價:" + cashFee.acceptCash(money));
}
}
思考:
使用簡單工廠設計模式的優缺點:
優點:
- 業務邏輯和前端展示相互分離開了。業務邏輯的修改,不影響前端代碼展示。
- 每一個業務邏輯單獨一個類,修改或者添加一個類,不會影響到其他的類。
- 使用工廠類封裝了業務邏輯類,前端不需要知道到底每種業務怎么實現,只需要知道他的父類即可。
缺點:
- 如果活動很頻繁,經常會搞各種各樣的活動,那么業務邏輯就會有很多種,每一次都要增加一個類。
- 每增加一個類都要修改工廠類,修改會很頻繁。
小結:
簡單工廠設計模式雖然也能解決這個問題,但這個模式只是解決對類的創建問題。
由此我們應該使用策略模式對需求做修改。
與簡單工廠模式類似,我們都需要先定義一個現金算法的抽象類,以及現金算法的各個實現類。
package com.lance.market.strategy;
public abstract class CashFee {
public abstract double acceptCash(double money);
}
package com.lance.market.factory.product;
public class NormalCashFee extends CashFee {
@Override
public double acceptCash(double money) {
return money;
}
}
package com.lance.market.factory.product;
public class DiscountCashFee extends CashFee {
private double discount = 0.00;
public DiscountCashFee(double discount) {
this.discount = discount / 10 ;
}
@Override
public double acceptCash(double money) {
return this.discount * money;
}
public double getDiscount() {
return discount;
}
public void setDiscount(double discount) {
this.discount = discount;
}
}
package com.lance.market.factory.product;
public class ReturnCashFee extends CashFee {
private double baseCash;
private double returnCash;
public ReturnCashFee(double baseCash, double returnCash) {
this.baseCash = baseCash;
this.returnCash = returnCash;
}
public double getBaseCash() {
return baseCash;
}
public void setBaseCash(double baseCash) {
this.baseCash = baseCash;
}
public double getReturnCash() {
return returnCash;
}
public void setReturnCash(double returnCash) {
this.returnCash = returnCash;
}
@Override
public double acceptCash(double money) {
return money - Math.floor(money / baseCash) * returnCash;
}
}
最后,我們來創建上下文類
package com.lance.market.strategy.context;
import com.lance.market.factory.product.CashFee;
import com.lance.market.factory.product.DiscountCashFee;
import com.lance.market.factory.product.NormalCashFee;
import com.lance.market.factory.product.ReturnCashFee;
import java.util.Scanner;
public class CashContext {
private CashFee cashFee;
public CashContext(CashFee cashFee) {
this.cashFee = cashFee;
}
public double getResult(double money) {
return cashFee.acceptCash(money);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入單價:");
String price = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入數量:");
String num = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入折扣類型(1 無折扣 2 打折 3 滿減):");
String type = scanner.nextLine();
double discount = 0.0d;
double basePrice = 0;
double returnPrice = 0;
if ("2".equals(type)) {
scanner = new Scanner(System.in);
System.out.println("輸入折扣:");
discount = Double.parseDouble(scanner.nextLine());
}
if ("3".equals(type)) {
scanner = new Scanner(System.in);
System.out.println("基礎金額:");
basePrice = Double.parseDouble(scanner.nextLine());
scanner = new Scanner(System.in);
System.out.println("返還現金:");
returnPrice = Double.parseDouble(scanner.nextLine());
}
Double money = Double.parseDouble(price) * Integer.parseInt(num);
CashContext cashContext = null;
switch (type) {
case "1":
cashContext = new CashContext(new NormalCashFee());
break;
case "2":
cashContext = new CashContext(new DiscountCashFee(discount));
break;
case "3":
cashContext = new CashContext(new ReturnCashFee(basePrice, returnPrice));
break;
}
System.out.println("總價:" + cashContext.getResult(money));
}
}
思考:
- 業務邏輯和前端頁面展示分開。
- 有一個context上下文類,在其內部引用了CashFee類,構造方法定義了具體的實現類。
- 但是這樣操作客戶端依然需要switch判斷。
這時我們可以將簡單工廠模式和策略模式結合起來使用,并且其他的都不用變化,變化的是CashContext。
package com.lance.market.strategy.context;
import com.lance.market.factory.product.CashFee;
import com.lance.market.factory.product.DiscountCashFee;
import com.lance.market.factory.product.NormalCashFee;
import com.lance.market.factory.product.ReturnCashFee;
import java.util.Scanner;
public class CashContext {
private CashFee cashFee;
public CashContext(int type, double discount, double basePrice, double returnPrice) {
switch (type) {
case 1:
this.cashFee = new NormalCashFee();
break;
case 2:
this.cashFee = new DiscountCashFee(discount);
break;
case 3:
this.cashFee = new ReturnCashFee(basePrice, returnPrice);
break;
}
}
public double getResult(double money) {
return cashFee.acceptCash(money);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入單價:");
String price = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入數量:");
String num = scanner.nextLine();
scanner = new Scanner(System.in);
System.out.println("輸入折扣類型(1 無折扣 2 打折 3 滿減):");
String type = scanner.nextLine();
double discount = 0.0d;
double basePrice = 0;
double returnPrice = 0;
if ("2".equals(type)) {
scanner = new Scanner(System.in);
System.out.println("輸入折扣:");
discount = Double.parseDouble(scanner.nextLine());
}
if ("3".equals(type)) {
scanner = new Scanner(System.in);
System.out.println("基礎金額:");
basePrice = Double.parseDouble(scanner.nextLine());
scanner = new Scanner(System.in);
System.out.println("返還現金:");
returnPrice = Double.parseDouble(scanner.nextLine());
}
Double money = Double.parseDouble(price) * Integer.parseInt(num);
CashContext cashContext = new CashContext(Integer.parseInt(type), discount, basePrice, returnPrice);
System.out.println("總價:" + cashContext.getResult(money));
}
}
現在我們將前端的switch轉移到了CashContext的內部,這樣,前端只需要傳遞給我類別信息就可以了。
下面來對比一下:簡單工廠設計模式 和 策略模式+簡單工廠模式的區別:
CashFee cashFee = CashFeeFactory.createCashFee(Integer.parseInt(type), discount, basePrice, returnPrice);
CashContext cashContext = new CashContext(Integer.parseInt(type), discount, basePrice, returnPrice);
對于客戶端而言,簡單工廠設計模式,客戶端需要知道兩個類,CashFee和CashFeeFactory;而簡單工廠模式+策略模式,客戶段只需要知道一個類CashContex即可,降低類耦合性。
鴨子項目
模擬鴨子項目
- 從項目“模擬鴨子游戲”開始
- 從OO的角度設計這個項目,鴨子超類,擴展超類:
package com.java.lance.stimulateduck.oo;
public abstract class Duck {
public Duck() {
}
public void Quack() {
System.out.println("~~gaga~~");
}
public abstract void display();
public void swim() {
System.out.println("~~i'm swim~~");
}
}
- GreenHeadDuck繼承Duck:
package com.java.lance.stimulateduck.oo;
public class GreenHeadDuck extends Duck {
@Override
public void display() {
System.out.println("**GreenHead**");
}
}
- RedHeadDuck繼承Duck:
package com.java.lance.stimulateduck.oo;
public class RedHeadDuck extends Duck {
@Override
public void display() {
System.out.println("**RedHead**");
}
}
- 測試
package com.java.lance.stimulateduck.oo;
public class StimulateDuck {
public static void main(String[] args) {
GreenHeadDuck greenHeadDuck = new GreenHeadDuck();
RedHeadDuck redHeadDuck = new RedHeadDuck();
greenHeadDuck.display();
greenHeadDuck.quack();
greenHeadDuck.swim();
redHeadDuck.display();
redHeadDuck.quack();
redHeadDuck.swim();
}
}
- 輸出
**GreenHead**
~~gaga~~
~~i'm swim~~
**RedHead**
~~gaga~~
~~i'm swim~~
Process finished with exit code 0
項目的新需求
- 于此同時為了應對新的需求,看看這個設計的可擴展性
- 添加會飛的鴨子
- OO思維里的繼承方式解決方案是:
package com.java.lance.stimulateduck.oo;
public abstract class Duck {
public Duck() {
}
public void quack() {
System.out.println("~~gaga~~");
}
public abstract void display();
public void swim() {
System.out.println("~~i'm swim~~");
}
public void fly() {
System.out.println("~~i'm fly~~");
}
}
在超類里添加飛方法,這時所有繼承超類的子類都擁有飛方法。但是并不是所有的鴨子都會飛。
所以:
繼承的問題:對類的局部改動,尤其超類的局部改動,會影響其他部分。影響會有溢出效應。
用OO原則解決新需求的不足
- 繼續嘗試用OO原理來解決,覆蓋:
package com.java.lance.stimulateduck.oo;
public class GreenHeadDuck extends Duck {
@Override
public void display() {
System.out.println("**GreenHead**");
}
public void fly() {
System.out.println("~~no fly~~");
}
}
這樣的話,如果有幾十種方法,這時工作量非常大,子類需要覆蓋超類的方法。
所以,超類挖的一個坑,每個子類都要來填,增加工作量,復雜度O(N^2)。不是好的設計方式。
需要新的設計方式,應對項目的擴展性,降低復雜度:
1)需要分析項目變化與不變的部分,提取變化的部分,抽象成接口+實現;
2)鴨子哪些功能是會根據新需求變化的?叫聲,飛行。。。
用策略模式來解決新需求
- 接口:
package com.java.lance.stimulateduck.flybehavior;
public interface FlyBehavior {
void fly();
}
package com.java.lance.stimulateduck.quackbehavior;
public interface QuackBehavior {
void quack();
}
好處:新增行為簡單,行為類更好的復用,組合更方便。既有繼承帶來的復用好處,沒有挖坑。
重新設計模擬鴨子項目
- 重新設計的鴨子項目:
package com.java.lance.stimulateduck.duck;
import FlyBehavior;
import QuackBehavior;
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {
}
public void fly() {
flyBehavior.fly();
}
public void quack() {
quackBehavior.quack();
}
public abstract void display();
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
}
總結策略模式定義
- 綠頭鴨,石頭鴨:
package com.java.lance.stimulateduck.duck;
import GoodFlyBehavior;
import GaGaQuackBehavior;
public class GreenHeadDuck extends Duck {
public GreenHeadDuck(){
flyBehavior = new GoodFlyBehavior();
quackBehavior = new GaGaQuackBehavior();
}
@Override
public void display() {
System.out.println("~~green head~~");
}
}
- 策略模式:分別封裝行為接口,實現算法族,超類里放行為接口對象,在子類里具體設定行為對象。原則就是:分離變化部分,封裝接口,基于接口編程各種功能。此模式讓行為算法的變化獨立于算法的使用者。
策略模式應用分析
策略模式分析
- 策略模式是一個比較容易理解和使用的設計模式,策略模式是對算法的封裝,它把算法的責任和算法本身分割開,委派給不同的對象管理。策略模式通常把一個系列的算法封裝到一系列的策略類里面,作為一個抽象策略類的子類。用一句話來說,就是“準備一組算法,并將每一個算法封裝起來,使得它們可以互換”。
- 在策略模式中,應當由客戶端自己決定在什么情況下使用什么具體策略角色。
- 策略模式僅僅封裝算法,提供新算法插入到已有系統中,以及老算法從系統中“退休”的方便,策略模式并不決定在何時使用何種算法,算法的選擇由客戶端來決定。這在一定程度上提高了系統的靈活性,但是客戶端需要理解所有具體策略類之間的區別,以便選擇合適的算法,這也是策略模式的缺點之一,在一定程度上增加了客戶端的使用難度。
認識策略模式
策略模式的重心
策略模式的重心不是如何實現算法,而是如何組織、調用這些算法,從而讓程序結構更靈活,具有更好的維護性和擴展性。
算法的平等性
策略模式一個很大的特點就是各個策略算法的平等性。對于一系列具體的策略算法,大家的地位是完全一樣的,正因為這個平等性,才能實現算法之間可以相互替換。所有的策略算法在實現上也是相互獨立的,相互之間是沒有依賴的。
所以可以這樣描述這一系列策略算法:策略算法是相同行為的不同實現。
運行時策略的唯一性
運行期間,策略模式在每一個時刻只能使用一個具體的策略實現對象,雖然可以動態地在不同的策略實現中切換,但是同時只能使用一個。
公有的行為
經常見到的是,所有的具體策略類都有一些公有的行為。這時候,就應當把這些公有的行為放到共同的抽象策略角色Strategy類里面。當然這時候抽象策略角色必須要用Java抽象類實現,而不能使用接口。
這其實也是典型的將代碼向繼承等級結構的上方集中的標準做法。
策略模式的優點
策略模式提供了管理相關的算法族的辦法。策略類的等級結構定義了一個算法或行為族。恰當使用繼承可以把公共的代碼移到父類里面,從而避免代碼重復。
使用策略模式可以避免使用多重條件(if-else)語句。多重條件語句不易維護,它把采取哪一種算法或采取哪一種行為的邏輯與算法或行為的邏輯混合在一起,統統列在一個多重條件語句里面,比使用繼承的辦法還要原始和落后。
策略模式的缺點
- 客戶端必須知道所有的策略類,并自行決定使用哪一個策略類。這就意味著客戶端必須理解這些算法的區別,以便適時選擇恰當的算法類。換言之,策略模式只適用于客戶端知道算法或行為的情況。
- 由于策略模式把每個具體的策略實現都單獨封裝成為類,如果備選的策略很多的話,那么對象的數目就會很可觀。
策略模式注意點
- 分析項目中變化部分與不變部分
- 多用組合少用繼承;用行為類組合,而不是行為的繼承。更有彈性。
適用環境
在以下情況下可以使用策略模式:
- 如果在一個系統里面有許多類,它們之間的區別僅在于它們的行為,那么使用策略模式可以動態地讓一個對象在許多行為中選擇一種行為。
- 一個系統需要動態地在幾種算法中選擇一種。
- 如果一個對象有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。
- 不希望客戶端知道復雜的、與算法相關的數據結構,在具體策略類中封裝算法和相關的數據結構,提高算法的保密性與安全性。
總結
- 在策略模式中定義了一系列算法,將每一個算法封裝起來,并讓它們可以相互替換。策略模式讓算法獨立于使用它的客戶而變化,也稱為政策模式。策略模式是一種對象行為型模式。
- 策略模式包含三個角色:環境類在解決某個問題時可以采用多種策略,在環境類中維護一個對抽象策略類的引用實例;抽象策略類為所支持的算法聲明了抽象方法,是所有策略類的父類;具體策略類實現了在抽象策略類中定義的算法。
- 策略模式是對算法的封裝,它把算法的責任和算法本身分割開,委派給不同的對象管理。策略模式通常把一個系列的算法封裝到一系列的策略類里面,作為一個抽象策略類的子類。
- 策略模式主要優點在于對“開閉原則”的完美支持,在不修改原有系統的基礎上可以更換算法或者增加新的算法,它很好地管理算法族,提高了代碼的復用性,是一種替換繼承,避免多重條件轉移語句的實現方式;其缺點在于客戶端必須知道所有的策略類,并理解其區別,同時在一定程度上增加了系統中類的個數,可能會存在很多策略類。
- 策略模式適用情況包括:在一個系統里面有許多類,它們之間的區別僅在于它們的行為,使用策略模式可以動態地讓一個對象在許多行為中選擇一種行為;一個系統需要動態地在幾種算法中選擇一種;避免使用難以維護的多重條件選擇語句;希望在具體策略類中封裝算法和與相關的數據結構。