設計模式之【策略模式】

Gopher大威發表於2022-03-29

表妹:哥啊,羽絨服應該用什麼模式洗呢?

:像這種比較厚的衣物,應該用標準模式洗滌,才能夠洗得乾淨,如果是雪紡的,或者是棉麻的,這些衣物不適合長時間洗滌的話,就選擇快洗模式。

表妹:這樣子~

 

你看,洗衣機針對不同的衣物材料,有不同的洗衣模式。在軟體開發中,我們也常常遇到這種情況,實現某一個功能有多種演算法或策略,需要根據環境或者條件的不同選擇不同的演算法或者策略來完成該功能。比如排序、查詢等。

一種常用的設計方式是硬編碼(Hard Coding)在一個類中,如需要提供多種排序演算法,可以將這些演算法寫到一個類中,在該類中提供多個方法,每一個方法對應一個具體的排序演算法。

也可以將這些排序演算法封裝在一個統一的方法中,通過if...else...或者是switch...case...等條件判斷語句來進行選擇。

如果要對某種排序演算法進行替換,或者是新增排序演算法的話,都會造成改動的範圍比較大。如果可供選擇的排序演算法越來越多的話,就會造成該類程式碼比較複雜,難以維護,如果將這些策略包含到客戶端,也會導致客戶端程式碼比較龐大且難以維護。

那麼,如何讓演算法和物件分開來,使得演算法可以獨立於使用它的客戶而變化?

這個時候,策略模式就派上用場啦~

策略模式

定義一族演算法類,將每個演算法分別封裝起來,讓他們可以相互替換。策略模式可以使演算法的變化獨立於使用它們的客戶端(這裡的客戶端代指使用演算法的程式碼)。

 

  • Strategy策略:抽象策略,是對策略、演算法家族的抽象,通常為介面,定義每個策略或演算法必須具有的方法和屬性。

  • Context上下文:起承上啟下的作用,遮蔽高層模組對策略、演算法的直接訪問,封裝可能存在的變化。

  • ConcreteStrategy具體策略:用於實現抽象策略中的操作,即實現具體的演算法。

假如我們需要對一個檔案進行排序,檔案中只包含整型數,並且相鄰的數字用逗號隔開。這個檔案大小的範圍很大,可能只有幾KB,但也可能比記憶體還要大,甚至可能達到TB級別。那麼,就需要有不同的排序策略了,如果檔案很小的話,可以放到記憶體中使用快排,如果超過記憶體大小了,那就需要進行外部排序,再大的話,還有多執行緒外部排序、MapReduce多機排序。

最終目的都是使檔案中的所有整型數有序,但是得根據不同數量級使用不同的排序策略。這個時候,策略模式就派上用場了。

Strategy策略介面

1 public interface ISort {
2     void sort(String filePath);
3 }

ConcreteStrategy具體策略

 1 public class QuickSort implements ISort {
 2     @Override 
 3     public void sort(String filePath) {
 4         System.out.println("使用快排");
 5     }
 6 }
 7  8 public class ExternalSort implements ISort {
 9     @Override 
10     public void sort(String filePath) {
11         System.out.println("使用外部排序");
12     }
13 }
14 15 public class ConcurrentExternalSort implements ISort {
16     @Override 
17     public void sort(String filePath) {
18         System.out.println("使用多執行緒外部排序");
19     }
20 }
21 22 public class MapReduceSort implements ISort {
23     @Override 
24     public void sort(String filePath) {
25         System.out.println("使用MapReduce多機排序");
26     }
27 }

Context上下文角色:起承上啟下封裝作用,遮蔽高層模組對策略、演算法的直接訪問,封裝可能存在的變化。

 1 public class Context {
 2     private ISort sortAlg;
 3     
 4     public Context(ISort sortAlg) {
 5         this.sortAlg = sortAlg;
 6     }
 7     
 8     // 這個執行策略只是通過委託呼叫對應的排序演算法,可能沒必要這個Context封裝
 9     // 但是,如果排序演算法比較複雜,或者存在變化,那麼就需要這一層封裝了。
10     public void executeStrategy(String filePath) {
11         this.sortAlg.sort();
12     }
13 }

客戶端實現:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         static final long GB = 1000 * 1000 * 1000;
 4         
 5         File file = new File(filePath);
 6         long fileSize = file.length();
 7         Context executor = new Context();
 8         ISort sortAlg;
 9         if (fileSize < 6*GB) {           // [0, 6GB)
10             sortAlg = new QuickSort();
11         } else if (fileSize < 10*GB) {   // [6GB, 10GB)
12             sortAlg = new ExternalSort();
13         } else if (fileSize < 100GB) {   // [10GB, 100GB)
14             sortAlg = new ConcurrentExternalSort();
15         } else {                         // [100GB, ~)
16             sortAlg = new MapReduceSort();
17         }
18         Context executor = new Context(sortAlg);
19         executor.executeStrategy(filePath);
20     }
21 }

可能有同學會說,客戶端裡一堆if...else...,而且,這樣子客戶端必須理解所有策略演算法的區別,以便選擇適當的演算法類。其次每種排序類都是無狀態的,沒必要在每次使用的時候,都重新建立一個新的物件。

是的,我們可以使用簡單工廠模式來對物件的建立進行封裝。

策略模式與簡單工廠模式結合

策略介面和具體策略實現類不變,主要是Context中使用簡單工廠模式:

 1 public class Context {
 2     static final long GB = 1000 * 1000 * 1000;
 3     private static final Map<String, ISort> algs = new HashMap<>();
 4     
 5     static {
 6         algs.put("QuickSort", new QuickSort());
 7         algs.put("ExternalSort", new ExternalSort());
 8         algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
 9         algs.put("MapReduceSort", new MapReduceSort());
10     }
11     private ISort sortAlg;
12     
13     // 使用簡單工廠模式
14     public Context(long fileSize) {
15         if (fileSize < 6*GB) {           // [0, 6GB)
16             this.sortAlg = algs("QuickSort");
17         } else if (fileSize < 10*GB) {   // [6GB, 10GB)
18             this.sortAlg = algs("ExternalSort");
19         } else if (fileSize < 100GB) {   // [10GB, 100GB)
20             this.sortAlg = algs("ConcurrentExternalSort");
21         } else {                         // [100GB, ~)
22             this.sortAlg = algs("MapReduceSort");
23         }
24     }
25     
26     public void executeStrategy(String filePath) {
27         this.sortAlg.sort();
28     }
29 }

客戶端實現:

1 public class Demo {
2     public static void main(String[] args) {   
3         File file = new File(filePath);
4         long fileSize = file.length();
5         Context executor = new Context(fileSize);
6         executor.executeStrategy(filePath);
7     }
8 }

將例項化具體策略的過程由客戶端轉移到Context類中,你看,這樣的客戶端程式碼是不是簡潔多了,如果有新的排序演算法,只需要修改Context類即可,而且實現客戶端與ConcreteStrategy解耦。

策略模式的優點

  • 演算法可以自由切換

    這是策略模式本身定義的,只要實現抽象策略,它就成為策略家族的一個成員,通過封裝角色對其進行封裝,保證對外提供“可以自由切換”的策略。

  • 避免使用多重條件判斷

  • 擴充套件性好

    在現有的系統中增加一個策略很容易,只要實現介面就可以了。

  • 策略模式把演算法的使用放到Context類中,而演算法的實現移到具體的策略類中,實現二者的分類。

策略模式的缺點

  • 每個策略都是一個類,複用的可能性很小,類數量增多。

  • 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。此時可能不得不向客戶暴露具體的實現邏輯,因此,通常會與工廠方法結合使用。

策略模式的應用場景

  • 許多相關的類僅僅是行為不同的時候,使用策略模式,提供了一種用多個行為中的一個行為來配置一個類的方法。即一個系統需要動態地在幾種演算法中選擇一種。

  • 需要使用一個演算法的不同變體。例如,你可能會定義一些反映不同的空間/時間權衡的演算法。當這些變體實現為一個演算法的類層次時,可以使用策略模式。

  • 演算法使用客戶不應該知道的資料,可使用策略模式以避免暴露覆雜的、與演算法相關的資料結構。

  • 一個類定義了多種行為,並且這些行為在這個類的操作中以多個條件語句的形式出現,將相關的條件分支移入它們各自的Strategy類中以代替這些條件語句。

策略模式在開原始碼中的應用

我們知道,Spring AOP是通過動態代理來實現的,Spring支援兩種動態代理實現方式,一種是JDK提供的動態代理實現方式,另一種是Cglib提供的動態代理實現方式。

Spring會在執行時動態地選擇不同的動態代理實現方式,這個應用場景實際上就是策略模式的典型應用場景。

AopProxy是策略介面,定義如下:

JdkDynamicAopProxy、CglibAopProxy是兩個實現了AopProxy介面的策略類。

在策略模式中,策略的建立一般通過工廠方法來實現。對應到Spring原始碼,AopProxyFactory是一個工廠類介面,DefaultAopProxyFactory是一個預設的工廠類,用來建立AopProxy物件。

總結

封裝不同的演算法,演算法之間能互相替換。

參考

極客時間專欄《設計模式之美》

Java設計模式之策略模式炸斯特的部落格-CSDN部落格設計模式之策略模式

 

相關文章