文章目录
1 策略模式
在我们什么都不会的情况下,我们先无需知道什么是策略模式,我们要做的是:在了解什么是设计模式之前,让我们先引入一个例子。
1.1 模拟鸭子
如果要用java做一个模拟鸭子的游戏,那么对于刚学完JavaSE的人来说,它会想的就是设计一个鸭子的超类(Duck)
,然后让其他的鸭子继承此类。其中,所有的鸭子都会叫(Quack)
和游泳(Swin)
。而每一种鸭子的外观不同,所以display方法是抽象的,每个鸭子子类要在自己的类上实现display的具体外观。
后面主管下来要求,说这个鸭子必须得会飞了。所以为了速度,我们直接在超类Duck上加上fly方法,这样,所有的鸭子都会飞了。
但是问题出现了,模拟鸭子的游戏中有一种橡皮鸭子,这种鸭子作为玩具是不会飞的,如果在Duck上面直接加fly就会导致橡皮鸭子也会飞,而如果不在Duck上面加则需要去其他会飞的鸭子类上一个一个写fly方法。而且对于橡皮鸭来说,其并不像真鸭一样会嘎嘎叫,而是吱吱叫(squeak)
。
或许有一种方法可以解决上述问题,可以在Duck上加fly方法,然后在橡皮鸭子类上重写该方法,使它不会飞即可。虽然这种做法能够解决一时的问题,但是后面如果又加入其他有特色的鸭,那就会导致代码复用性不高,代码冗余。
还有一种办法是,fly方法不写在超类里,而是写在接口里,哪只鸭子会飞就去调用。明显地,这种做法对于鸭子多数会飞的情况简直是噩梦。
1.2 设计原则
为此,我们引入第一个设计原则:
如果应用中有可能变化的地方,把他找出来并且独立,不要和那些不需要变化的代码混在一起。
既然fly和quack都有可能变化,那我们干脆把它们独立出来。我们可以建立两组类,一组是实现fly的,一组实现quack。在每组类中实现各自的动作。我们把这种独立出来具有多种选择的类叫做策略类
,对应策略类中算法的不同,每组类都含有许多算法,我们把某组类叫做算法族
。
我们在前面做的一切都是为了我们设计的鸭子有弹性,即可以各式各样。而各式各样的原因大多是因为行为不一。既然行为不一样,那我们在Duck类中就应该包含设定行为的方法,使其可以动态地改变鸭子的行为。
为此,我们引入第二个设计原则:
针对接口编程,而不是针对实现编程。
但是与前面提到的不一样的是,我们设计的接口并不是接在实例类上的,而是接在行为类上的。这样的话,鸭子的行为将被放在分开的类中,此类专门提供某行为的实现。这样的话鸭子类就不需要知道行为的实现细节。
我们现在来体会一个例子,假如现在有一个抽象类Animal,有两个具体的实现Dog和Cat,这两个类都是基础Animal。
//Animal
make Sound(); //指定抽象类叫声
//Dog
makeSound(){
bark(); //汪汪叫
}
bark();
//Cat
makeSound(){
meow(); //喵喵叫
}
meow();
如果是针对实现编程,那我们会做如下所示的事:
Dog d = new Dog();
d.bark();
如果是针对接口/超类型编程,那我们会做如下所示的事:
Animal animal = new Dog();
animal.makeSound();
也就是说,我们并不关心子类是什么,只关心子类能否正确的实现其对应的父类行为即可。
现在让我们回到鸭子。如果我们有两个接口,一个接口是飞接口(FlyBehavior)
,还有一个叫接口(QuackBehavior)
。我们还有两个对应的行为类,其负责实现具体的行为。如图所示:
这样的设计使得飞行类组合叫声类组都已经和鸭子类无关了,甚至于,我们还可以设计多种新的行为,比如鸭子会跳舞。
或许这样新奇的设计会让人感到奇怪,用类代表行为而不是代表东西,这样是否不太恰当。实际上,行为也是可以理解为东西的,看具体情况而定,例如在鸭子的问题上,鸭子的飞行类可以有翅膀每秒拍动几下、飞的最大高度、飞的速度等行为属性。
1.3 整合行为
现在我们已经设计好了,该整合了。鸭子不太可能变化的行为处于自身超类Duck,而鸭子变化的类叫声类和飞行类被我们分离出两组类来,一个类对应一个情况。
接下来,我们需要动态控制鸭子的飞行和叫声行为。我们把Duck类中和其子类中所有的fly和quack移除,然后用peerformFly和performQuack来取代Duck类中的fly和quack。
也就是说,Duck类里面现在有如下的东西:
//Duck
FlyBehavior flyBehavior //接口
QuackBehavior quackBehavior //接口
performQuack()
swin()
display()
performFly()
我们在performFly和performQuack两个方法中委托两个行为接口来帮我们做叫和飞这两件事,如下:
public void performQuack()
{
quackBehavior.quack();
}
整合完成后。我们来尝试创建一个实例类,比如绿头鸭(MallardDuck)。
public class MallardDuck extends Duck
{
public MallardDuck(){
quackBehavior = new Quack(); //会呱呱叫
flyBehavior = new FlyWithWings(); //会飞
}
public void display(){
System.out.println("i'm a real Mallard duck");
}
}
1.4 模拟鸭子代码的代码
/*鸭子超类*/
public abstract class Duck {
//声明两个接口变量
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){}
public abstract void display();
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
public void swim(){
System.out.println("All ducks float,even decoys!");
}
}
/*鸭子飞行接口*/
public interface FlyBehavior {
void fly();
}
/*鸭子叫声接口*/
public interface QuackBehavior {
void quack();
}
/*鸭子飞行类*/
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("I'm flying!");
}
}
/*鸭子不能飞行类*/
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("I can't fly");
}
}
/*鸭子呱呱叫类*/
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("Quack");
}
}
/*鸭子吱吱叫类*/
public class Squeak implements QuackBehavior{
@Override
public void quack() {
System.out.println("Squeak");
}
}
/*鸭子不会叫类*/
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("<<Silence>>");
}
}
/*实例绿头鸭*/
public class MallardDuck extends Duck{
@Override
public void display() {
System.out.println("I'm a real Mallard duck");
}
public MallardDuck(){
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
}
/*测试类*/
public class Main {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
}
}
1.5 动态设定行为
在Duck类中,我们加入两个新方法,这两个方法的好处是,通过调用这两个方法传入一个匿名内部类,该内部类实现了飞行接口,这样通过了接口多态的形式改变了实例类内部提前设定好的特性,从而提高了动态设定行为的动态性。
public void setFlyBehavior(FlyBehavior fb)
{
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb)
{
quackBehavior = qb;
}
如果会看前面的绿头鸭,我们会发现我们前面设定行为是在实例类里面的,而结果上面代码的操作,我们可以通过插入一个新的功能类来改变原先设定好的功能,这样耦合性提高了。
public class MallardDuck extends Duck
{
public MallardDuck(){
quackBehavior = new Quack(); //会呱呱叫
flyBehavior = new FlyWithWings(); //会飞
} public void display(){
System.out.println(“i’m a real Mallard duck”);
}}
现在我们可以随意调动上面的两个方法来控制新的实例鸭子的行为了,我们建立一个模型鸭来验证我们的说法。
public class ModelDuck extends Duck{
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("I'm a model duck");
}
}
建立一个新的行为,这种行为是给鸭子绑定一个火箭让其飞行。
public class FlyRocketPowered implements FlyBehavior{
@Override
public void fly() {
System.out.println("I'm flying with a rocket!");
}
}
在上面模型鸭的设定中,它原本是不会飞的,但是我们可以在设置飞行的方法中传入参数,使其能够飞。
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
1.6 重新查看整体
1.7 继承和组合
实际上,在开发中应该转变一些思维方式。比如鸭子是一个会飞的生物(继承),我们重新看做鸭子有一个会飞的技能会更好(组合)。这也是我们第三个设计原则:
多用组合,少用继承。
是的,很多人都会有疑惑,实际上用继承
写起代码来更加地简洁,但是后期维护起来确是组合
要更加地方便。
1.8 总结
是的,对于不熟悉上面流程的同学,可以从头到尾再看一遍,上面的鸭子代码中,用到了我们学的第一个设计模式:策略模式
。不仅如此,我们还用到了三个设计原则,让我们重新来认识一下它们吧。
- 单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
- 依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
- 合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的。
当然,在这里我们不细讲这些原则,只是了解了个大概,在后面的学习中我们会详细阐述,现在重要的问题是,我们应该回过头重新审视一下策略模式。
在软件开发中,我们常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径,这种模式我们叫做策略模式
。
策略模式实用性强,扩展性好,在软件开发中得以广泛使用,是使用频率较高的设计模式之一。其典型的应用莫过于Sun公司开发JavaSE中的容器布局管理了。其基本结构示意图如下所示:
对于容器环境来说,容器环境该有的组件基本不变,所以将JPanel放在Container下没什么毛病,而对于布局Layout来说,布局有各式各样的布局,根据对应的软件开发来选择合适的布局,其中LayoutManger作为接口充当了抽象策略角色,当我们需要哪类具体的情况,只需要调用其子类即可。
1.9 优劣期间应用场景
从上面的一系列场景可以看出,策略模式适用处理较为灵活问题。譬如想要方便扩展、更换、修改某类功能,那么你就可以把功能对应的算法封装起来,然后采用策略模式。
-
主要优点
策略模式提供了对
开闭原则
的完美支持。代码书写人员在不修改以前代码的情况下,也能扩展新的功能。策略模式可以管理相关的算法族。如上所述,策略模式可以定义一个接口来管理旗下的算法族,从而达到动态控制的效果。
策略模式提供了一种可以替换继承关系的办法。如果随时变化的功能和不太可能变化的功能混写于一个类中,那么这不符合单一职责原则。但是使用策略模式可以提供一种动态的切换效果。
使用策略模式可以避免多重条件选择语句。回想起前面的模拟鸭子游戏,如果不使用算法族,那么我们需要使用大量的控制语句来判断鸭子到底应该有什么功能。
-
主要缺点
代码书写人必须知道所有的策略类。如上模拟鸭子的代码中,如果你不知道模型鸭有绑定火箭飞行这个策略类,那你就无法调用。
策略类容易造成控制粒度过细。如果每个变化就要写一个策略类,那么写的代码就会过多。
无法同时使用同个功能下的多个策略类。如果你的飞行接口下只规定了三种情况,飞行、不会飞、绑定火箭飞行。那么你的实例类是不可以兼具一个接口下管理的多个策略类的,换言之,你的鸭不可能不会飞又会飞。
-
适用场景
一个功能需要动态地选择不同的策略。如果是这种情况,你可以把某个功能对应的不同情况写入策略类,然后根据面向对象的多态性来选择具体的策略算法。
一个对象由很多行为。比如博主开发的Galgame游戏就是因为游戏功能过多,每个功能的行为也有很多,从而使用了策略模式。
不希望使用者知道复杂的数据结构。在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性和安全性。
2.0 参照资料
算法的封装与切换——策略模式(四)_刘伟技术博客-CSDN博客_算法切换
HeadFirst设计模式——策略模式
本人技术有限,如果博客有语法或者文字错误,欢迎指正!