策略模式-定义一个算法族
目录
公号:码农充电站pro
本篇来介绍策略模式(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:返回动物的种类,比如“狗”,“兔子”。这是一个抽象方法,子类要去实现。
然后我们编写 Dog
,Pig
和 Rabbit
:
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 方法,如下:
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 不具有实际的能力,就像这样:
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
接口,但是不用动物类来实现该接口,而是我们创建了一些行为类 EatGreenGrass
,EatDogtailGrass
和 EatNoGrass
,这些行为类实现了 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
接口与三个行为类 EatGreenGrass
,EatDogtailGrass
和 EatNoGrass
。在策略模式中,这些行为类被称为算法族,所谓的“策略”,可以理解为“算法”,这些算法可以互相替换。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
我将完整的代码放在了这里,供大家参考,类图如下:
5,继承与组合
在一开始的设计中,我们使用的是继承(Is-a) 的方式,但是效果并不是很好。
最终的方案使用了策略模式,它是一种组合(Has-a) 关系,即 Animal
与 EatGrassable
之间的关系。
这也是一种设计原则:多用组合,少用继承,组合关系比继承关系有更好的弹性。
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,总结
策略模式定义了一系列算法族,这些算法族也可以叫作行为类。策略模式使用了组合而非继承来构建类之间的关系,组合关系比继承关系更加有弹性,使用组合也比较容易动态的改变类的行为。
(本节完。)
推荐阅读:
欢迎关注作者公众号,获取更多技术干货。
文章作者 @码农加油站
上次更改 2020-12-25