策略模式-定义一个算法族

公号:码农充电站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

posted @ 2020-12-28 10:59  码农充电站  阅读(536)  评论(2编辑  收藏  举报