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

碼農充電站發表於2020-12-28

公號:碼農充電站pro
主頁:https://codeshellme.github.io

本篇來介紹策略模式(Strategy Design Pattern)。

假設我們要為動物進行建模,比如狗,豬,兔子等,每種動物的能力是不同的。

1,使用繼承

首先你可能想到用繼承的方式來實現,所以我們編寫了下面這個 Animal 類:

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:返回動物的種類,比如“狗”,“兔子”。這是一個抽象方法,子類要去實現。

然後我們編寫 DogPigRabbit

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 中的 rundrinkWater,並且都實現了自己的 type 方法。

現在我們想給 PigRabbit 加入吃草的能力,最直接的辦法是分別在這兩個類中加入 eatGrass 方法,如下:

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";
    }
}

上面程式碼能夠達到目的,但是不夠好,因為PigRabbit 中的 eatGrass 一模一樣,是重複程式碼,程式碼沒能複用。

為了解決程式碼複用,我們可以將 eatGrass 方法放到 Animal 中,利用繼承的特性,PigRabbit 中就不需要編寫 eatGrass 方法,而直接從 Animal 中繼承就行。

但是,這樣還是有問題,因為如果將 eatGrass 放在 Animal 中,Dog 中也會有 eatGrass ,而我們並不想讓 Dog 擁有吃草的能力。

也許你會說,我們可以在 Dog 中將 eatGrass 覆蓋重寫,讓 eatGrass 不具有實際的能力,就像這樣:

class Dog extends Animal {
    public void eatGrass() {
        // 什麼都不寫,就沒有了吃草的能力
    }

    public String type() {
        return "Rabbit";
    }
}

這樣做雖然能到達目的,但是並不優雅。如果 Animal 的子類特別多的話,就會有很多子類都得這樣覆蓋 eatGrass 方法。

所以,將 eatGrass 放在 Animal 中也不是一個好的方案。

2,使用介面

那是否可以將 eatGrass 方法提取出來,作為一個介面?

就像這樣:

interface EatGrassable {
    void eatGrass();
}

然後,讓需要有吃草能力的動物都去實現該介面,就像這樣:

class Rabbit extends Animal implements EatGrassable {
    public void eatGrass() {
        System.out.println("I can eat grass.");
    }

    public String type() {
        return "Rabbit";
    }
}

這樣做可以達到目的,但是,缺點是每個需要吃草能力的動物之間就會有重複的程式碼,依然沒有達到程式碼複用的目的。

所以,這種方式還是不能很好的解決問題。

3,使用行為類

我們可以將吃草的能力看作一種“行為”,然後使用“行為類”來實現。那麼需要有吃草能力的動物,就將吃草類的物件,作為自己的屬性。

這些行為類就像一個個的元件,哪些類需要某種功能的元件,就直接拿來用。

下面我們編寫“吃草類”:

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 介面,但是不用動物類來實現該介面,而是我們建立了一些行為類 EatGreenGrassEatDogtailGrassEatNoGrass,這些行為類實現了 EatGrassable介面。

這樣,需要吃草的動物,不但能夠吃草,而且可以吃不同種類的草。

那麼,該如何使用 EatGrassable 介面呢?需要將 EatGrassable 作為 Animal 的屬性,如下:

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 屬性:

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";
    }
}

對程式碼測試:

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 介面與三個行為類 EatGreenGrassEatDogtailGrassEatNoGrass。在策略模式中,這些行為類被稱為演算法族,所謂的“策略”,可以理解為“演算法”,這些演算法可以互相替換。

策略模式定義了一系列演算法族,並封裝在類中,它們之間可以互相替換,此模式讓演算法的變化獨立於使用演算法的客戶

我將完整的程式碼放在了這裡,供大家參考,類圖如下:

在這裡插入圖片描述

5,繼承與組合

在一開始的設計中,我們使用的是繼承(Is-a) 的方式,但是效果並不是很好。

最終的方案使用了策略模式,它是一種組合(Has-a) 關係,即 AnimalEatGrassable 之間的關係。

這也是一種設計原則:多用組合,少用繼承,組合關係比繼承關係有更好的彈性。

6,動態設定行為

策略模式不僅重在建立一組演算法(行為類),能夠動態的讓這些演算法互相替換,也是策略模式典型應用。

所謂的“動態”是指,在程式的執行期間,根據配置,使用者輸入等方式,動態的設定演算法。

只需要在 Animal 中加入 setter 方法即可,如下:

abstract class Animal {
    // 省略了其它程式碼
    
    public void setEatGrassable(EatGrassable eg) {
        this.eg = eg;
    }
}

使用 setter 方法:

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,總結

策略模式定義了一系列演算法族,這些演算法組也可以叫作行為類。策略模式使用了組合而非繼承來構建類之間的關係,組合關係比繼承關係更加有彈性,使用組合也比較容易動態的改變類的行為。

(本節完。)


推薦閱讀:

設計模式之高質量程式碼

單例模式-讓一個類只有一個例項

工廠模式-將物件的建立封裝起來


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro

相關文章