1. 程式人生 > >策略模式-定義一個演算法族

策略模式-定義一個演算法族

> **公號:碼農充電站pro** > **主頁:** 本篇來介紹策略模式(**Strategy Design Pattern**)。 假設我們要為動物進行建模,比如狗,豬,兔子等,每種動物的能力是不同的。 ### 1,使用繼承 首先你可能想到用繼承的方式來實現,所以我們編寫了下面這個 `Animal` 類: ```java abstract class Animal { public void run() { System.out.println("I can run."); } public void drinkWater() { System.out.println("I can drink water."); } protected abstract String type(); } ``` `Animal` 是一個抽象類,其中包括了動物的能力,每種能力用一個方法表示: - *run*:奔跑能力。 - *drinkWater*:喝水能力。 - *type*:返回動物的種類,比如“狗”,“兔子”。這是一個抽象方法,子類要去實現。 然後我們編寫 `Dog`,`Pig` 和 `Rabbit`: ```java class Dog extends Animal { public String type() { return "Dog"; } } class Pig extends Animal { public String type() { return "Pig"; } } class Rabbit extends Animal { public String type() { return "Rabbit"; } } ``` 上面的三種動物都繼承了 `Animal` 中的 *run* 和 *drinkWater*,並且都實現了自己的 *type* 方法。 現在我們想給 `Pig` 和 `Rabbit` 加入`吃草`的能力,最直接的辦法是分別在這兩個類中加入 *eatGrass* 方法,如下: ```java class Pig extends Animal { public void eatGrass() { System.out.println("I can eat grass."); } public String type() { return "Pig"; } } class Rabbit extends Animal { public void eatGrass() { System.out.println("I can eat grass."); } public String type() { return "Rabbit"; } } ``` 上面程式碼能夠達到目的,但是不夠好,因為`Pig` 和 `Rabbit` 中的 *eatGrass* 一模一樣,是重複程式碼,程式碼沒能複用。 為了解決程式碼複用,我們可以將 *eatGrass* 方法放到 `Animal` 中,利用繼承的特性,`Pig` 和 `Rabbit` 中就不需要編寫 *eatGrass* 方法,而直接從 `Animal` 中繼承就行。 但是,這樣還是有問題,因為如果將 *eatGrass* 放在 `Animal` 中,`Dog` 中也會有 *eatGrass* ,而我們並不想讓 `Dog` 擁有`吃草`的能力。 也許你會說,我們可以在 `Dog` 中將 *eatGrass* 覆蓋重寫,讓 *eatGrass* 不具有實際的能力,就像這樣: ```java class Dog extends Animal { public void eatGrass() { // 什麼都不寫,就沒有了吃草的能力 } public String type() { return "Rabbit"; } } ``` 這樣做雖然能到達目的,但是並不優雅。如果 `Animal` 的子類特別多的話,就會有很多子類都得這樣覆蓋 *eatGrass* 方法。 所以,將 *eatGrass* 放在 `Animal` 中也不是一個好的方案。 ### 2,使用介面 那是否可以將 *eatGrass* 方法提取出來,作為一個介面? 就像這樣: ```java interface EatGrassable { void eatGrass(); } ``` 然後,讓需要有`吃草`能力的動物都去實現該介面,就像這樣: ```java class Rabbit extends Animal implements EatGrassable { public void eatGrass() { System.out.println("I can eat grass."); } public String type() { return "Rabbit"; } } ``` 這樣做可以達到目的,但是,缺點是每個需要`吃草`能力的動物之間就會有重複的程式碼,依然沒有達到程式碼複用的目的。 所以,這種方式還是不能很好的解決問題。 ### 3,使用行為類 我們可以將`吃草`的能力看作一種“行為”,然後使用“行為類”來實現。那麼需要有`吃草`能力的動物,就將`吃草類`的物件,作為自己的屬性。 這些行為類就像一個個的元件,哪些類需要某種功能的元件,就直接拿來用。 下面我們編寫“吃草類”: ```java interface EatGrassable { void eatGrass(); } class EatGreenGrass implements EatGrassable { // 吃綠草 public void eatGrass() { System.out.println("I can eat green grass."); } } class EatDogtailGrass implements EatGrassable { // 吃狗尾草 public void eatGrass() { System.out.println("I can eat dogtail grass."); } } class EatNoGrass implements EatGrassable { // 不是真的吃草 public void eatGrass() { System.out.println("I can not eat grass."); } } ``` 首先建立了一個 `EatGrassable` 介面,但是不用動物類來實現該介面,而是我們建立了一些行為類 `EatGreenGrass` ,`EatDogtailGrass` 和 `EatNoGrass`,這些行為類實現了 `EatGrassable`介面。 這樣,需要吃草的動物,不但能夠吃草,而且可以吃不同種類的草。 那麼,該如何使用 `EatGrassable` 介面呢?需要將 `EatGrassable` 作為 `Animal` 的屬性,如下: ```java abstract class Animal { // EatGrassable 物件作為 Animal 的屬性 protected EatGrassable eg; public Animal() { eg = null; } public void run() { System.out.println("I can run."); } public void drinkWater() { System.out.println("I can drink water."); } public void eatGrass() { if (eg != null) { eg.eatGrass(); } } protected abstract String type(); } ``` 可以看到,`Animal` 中增加了 `eg` 屬性和 `eatGrass` 方法。 其它動物類在**建構函式**中,要初始化 `eg` 屬性: ```java class Dog extends Animal { public Dog() { // Dog 不能吃草 eg = new EatNoGrass(); } public String type() { return "Dog"; } } class Pig extends Animal { public Pig() { eg = new EatGreenGrass(); } public String type() { return "Pig"; } } class Rabbit extends Animal { public Rabbit() { eg = new EatDogtailGrass(); } public String type() { return "Rabbit"; } } ``` 對程式碼測試: ```java public class Strategy { public static void main(String[] args) { Animal dog = new Dog(); Animal pig = new Pig(); Animal rabbit = new Rabbit(); dog.eatGrass(); // I can not eat grass. pig.eatGrass(); // I can eat green grass. rabbit.eatGrass(); // I can eat dogtail grass. } } ``` ### 4,策略模式 實際上,上面的實現方式使用的就是**策略模式**。重點在於 `EatGrassable ` 介面與三個行為類 `EatGreenGrass`,`EatDogtailGrass` 和 `EatNoGrass`。在策略模式中,這些行為類被稱為**演算法族**,所謂的“策略”,可以理解為“演算法”,這些演算法可以互相替換。 **策略模式定義了一系列演算法族,並封裝在類中,它們之間可以互相替換,此模式讓演算法的變化獨立於使用演算法的客戶**。 我將完整的程式碼放在了[這裡](https://github.com/codeshellme/codeshellme.github.io/blob/master/somecode/dp/Strategy.java),供大家參考,類圖如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201225103107536.png?) ### 5,繼承與組合 在一開始的設計中,我們使用的是**繼承(Is-a)** 的方式,但是效果並不是很好。 最終的方案使用了策略模式,它是一種**組合(Has-a)** 關係,即 `Animal` 與 `EatGrassable` 之間的關係。 這也是一種設計原則:**多用組合,少用繼承**,組合關係比繼承關係有更好的彈性。 ### 6,動態設定行為 策略模式不僅重在建立一組演算法(行為類),能夠動態的讓這些演算法互相替換,也是策略模式典型應用。 所謂的“動態”是指,在程式的執行期間,根據配置,使用者輸入等方式,動態的設定演算法。 只需要在 `Animal` 中加入 `setter` 方法即可,如下: ```java abstract class Animal { // 省略了其它程式碼 public void setEatGrassable(EatGrassable eg) { this.eg = eg; } } ``` 使用 `setter` 方法: ```java Animal pig = new Pig(); pig.eatGrass(); // I can eat green grass. pig.setEatGrassable(new EatDogtailGrass()); // 設定新的演算法 pig.eatGrass(); // I can eat dogtail grass. ``` 本來 `pig` 吃的是`綠草`,我們通過 `setter` 方法將 `綠草` 換成了 `狗尾草`,可以看到,演算法的切換非常方便。 ### 7,總結 策略模式定義了一系列**演算法族**,這些演算法族也可以叫作**行為類**。策略模式使用了**組合而非繼承**來構建類之間的關係,組合關係比繼承關係更加有彈性,使用組合也比較容易動態的改變類的行為。 (本節完。) --- **推薦閱讀:** [設計模式之高質量程式碼](https://www.cnblogs.com/codeshell/p/13968620.html) [單例模式-讓一個類只有一個例項](https://www.cnblogs.com/codeshell/p/14177102.html) [工廠模式-將物件的建立封裝起來](https://www.cnblogs.com/codeshell/p/14187677.html) --- *歡迎關注作者公眾號,獲取更多技術乾貨。* ![碼農充電站pro](https://img-blog.csdnimg.cn/20200505082843773.png?#pic_center)