通过《植物大战僵尸》学策略模式

案例

《植物大战僵尸》这个游戏很多人都玩过,里面有各种不同的植物和僵尸。不同的植物、僵尸各自有不同的特点。假如你要开发这样一款游戏,游戏最开始的版本比较简单,只有两种僵尸:普通僵尸、旗手僵尸。

第一版

类型 外观 移动 攻击
普通僵尸 普通 朝着一个方向
旗手僵尸 普通+手持旗子 朝着一个方向

抽象类:

abstract class AbstractZombie{
    
    public abstract void display();

    public void attack(){
        System.out.println("咬");
    }

    public void move(){
        System.out.println("一步一步移动");
    }
}

普通僵尸:

class NormalZombie extends AbstractZombie{

    @Override
    public void display() {
        System.out.println("我是普通僵尸");
    }
}

旗手僵尸:

class FlagZombie extends AbstractZombie{

    @Override
    public void display() {
        System.out.println("我是旗手僵尸");
    }
}

测试:

public class ZombieTest {

    public static void main(String[] args) {
        AbstractZombie normalZombie = new NormalZombie();
        AbstractZombie flagZombie = new FlagZombie();

        flagZombie.display();
        flagZombie.move();
        flagZombie.attack();
        System.out.println("---------------");

        normalZombie.display();
        normalZombie.move();
        normalZombie.attack();
    }

}

执行结果:

我是旗手僵尸
一步一步移动
咬
---------------
我是普通僵尸
一步一步移动
咬

完美!游戏可以上线了。

但是没过多久你发现你开发的这个游戏玩家越来越少,打开评论一看,都在吐槽这个游戏僵尸种类太少,玩了几次就没啥意思了。
这好办,再加几种僵尸呗,于是就有了第二版:

第二版

类型 外观 移动 攻击
普通僵尸 普通 朝着一个方向
旗手僵尸 普通+手持旗子 朝着一个方向
投篮僵尸 带着篮球 朝着一个方向 投球
撑杆僵尸 带着杆 会撑杆跳

这简单,我再写俩僵尸类,然后重写跟抽象僵尸类(AbstractZombie)不一样的方法实现就行。

投篮僵尸:

class BallZombie extends AbstractZombie{

    @Override
    public void display() {
        System.out.println("我是投球僵尸");
    }

    @Override
    public void attack() {
        System.out.println("投球");
    }
}

撑杆僵尸:

class PoleVaultZombie extends AbstractZombie{

    @Override
    public void display() {
        System.out.println("我是撑杆僵尸");
    }

    @Override
    public void move() {
        System.out.println("撑杆跳");
    }
}

你突然想到还可以加一种高级撑杆跳僵尸,移动方式跟撑杆跳僵尸一样,但是这个僵尸攻击方式是拿杆戳植物。好办,这个直接继承撑杆僵尸。

class SuperPoleVaultZombie extends PoleVaultZombie{

    @Override
    public void display() {
        System.out.println("我是高级撑杆僵尸");
    }

    @Override
    public void attack() {
        System.out.println("戳");
    }
}

搞定!第二版上线!

但是历史总是惊人的相似,没过多久用户们又玩腻了,你心想这届用户真难带,又要加僵尸了。不过还好你已经得心应手了,不就是各种继承吗,你继承写的贼6~

但是只是无脑加僵尸哪够,还有一堆用户吐槽你这游戏的BUG:你这僵尸遇到障碍物都不带停的,遇到植物应该停止移动,开始攻击;而且撑杆僵尸明明应该在没有遇到植物时候是跑的,遇到第一个植物才会撑杆跳。所以这些僵尸的各个行为在不同情况下是不一样的,这可咋办,你已经写了一堆僵尸类了,难倒要挨个类加判断逻辑改变行为?真叫人头秃!!!这个时候你想到了开闭原则:对扩展开放,对修改关闭。看来你的代码需要重构一下了。

僵尸的移动方式和攻击方式有不同的实现方式,而且要可以动态改变。先把这两个行为抽取成接口。

移动行为接口:

interface MoveBehavior {
    void move();
}

攻击行为接口

interface AttackBehavior {
    void attack();
}

抽象类

abstract class AbstractZombie {

    MoveBehavior moveBehavior;
    AttackBehavior attackBehavior;

    public AbstractZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior){
        this.moveBehavior = moveBehavior;
        this.attackBehavior = attackBehavior;
    }

    abstract void display();

    void move(){
        moveBehavior.move();
    }

    void attack(){
        attackBehavior.attack();
    }

    public void setAttackBehavior(AttackBehavior attackBehavior) {
        this.attackBehavior = attackBehavior;
    }

    public AttackBehavior getAttackBehavior() {
        return attackBehavior;
    }

    public void setMoveBehavior(MoveBehavior moveBehavior) {
        this.moveBehavior = moveBehavior;
    }

    public MoveBehavior getMoveBehavior() {
        return moveBehavior;
    }
}

各种僵尸子类:

class NormalZombie extends AbstractZombie {

    public NormalZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior) {
        super(moveBehavior, attackBehavior);
    }

    @Override
    void display() {
        System.out.println("我是普通僵尸");
    }

}

class FlagZombie extends AbstractZombie {

    public FlagZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior) {
        super(moveBehavior, attackBehavior);
    }

    @Override
    void display() {
        System.out.println("我是旗手僵尸");
    }
}

class BallZombie extends AbstractZombie {

    public BallZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior) {
        super(moveBehavior, attackBehavior);
    }

    @Override
    void display() {
        System.out.println("我是投篮僵尸");
    }
}

class PoleVaultZombie extends AbstractZombie {

    public PoleVaultZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior) {
        super(moveBehavior, attackBehavior);
    }

    @Override
    void display() {
        System.out.println("我是撑杆僵尸");
    }
}

移动行为子类:

class StepByStepMove implements MoveBehavior{

    @Override
    public void move() {
        System.out.println("一步一步移动");
    }
}

class RunMove implements MoveBehavior{

    @Override
    public void move() {
        System.out.println("跑");
    }
}

class PoleVaultMove implements MoveBehavior{

    @Override
    public void move() {
        System.out.println("撑杆跳");
    }
}

攻击行为子类:

class BiteAttack implements AttackBehavior{

    @Override
    public void attack() {
        System.out.println("咬");
    }
}

class BallAttack implements AttackBehavior{

    @Override
    public void attack() {
        System.out.println("扔球");
    }
}

测试:

public class StrategyTest {
    
    public static void main(String[] args) {
        
        NormalZombie normalZombie = new NormalZombie(new StepByStepMove(), new BiteAttack());
        normalZombie.display();
        normalZombie.move();
        normalZombie.attack();
        System.out.println("-----------");

        FlagZombie flagZombie = new FlagZombie(new StepByStepMove(), new BiteAttack());
        flagZombie.display();
        flagZombie.move();
        flagZombie.attack();
        System.out.println("-----------");

        BallZombie ballZombie = new BallZombie(new StepByStepMove(), new BallAttack());
        ballZombie.display();
        ballZombie.move();
        ballZombie.attack();
        System.out.println("-----------");

        PoleVaultZombie poleVaultZombie = new PoleVaultZombie(new RunMove(), new BiteAttack());
        poleVaultZombie.display();
        poleVaultZombie.move();
        //如果撑杆僵尸遇到了第一个植物
        System.out.println("我遇到了第一个植物");
        poleVaultZombie.setMoveBehavior(new PoleVaultMove());
        poleVaultZombie.move();
        poleVaultZombie.attack();

    }
}

执行结果:

我是普通僵尸
一步一步移动
咬
-----------
我是旗手僵尸
一步一步移动
咬
-----------
我是投篮僵尸
一步一步移动
扔球
-----------
我是撑杆僵尸
跑
我遇到了第一个植物
撑杆跳
咬

从此以后你想改某个僵尸的行为都不需要去改僵尸类,直接传一个不同的行为实例给僵尸就行。而且可以根据不同情况随便修改各个僵尸的行为。完美!

这就是策略模式

模式定义

策略模式(Strategy)指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。
策略模式定义了算法家族,分别封装了起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到算法的使用者。

类图

策略模式类图

案例中僵尸就是环境角色Context,两个行为接口就是抽象策略Strategy,具体的移动、攻击子类就是具体实现策略ConcreteStrategy

应用场景

  1. 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
  2. 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
  3. 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。

优缺点

优点

  1. 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码转移到父类里面,从而避免重复的代码。
  2. 策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为。如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样一来算法或行为的使用者就和算法或行为本身混在一起。决定使用哪一种算法或采取哪一种行为的逻辑就和算法或行为的逻辑混合在一起,从而不可能再独立演化。继承使得动态改变算法或行为变得不可能。
  3. 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。

缺点

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
  2. 策略模式造成很多的策略类,每个具体策略类都会产生一个新类。有时候可以通过把依赖于环境的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。

JDK中的策略模式

比较器Comparator

在Java的集合框架中,经常需要传入一个比较器Comparator用于排序,这使用的就是策略模式。

什么?你说你没有用过比较器?看一下下面的测试代码你就明白了

假如现在有很多只猫,需要排个序

定义一个Cat类:

class Cat implements Comparable<Cat>{

    int age;
    int weight;

    public Cat(int age,int weight){
        this.age = age;
        this.weight = weight;
    }

    @Override
    public int compareTo(Cat o) {
        return this.age - o.age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", weight=" + weight +
                '}';
    }
}

这Cat里有age、weight,虽然实现了Comparable接口的compareTo方法,但是这个比较逻辑是不变的,永远是根据年龄排序,哪天你想根据体重排序就要去修改Cat里的compareTo方法。

所以我们定义两个比较器:

class SortByAge implements Comparator<Cat> {

    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.age - o2.age;
    }
}

class SortByWeight implements Comparator<Cat> {

    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.weight - o2.weight;
    }
}

测试:

public class CatSortTest {

    public static void main(String[] args) {
        List<Cat> list = new ArrayList<>();
        list.sort(new SortByAge());
        list.add(new Cat(1,8));
        list.add(new Cat(3,2));
        list.add(new Cat(4,5));
        list.add(new Cat(2,7));
        list.add(new Cat(5,3));

        System.out.println("使用Cat类中实现了Comparable的compareTo方法排序");
        Collections.sort(list);
        System.out.println(list);
        System.out.println("---------");

        System.out.println("使用SortByAge比较器排序");
        //和list.sort(new SortByAge())效果一样
        Collections.sort(list,new SortByAge());
        System.out.println(list);
        System.out.println("---------");

        System.out.println("使用SortByWeight比较器排序");
        //和list.sort(new SortByWeight())效果一样
        Collections.sort(list,new SortByWeight());
        System.out.println(list);

    }
}

打印结果:

使用Cat类中实现了Comparable的compareTo方法排序
[Cat{age=1, weight=8}, Cat{age=2, weight=7}, Cat{age=3, weight=2}, Cat{age=4, weight=5}, Cat{age=5, weight=3}]
---------
使用SortByAge比较器排序
[Cat{age=1, weight=8}, Cat{age=2, weight=7}, Cat{age=3, weight=2}, Cat{age=4, weight=5}, Cat{age=5, weight=3}]
---------
使用SortByWeight比较器排序
[Cat{age=3, weight=2}, Cat{age=5, weight=3}, Cat{age=4, weight=5}, Cat{age=2, weight=7}, Cat{age=1, weight=8}]

这里Collections就是环境角色Context,Comparator就是抽象策略Strategy,两个比较器实现就是具体实现策略ConcreteStrategy

ThreadPoolExecutor中的拒绝策略

在创建线程池时,需要传入拒绝策略,当创建新线程使当前运行的线程数超过maximumPoolSize时,将会使用传入的拒绝策略进行处理。这也是策略模式。

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务
  • DiscardPolicy:不处理,直接丢弃
  • DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343