设计模式系列 — 策略模式

667 阅读8分钟

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

前言

23种设计模式快速记忆的请看上面第一篇,本篇和大家一起来学习策略模式相关内容。 image.png

模式定义

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式的变化独立于算法的使用者。 image.png

策略模式中体现了几个设计原则: ①把变化的代码从不变的代码中分离出来; ②针对接口编程而不是具体类; ③多用组合/聚合,少用继承,策略模式中Context通过聚合使用策略。

解决的问题

在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护,使用策略模式将算法的责任和本身进行解耦。

模式组成

组成(角色)作用
抽象策略角色(Strategy)定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
具体策略角色(ConcreteStrategy)实现了抽象策略定义的接口,提供具体的算法实现。
环境类(Context)持有一个策略类的应用,最终供客户端调用

实例说明

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

第一版

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

image.png

步骤1:定义抽象策略角色,抽象类

abstract class AbstractZombie{

    public abstract void display();

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

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

步骤2:定义具体的策略角色,普通僵尸

class NormalZombie extends AbstractZombie{

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

步骤3:定义具体的策略角色,旗手僵尸

class FlagZombie extends AbstractZombie{

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

步骤4:测试

public class StrategyPattern {

    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();
    }
}

输出结果

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

完美!游戏可以上线了。

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

第二版

类型外观移动攻击
普通僵尸普通朝着一个方向移动
旗手僵尸普通+手持旗子朝着一个方向移动
大头僵尸大头朝着一个方向移动头撞
石膏僵尸石膏装一瘸一拐武器
XXX僵尸.........

image.png

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

步骤1:定义具体的策略角色,大头僵尸

class BigZombie extends AbstractZombie {
    @Override
    public void display() {
        System.out.println("我是大头僵尸");
    }

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

步骤2:定义具体的策略角色,石膏僵尸

class GypsumZombie extends AbstractZombie{

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

    @Override
    public void move() {
        System.out.println("一瘸一拐");
    }

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

OK!第二版上线!

没过多久用户们又玩腻了,用户玩腻了,又要加僵尸了。不过还好你已经得心应手了,不就是各种继承吗。 image.png

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

第三版

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

步骤1:定义移动行为接口

interface MoveBehavior {
    void move();
}

步骤2:定义攻击行为接口

interface AttackBehavior {
    void attack();
}

步骤3:定义抽象策略角色,抽象类

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;
    }
}

步骤4:定义各种僵尸子类

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 BigZombie extends AbstractZombie {
    public BigZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior) {
        super(moveBehavior, attackBehavior);
    }

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

class GypsumZombie extends AbstractZombie {
    public GypsumZombie(MoveBehavior moveBehavior, AttackBehavior attackBehavior) {
        super(moveBehavior, attackBehavior);
    }

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

步骤5:定义移动行为子类

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

class LameMove implements MoveBehavior {
    @Override
    public void move() {
        System.out.println("一瘸一拐");
    }
}

步骤6:定义攻击行为子类

class BiteAttack implements AttackBehavior{
    @Override
    public void attack() {
        System.out.println("咬");
    }
}
class HeadAttack implements AttackBehavior{
    @Override
    public void attack() {
        System.out.println("头撞");
    }
}
class ArmsAttack implements AttackBehavior{
    @Override
    public void attack() {
        System.out.println("武器");
    }
}

测试

public class StrategyPattern {
    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("-----------");
        // 大头僵尸
        BigZombie bigZombie = new BigZombie(new StepByStepMove(), new HeadAttack());
        bigZombie.display();
        bigZombie.move();
        bigZombie.attack();
        System.out.println("-----------");
        // 石膏僵尸
        GypsumZombie gypsumZombie = new GypsumZombie(new LameMove(), new BiteAttack());
        gypsumZombie.display();
        gypsumZombie.move();
        //如果石膏僵尸遇到了第一个植物
        System.out.println("我遇到了第一个植物");
        gypsumZombie.setAttackBehavior(new ArmsAttack());
        gypsumZombie.move();
        gypsumZombie.attack();
    }
}

执行结果

我是普通僵尸
一步一步移动
咬
-----------
我是旗手僵尸
一步一步移动
咬
-----------
我是大头僵尸
一步一步移动
头撞
-----------
我是石膏僵尸
一瘸一拐
我遇到了第一个植物
一瘸一拐
武器

优点

  1. 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
  2. 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
  3. 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
  4. 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  5. 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。

缺点

  1. 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  2. 策略模式造成很多的策略类。

应用场景

  1. 当你有很多类似的类,但它们执行某些行为的方式不同时,请使用此策略;

  2. 使用该模式将类的业务逻辑与算法的实现细节隔离开来,这些算法在逻辑上下文中可能不那么重要;

  3. 当你的类具有大量的条件运算符,并且在同一算法的不同变体之间切换时,请使用此模式。

源码中的应用

#JDK
java.util.Comparator
java.util.concurrent.ThreadPoolExecutor

#Spring
org.springframework.beans.factory.support.InstantiationStrategy
......

比较器Comparator

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

我们看一个案例

定义一个 Person 类

class Person {

    int age;
    int height;

    public Person(int age, int height) {
        this.age = age;
        this.height = height;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

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

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

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

//策略1 根据年龄排序
class SortByAge implements Comparator<Person> {

    @Override
    public int compare(Person o1, Person o2) {
        if (o1.getAge() > o2.getAge()) {
            return 1;
        } else if (o1.getAge() < o2.getAge()) {
            return -1;
        }
        return 0;
    }
}

//策略2 根据身高排序
class SortByHeight implements Comparator<Person> {

    @Override
    public int compare(Person o1, Person o2) {
        if (o1.getHeight() > o2.getHeight()) {
            return 1;
        } else if (o1.getHeight() < o2.getHeight()) {
            return -1;
        }
        return 0;
    }
}

测试:

public class ComparatorTest {
    public static void main(String[] args) {
        Person[] persons =
                new Person[]{new Person(10, 111), new Person(18, 99), new Person(15, 122)};

        Arrays.sort(persons, new SortByHeight());

        print(persons);

    }

    static void print(Person[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }

    }
}

打印结果:

Person{age=18, height=99}
Person{age=10, height=111}
Person{age=15, height=122}

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

ThreadPoolExecutor中的拒绝策略

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

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务
  • DiscardPolicy:不处理,直接丢弃
  • DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去

Spring中bean实例化策略

image.png 接口InstantiationStrategy是实例化策略接口类,它定义了三个实例化接口,然后SimpleInstantiationStrategy实现了该策略,它主要做一些简单的根据构造函数实例号bean的工作,然后CglibSubclassingInstantiationStrategy又继承了SimpleInstantiationStrategy新增了方法注入方式根据cglib生成代理类实例化方法。

在AbstractAutowireCapableBeanFactory中管理了该策略的一个对象,默认是CglibSubclassingInstantiationStrategy策略,运行时候可以通过setInstantiationStrategy改变实例化策略,如果你自己写个个策略的话。

PS:以上代码提交在 Githubgithub.com/Niuh-Study/…

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。