《Head First 设计模式》[01] 策略模式
《Head First 设计模式》(点击查看详情)
1、写在前面的话
之前在列书单的时候,看网友对于设计模式的推荐里说,设计模式的书类别都大同小异,于是自己就选择了Head First系列,基于之前读过《Head First Java》,被文中生动的图片和讲解所吸引,相信这本书也能带给我更好的体验,关键在于,设计模式作为Java中更巧妙的部分,需要的不光是知道它是什么,更应该去理解为什么,才能更好地去掌握和应用,而Head First系列图书正是为了如此。
对于这本书的读书笔记,就以每个设计模式来分章进行记录吧,好了,不多叨叨,正式进入读书学习章节吧。
2、策略模式
我们知道,单纯利用继承来提供父类其下子类的各种行为,是难以维护的,我们无法确保所有子类的行为。比如我们有一只鸭子Duck作为父类,写下了fly()方法,可是涉及到例如玩具鸭ToyDuck作为子类的时候,它并不会飞啊?
那么这样,我们干脆把一些方法独立出来做成接口,比如一个Flyable的接口,让会飞的鸭子去实现它,玩具鸭不实现它不就行了,那么这个设计怎么样?
然而这个方法也并不是那么好,接口的方式虽然可以解决部分问题,可是接口不具有实现代码,这样一来,我们对于fly()就无法达到代码复用的目的,这意味着,如果某时我们需要修改一个方法(比如飞行是利用翅膀,随着科技化现在鸭子是用火箭飞行了),你必须从接口往下追踪并修改每个定义该行为的类,不仅工作量重复,更可能不小心造成新的错误。
2.1 剥离变化之处
为了解决这种问题,我们在设计之初要利用好一个设计原则:
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
1
1
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
(这个概念很简单,却几乎是每个设计模式的精神所在,所有模式都提供了一套方法让“系统中某部分改变不会影响其他部分”)
我们回到刚才的鸭子案例,随着鸭子的不同fly()可能不同,那么我们把它提取出来,建立新的类(接口)来代表这个行为。
2.2 接口编程,独立实现
为了动态地决定鸭子的飞行行为,我们要利用到第二个设计原则:
针对接口编程,而不是针对实现编程
1
1
针对接口编程,而不是针对实现编程
我们利用接口来代表行为,比如FlyBehavior接口,这一次,我们不让鸭子类来实现这个接口,而是专门制造一组其他类来实现接口,这个类我们称之为“行为类”,即由行为类实现接口而不是Duck类来实现。这样一来,“实现”不必死死地捆绑在子类中。
这样的好处在于:
- 动作可以被其他对象复用,因为独立为动作,而跟鸭子类无关了,比如麻雀也可以使用fly()了
- 我们可以新增行为,且不影响既有行为类,也不影响使用飞行行为的鸭子类
2.3 整合
做完这些,你可能要问了,行为既然委托给别人了,不在鸭子身上了,那么我们如何整合鸭子的行为呢?
- 将剥离部分作为实例变量,加入到类中(即把FlyBehavior接口作为变量设置到Duck类中)
- 设置类似原来fly()的方法performFly(),调用已经作为实例变量的接口中定义的方法fly()
public class Duck {
protected FlyBehavior flyBehavior;
private String name;
private int size;
public String performFly() {
return flyBehavior.fly();
}
public String swim() {
return "Swimming ~ ~ ~";
}
}
13
1
public class Duck {
2
protected FlyBehavior flyBehavior;
3
private String name;
4
private int size;
5
6
public String performFly() {
7
return flyBehavior.fly();
8
}
9
10
public String swim() {
11
return "Swimming ~ ~ ~";
12
}
13
}
- 其他类实现行为接口
//通过翅膀飞行
public class FlyingByWings implements FlyBehavior {
public String fly() {
return "Flying By Wings~";
}
}
7
1
//通过翅膀飞行
2
public class FlyingByWings implements FlyBehavior {
3
4
public String fly() {
5
return "Flying By Wings~";
6
}
7
}
//通过火箭飞行
public class FlyingByRocket implements FlyBehavior {
public String fly() {
return "Flying By super-cool Rocket!!!";
}
}
7
1
//通过火箭飞行
2
public class FlyingByRocket implements FlyBehavior {
3
4
public String fly() {
5
return "Flying By super-cool Rocket!!!";
6
}
7
}
- 子类初始化时设定想要的实例变量
public class RocketDuck extends Duck {
public RocketDuck() {
flyBehavior = new FlyingByRocket();
}
}
7
1
public class RocketDuck extends Duck {
2
3
public RocketDuck() {
4
flyBehavior = new FlyingByRocket();
5
}
6
7
}
- 编辑测试类
public class Test {
public static void main(String[] args) {
RocketDuck rocketDuck = new RocketDuck();
System.out.println(rocketDuck.performFly());
}
}
6
1
public class Test {
2
public static void main(String[] args) {
3
RocketDuck rocketDuck = new RocketDuck();
4
System.out.println(rocketDuck.performFly());
5
}
6
}
另外,我们还可以在父类Duck中对行为接口设置setter,这样一来,我们随时都可以动态地设定方法了!
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
3
1
public void setFlyBehavior(FlyBehavior flyBehavior) {
2
this.flyBehavior = flyBehavior;
3
}
3、模式总结
回忆一下我们从最开始的想法到现在的方式:
- 最开始,我们直接让子类继承父类来复用其方法,但是子类不同对于方法也不同,类的行为划分不清,同时方法覆盖的方式并不灵活;
- 然后,改变成通过实现接口来实现行为,行为划分清晰,可无法使用代码复用,后期维护困难;
- 最终,我们利用接口和多态原理,剥离行为,让一些专门的类来实现行为接口,让不同的子类去初始化不同的行为(实现类调用接口引用),完美解决了问题。
第三种的方式优秀在于,父类是 “有一个行为类型”,这种和继承不同的是,行为不是继承而来的,而是和适当的对象 “组合” 而来的,这里,也利用到了我么要说到的第三个设计模式:
多用组合,少用继承
1
1
多用组合,少用继承
最后,我们来看看策略模式的正儿八经的定义吧:
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
策略模式把对象本身和运算规则区分开来,其功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性的思想。
x
1
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
2
3
策略模式把对象本身和运算规则区分开来,其功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性的思想。
其适用性常在于:
- 许多相关的类仅仅是行为有异。 “策略”提供了一种用多个行为中的一个行为来配置一个类的方法。即一个系统需要动态地在几种算法中选择一种;
- 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间 / 时间权衡的算法。当这些变体实现为一个算法的类层次时 ,可以使用策略模式;
- 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构;
- 一个类定义了多种行为 , 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
策略模式的场景示例:游戏中,每个角色只能使用一种武器,在游戏过程中可以更换武器:
4、本文涉及的设计原则汇总
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 多用组合,少用继承