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

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

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

假設咱們要爲動物進行建模,好比狗,豬,兔子等,每種動物的能力是不一樣的。git

1,使用繼承

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

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

而後咱們編寫 DogPigRabbitshell

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 中繼承就行。this

可是,這樣仍是有問題,由於若是將 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

相關文章
相關標籤/搜索