策略模式 --- 动态更改算法
然后就在我们的程序中这样写:
public static void main(String[] args){
FlyMaxPlane plane1 = new FlyMaxPlane();
AttackMaxPlane plane2 = new AttackMaxPlane();
showFunctionOfPlane(plane1);
showFunctionOfPlane(plane2);
}
private void showFunctionOfPlane(Plane plane){
plane.fly();
plane.attack();
}
使用继承可以解决这个问题,但是,继承也有它自己的问题。继承最大的问题就是,基类的改变会传给所有的子类,这是我们类设计者不想看到的。那么,不使用继承不就可以了?接口,就是我们这种情况下最好的替代方案。
使用接口,我们的代码就可以更加灵活。
接口是一个好东西,但如何使用,也是一个重要的问题,就是我们该让什么成为接口?如果我们这里将战斗机这个抽象作为接口,像是这样:
public Interface Plane{
void fly();
void attack();
}
这样就将具体的实现交给实现类,从而避免我们上面的问题。确实如此,但不同型号的战斗机,就算外观差距太大,基本的东西都是不变的,像是重量这些基本的属性,至少在很长的一段时间都不会发生变化,如果用接口的话,我们就不能设置一些共同的属性和方法,当然我们可以将这样的东西交给实现类来实现,这样,代码重复的程度太可怕了!
抽象类还是有抽象类的好处,接口依然不能完全代替,就像上面的例子。接口在很大程度上都被人滥用了,因为它是一个非常好用的东西,尤其是多态的使用。但是类的设计应该贴近现实生活,就像上面战斗机的例子,我们是对战斗机这样的具体东西进行抽象,而且代码应该能体现程序员对待一个问题的思维,并不仅仅是交给计算机自己处理的字节码。滥用接口本身就是对这种原则的破坏,因为很多人都不清楚接口的真正意义。
使用抽象类是为了表达“is-a”关系,而使用接口是为了表达"has-a"关系。继承自一个抽象类,子类本身就是抽象类的一个特例,在分类上它属于抽象类,但是实现一个接口,并不能说,我们的实现类就是一个接口,准确的说法就是我们的实现类具有该接口定义的行为,当然,对于类型识别来说,接口和抽象类是没有什么区别的,都是一个事物的抽象。接口的真正意义是一组行为协议,规定我们的实现类应该具有的行为。也许有些人会说,这样是“is-like-a"关系,这样的说法其实搞错了"is-like-a"关系,"is-like-a"关系本身也属于继承的一种关系,传统意义上的继承应该是完全继承,就是不添加新的功能,只是覆写我们基类的方法,这样子类就可以向上转型为基类而不会出错,但是,现实就是子类会有它们自己的行为,会有自己特有的属性和行为,使得它们无法向上转型,这就是"is-like-a"关系。
理解好接口和抽象类的逻辑意义,我们在设计的时候就能根据现实生活来决定到底应该采用什么样的抽象。上面的例子,我们依然使用抽象类,因为我们需要一个地方来存放所有战斗机都具有的属性和行为,抽象类是一个非常好的选择。接着,我们将会改动的行为抽取出来作为接口。如何判断一个行为是否应该抽取出来,我们就看:如果我们对该行为进行修改,相应的其他代码是否也要进行修改,如果需要,说明这个行为是一个变化的行为因素。这里我们就抽取出飞行和攻击这两个行为。
我们现在抽取出飞行和攻击这两个接口:
public Interface FlyAble{
void fly();
}
public Interface AttackAble{
void attack();
}
这里我们就可能犯一个错误,像是这样:
public class FlyMaxPlane extends Plane implements FlyAble, AttackAble{
void fly(){}
void attack(){}
}
public class AttackMaxPlane extends Plane implements FlyAble, AttackAble{
void fly(){}
void attack(){}
}
为什么会这样写?很简单,因为我们可能有些飞机根本不具有飞行能力,像是这样:
public class NotFlyPlane extends Plane implements AttackAble{
void attack(){}
}
但是,根本不需要我们的子类实现这些接口,接口更大的意义是对象组合,这样根本就失去了接口的优点。要想利用接口的这些优点,我们可以这样建立这两个接口的实现类组,像是这样:
public class FlyMax implements FlyAble{
void fly(){}
}
public class AttackMax implements AttackAble{
void attack(){}
}
然后再在我们的代码中使用这些实现类:
public class FlyMaxPlane extends Plane{
FlyMax = new FlyMax();
FlyMax.fly();
}
这就是使用对象组合的方式,但是这样的方式还不够优雅。这时,策略模式就正式登场了,因为它就是处理对象组合的一种模式。
使用策略模式,我们需要有一个委托类,像是这样:
public class BehaviorChange{
private FlyAble fly;
private AttackAble attack;
private void setFly(FlyAble fly){
this.fly = fly;
}
private void setAttack(AttackAble attack){
this.attack = attack;
}
private void execute(){
fly.fly();
attack.attack();
}
}
接着再在我们的子类中使用这个委托类:
public class FlyMaxPlane extends Plane{
public static void main(String[] args){
BehaviorChange behaviorChange = new BehaviorChange();
behaviorChange.setFly(new FlyMax());
behaviorChange.execute();
}
}
如果战斗机以后的飞行能力发生变化,我们可以动态的更改它的行为,像是这样:
behaviorChange.setFly(new FlyMin());
这样,它就从拥有最大飞行能力变成拥有最小飞行能力!!而且我们客户可以随时更换飞行能力,只要他喜欢。
以上就是策略模式的标准用法,它完全体现了策略模式的意图。但是,继承的问题依然存在,基类的变化依然会传给子类,所以,我们必须保证,基类中的非抽象部分是在很长一段时间内都不会发生变化的。
策略模式有必要使用委托类吗?委托类其实提供的就是一个间接层,我们不需要知道有关于 FlyAble 和 AttackAble 的具体细节,我们只知道,使用 execute()就可以让我们的战斗机飞起来,攻击敌人。这就是封装。不使用委托类的话,我们就必须在子类中显示的调用它们的方法,也就是说我们必须知道 FlyAble 和 AttackAble 的内部代码,如果是委托类的话,我们就不需要知道它们的细节,因为所有的细节都封装在委托类里面,客户要了解的只是委托类中的方法。
到了这里,策略模式的基本内容已经讲完了,通过使用接口来实现对象组合,我们就可以充分的做到代码复用。实现策略模式真的需要继承吗?不一定,因为策略模式只是为了封装一组算法族,然后实现算法的替换而已,只要达到这个目的都可以说是策略模式,就算委托类,也可以没有。
介绍完策略模式后,最后的部分就是针对我们上面提出的三个问题进行解答:
1.使用策略模式的原因?
评论