设计模式系列 — 策略模式
点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。
前言
- 23种设计模式速记
- 单例(singleton)模式
- 工厂方法(factory method)模式
- 抽象工厂(abstract factory)模式
- 建造者/构建器(builder)模式
- 原型(prototype)模式
- 享元(flyweight)模式
- 外观(facade)模式
- 适配器(adapter)模式
- 装饰(decorator)模式
- 观察者(observer)模式
- 持续更新中......
23种设计模式快速记忆的请看上面第一篇,本篇和大家一起来学习策略模式相关内容。
模式定义
定义了算法族,分别封装起来,让它们之间可以互相替换,此模式的变化独立于算法的使用者。
策略模式中体现了几个设计原则:
①把变化的代码从不变的代码中分离出来;
②针对接口编程而不是具体类;
③多用组合/聚合,少用继承,策略模式中Context通过聚合使用策略。
解决的问题
在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护,使用策略模式将算法的责任和本身进行解耦。
模式组成
组成(角色) | 作用 |
---|---|
抽象策略角色(Strategy) | 定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。 |
具体策略角色(ConcreteStrategy) | 实现了抽象策略定义的接口,提供具体的算法实现。 |
环境类(Context) | 持有一个策略类的应用,最终供客户端调用 |
实例说明
《植物大战僵尸》这个游戏很多人都玩过,里面有各种不同的植物和僵尸。不同的植物、僵尸各自有不同的特点。假如你要开发这样一款游戏,游戏最开始的版本比较简单,只有两种僵尸:普通僵尸、旗手僵尸。
第一版
类型 | 外观 | 移动 | 攻击 |
---|---|---|---|
普通僵尸 | 普通 | 朝着一个方向移动 | 咬 |
旗手僵尸 | 普通+手持旗子 | 朝着一个方向移动 | 咬 |
步骤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僵尸 | ... | ... | ... |
这简单,我再写俩僵尸类,然后重写跟抽象僵尸类(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();
}
}
执行结果:
我是普通僵尸
一步一步移动
咬
-----------
我是旗手僵尸
一步一步移动
咬
-----------
我是大头僵尸
一步一步移动
头撞
-----------
我是石膏僵尸
一瘸一拐
我遇到了第一个植物
一瘸一拐
武器
优点
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
缺点
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。
应用场景
- 当你有很多类似的类,但它们执行某些行为的方式不同时,请使用此策略;
- 使用该模式将类的业务逻辑与算法的实现细节隔离开来,这些算法在逻辑上下文中可能不那么重要;
- 当你的类具有大量的条件运算符,并且在同一算法的不同变体之间切换时,请使用此模式。
源码中的应用
#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:以上代码提交在 Github :https://github.com/Niuh-Study/niuh-designpatterns.git
文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。