案例
《植物大战僵尸》这个游戏很多人都玩过,里面有各种不同的植物和僵尸。不同的植物、僵尸各自有不同的特点。假如你要开发这样一款游戏,游戏最开始的版本比较简单,只有两种僵尸:普通僵尸、旗手僵尸。
第一版
类型 | 外观 | 移动 | 攻击 |
---|---|---|---|
普通僵尸 | 普通 | 朝着一个方向 | 咬 |
旗手僵尸 | 普通+手持旗子 | 朝着一个方向 | 咬 |
抽象类:
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
应用场景
- 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
- 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
- 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
优缺点
优点
- 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码转移到父类里面,从而避免重复的代码。
- 策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为。如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样一来算法或行为的使用者就和算法或行为本身混在一起。决定使用哪一种算法或采取哪一种行为的逻辑就和算法或行为的逻辑混合在一起,从而不可能再独立演化。继承使得动态改变算法或行为变得不可能。
- 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。
缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
- 策略模式造成很多的策略类,每个具体策略类都会产生一个新类。有时候可以通过把依赖于环境的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。
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:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去