一. 什么是策略模式
这里引用百度百科的定义。策略模式是指有一定行动内容的相对稳定的策略名称。策略模式在古代中又称“计策”,简称“计”,如《汉书·高帝纪上》:“汉王从其计”。这里的“计”指的就是计谋、策略。策略模式具有相对稳定的形式,如“避实就虚”、“出奇制胜”等。一定的策略模式,既可应用于战略决策,也可应用于战术决策;既可实施于大系统的全局性行动,也可实施于大系统的局部性行动。
二. 策略模式的优缺点
无论哪种模式都有其优缺点,当然我们每次在编写代码的时候需要考虑下其利弊。
策略模式的优点:
1. 提供了快速替换继承关系的办法:案例中的我们能快速的替换僵尸类,速度,攻击类型。
2. 减少代码中的if else 等判断语句:我们在更换攻击状态的时候不用去判断,当达到某个条件时直接切换攻击实现类就可以了。比如上文中的普通攻击变超级攻击。
3. 实现的选择:策略模式可以提供不同的实现。
策略模式的缺点:
1. 策略模式会造成很多策略类:上文中的所有速度实现类,攻击实现类……
2. 通讯开销很大:比如我们上面的速度接口的speed需要传递3个参数。这三个参数对有的僵尸类是有意义的,对大部分僵尸无意义,这时候就会造成没必要的通讯开销。
3. 必须自行知道所有的实现类有何不同?每次新建一个僵尸类的时候必须要知道所有的速度类及攻击实现类的不同。此时这些就会暴露出具体的实现问题。因此只有当行为类和角色行为相关的时候才需要使用策略模式。
三. 策略模式的设计原则
设计原则有很多,这里直说策略模式中使用到的,参看实例思考哪些地方有用到下面的设计模式:
1. 封装变化(找出应用中可能需要变化之处,把它们独立出来,不要和哪些不需要变化的代码混在一起。)
2. 针对接口,超类编程,而不是针对实现编程。
3. 多用组合,少用继承。
四. 策略模式的实现和案例
假设我们现在正在开发一款类似植物大战僵尸的游戏产品要求:所有僵尸的必要条件是可以动,可以攻击,外观上可以区分就可以了。
这没问题,写一个抽象类Character,然后让所有角色(僵尸)继承这个类就可以了。So Easy.
抽象类
public abstract class Character { void move(){ System.err.println("move"); } void attack(){ System.err.println("attack"); } abstract void display(); }
1 public class RedHeadZombie extends Character {//红头僵尸 2 @Override 3 void display() { 4 System.err.println("My head is Red"); 5 } 6 }
绿头僵尸:
public class GreenHeadZombie extends Character { @Override void display() { System.err.println("My head is Green"); } }
写完了,问题解决,正准备看看新闻资讯的时候,产品经理走过来对我说,我们需要给我们每个僵尸设置一个前进速度,而且速度要有区别,不能所有僵尸都一样的。好吧,设置速度嘛,也简单,我在抽象类里面添加一个speed的方法,让红头僵尸和绿头僵尸都分别实现这个方法。也不难,分分种搞定。
public abstract class Character {//抽象类 …… abstract void speed(); }
public class GreenHeadZombie extends Character {//绿头僵尸 …… @Override void speed() { System.err.println("speed is 1.0"); } }
public class RedHeadZombie extends Character {//红头僵尸 …… @Override void speed() { System.err.println("speed is 3.0"); } }
好吧,该有的速度也有了,提交代码,这下满足你了吧,产品经理这时走过来告诉你,僵尸的攻击方式不能一样的,你怎么让所有僵尸的攻击方式都一样了,这肯定不行的。好像有点道理的样子,那我就改咯。我重写所有的attack方法不就行了。
public class RedHeadZombie extends Character {//红头僵尸 …… @Override void attack() { System.err.println("RedHeadZombie's attack"); } }
public class GreenHeadZombie extends Character {//绿头僵尸 …… @Override void attack() { System.err.println("GreenHeadZombie's attack"); } }
这时候产品经理觉得需要再添加两种僵尸(短腿僵尸,无攻击力僵尸),短腿僵尸速度很慢,但是可以远程攻击,无攻击力僵尸(不知道用来干嘛,可能是后勤部队,用来给其他僵尸运送军火的)。可以移动,但是不能攻击。还是乖乖写吧,继续写两个类ShortLegZombie(短腿),NoAttackZombie(无攻击)继承自Character.
public class ShortLegZombie extends Character {//短腿僵尸 @Override void display() { System.err.println("I'm ShortLegZombie"); } @Override void speed() { System.err.println("speed is slow"); } @Override void attack() { System.err.println("ShortLegZombie's attack"); } }
public class NoAttackZombie extends Character {//无攻击力僵尸 @Override void display() { System.err.println("I'm NoAttackZombie"); } @Override void speed() { System.err.println("speed is 5.0"); } @Override void attack() { //nothing to do } }
又添加了几个僵尸类,先拿给产品看一下。产品一看,眼睛微眯,皱着眉头:你这不行呀,你这僵尸怎么速度都是不变的?你遇到障碍物肯定的减速,没有障碍物得加速呀,这个要修改。僵尸在起跑线应该都不具有攻击方式,你这个攻击方式需要修改下。 听你这么一说好像有点道理,当僵尸满足某个条件时修改下速度状态和攻击方式。当是我现在已经创建十几个僵尸类了,这要修改的工作量有点大呀!而且还有几十种都没写上去呢!
能不能优化一下代码,让新建一个僵尸类的时候尽量用几行代码就搞定?而且可以随时修改其状态。
这样我们重写梳理下思路:我们编写一个抽象类(Character),让所有的僵尸都继承这个类。速度和攻击方式是可以动态改变的。我们将需要动态改变的部分抽取出来。我们分别编写ISpeedBehavior和IAttackBehavior两个接口,将所有的速度和攻击行为都抽取出来。速度和攻击这里暂时写三种:
接下里我们重新整理我们的代码:
抽象类Character及子类
public abstract class Character {//抽象类 public IAttackBehavior mIAttackBehavior; public ISpeedBehavior mISpeedBehavior; void move(){ System.err.println("move"); } void attack(){ mIAttackBehavior.attack(); } void speed(){ mISpeedBehavior.speed(); } public void setIAttackBehavior(IAttackBehavior IAttackBehavior) { mIAttackBehavior = IAttackBehavior; } public void setISpeedBehavior(ISpeedBehavior ISpeedBehavior) { mISpeedBehavior = ISpeedBehavior; } abstract void display(); }
public class RedHeadZombie extends Character {//红头僵尸 public RedHeadZombie(IAttackBehavior iAttackBehavior, ISpeedBehavior iSpeedBehavior) { mIAttackBehavior = iAttackBehavior; mISpeedBehavior = iSpeedBehavior; } @Override void display() { System.err.println("My head is Red"); } }
攻击行为类接口:IAttackBehavior及实现类
public interface IAttackBehavior { void attack(); }
public class OrdinaryAttack implements IAttackBehavior { @Override public void attack() { System.err.println("I use ordinary attack"); } }
速度接口ISpeedBehavior及子类:
public interface ISpeedBehavior { void speed(); }
public class NormalSpeed implements ISpeedBehavior{ @Override public void speed() { System.err.println("My speed is Normal!"); } }
最终动态修改速度和攻击的操作:
/** * 策略模式 */ public class StrategyActivity extends AppCompatActivity { @BindView(R.id.zombie_tv) TextView mZombieTv; @BindView(R.id.speed_tv) TextView mSpeedTv; @BindView(R.id.attack_tv) TextView mAttackTv; private Character mCharacter; private IAttackBehavior mIAttackBehavior; private ISpeedBehavior mISpeedBehavior; private int mRandomZombieNum = 0; private int mRandomSpeedNum = 0; private int mRandomAttackNum = 0; private Random mRandom; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_strategy); ButterKnife.bind(this); //默认设置 mIAttackBehavior = new OrdinaryAttack(); mISpeedBehavior = new NormalSpeed(); mCharacter = new RedHeadZombie(mIAttackBehavior,mISpeedBehavior); mRandom = new Random(); } @OnClick({R.id.btn_change_zombie, R.id.btn_change_speed, R.id.btn_change_attack}) public void onViewClicked(View view) { switch (view.getId()) { case R.id.btn_change_zombie: mRandomZombieNum = mRandom.nextInt(4); switch (mRandomZombieNum){ case 0: mCharacter = new RedHeadZombie(mIAttackBehavior,mISpeedBehavior); mZombieTv.setText("红头僵尸"); break; case 1: mCharacter = new GreenHeadZombie(mIAttackBehavior,mISpeedBehavior); mZombieTv.setText("绿头僵尸"); break; case 2: mCharacter = new ShortLegZombie(mIAttackBehavior,mISpeedBehavior); mZombieTv.setText("短腿僵尸"); break; case 3: mCharacter = new NoAttackZombie(mIAttackBehavior,mISpeedBehavior); mZombieTv.setText("无攻击力僵尸"); break; } mCharacter.attack(); mCharacter.speed(); break; case R.id.btn_change_speed: mRandomSpeedNum = mRandom.nextInt(3); switch (mRandomSpeedNum){ case 0: mISpeedBehavior = new NormalSpeed(); mSpeedTv.setText("My speed is normal"); break; case 1: mISpeedBehavior = new SlowSpeed(); mSpeedTv.setText("My speed is slow"); break; case 2: mISpeedBehavior = new FastSpeed(); mSpeedTv.setText("My speed is fast"); break; } mCharacter.setISpeedBehavior(mISpeedBehavior); mCharacter.speed(); break; case R.id.btn_change_attack: mRandomAttackNum = mRandom.nextInt(3); switch (mRandomAttackNum){ case 0: mIAttackBehavior = new OrdinaryAttack(); mAttackTv.setText("I use ordinary rtdinary attack"); break; case 1: mIAttackBehavior = new ReinforceAttack(); mAttackTv.setText("I use ordinary reinforce attack"); break; case 2: mIAttackBehavior = new SuperAttack(); mAttackTv.setText("I use ordinary super attack"); break; case 3: break; } mCharacter.setIAttackBehavior(mIAttackBehavior); mCharacter.attack(); break; } } }
这样就可以随便添加角色,随便修改速度和攻击状态。
五. 策略模式总结
现在我们回顾下刚开始说的策略模式所用到的三个设计原则:
1. 封装变化。我们将变化的速度和攻击行为封装出来,供动态改变调用。
2. 针对接口,超类编程,不针对实现编程。我们用ISpeedBehavior和IAttackBehavior接口去代表速度和攻击行为,每种具体的行为实现其中一个接口。
3. 多用组合,少用继承。组合最大的好处就是具有弹性,给代码维护和需求修改提供极大的便利。刚开始我们使用的就是继承的方式,在添加大量僵尸类的时候,代码的维护及修改量将非常大。后面优化过后我们就使用了速度和攻击类及僵尸类的组合去编写代码。