- 设计模式解析一 工厂模式的不同
- 设计模式解析二 结构模式三剑客
- 设计模式解析三 行为模式三剑客
- 设计模式解析四 模板方法模式和外观模式
- 设计模式解析五 观察者模式和桥接模式
- 设计模式解析六 单例模式
一. 前言
前一章讲了结构模式三剑客,而为什么会把装饰模式、代理模式和适配器模式称为结构模式三剑客呢,实际上这三个模式在结构模式使用使用最为广泛,而且最容易弄混,因为这三种设计模式非常相像。
实际上行为模式中也有这样三个设计模式,他们使用广泛,但又非常相似,让人迷惑,那就是接下来要讲的三个设计模式:策略模式、状态模式、命令模式。
相信很多java程序开发者应该都看过阿里巴巴Java开发手册,如果没看过的话,也建议看一看,对于自己写代码来说还是很有帮助,其中编程规约第七节有这样一段描述:
最初没学习设计模式的时候我就在想,什么是策略模式,状态模式呢?平时看到自己的if else if else 代码就感觉很头疼,感觉代码很乱,有没有更优雅的方式呢?
二. 策略模式
相信我们都应该玩过游戏植物大战僵尸,在这里面有多种多样的植物,还有多种多样的僵尸,我们今天就从这个植物来开始。
我们的每种植物都有自己的攻击方式,这是一种行为,比如豌豆可以吐豌豆进行攻击,比如大嘴花可以直接吃掉面前的僵尸,那么写成代码是怎么样呢?
伪代码:
if ("绿豌豆") {
System.out.println("发射绿豌豆");
} else if ("蓝豌豆") {
System.out.println("发射蓝豌豆");
} else if ("大嘴花") {
System.out.println("一口吃掉");
}
不论怎么看,这样都不够优雅,那么如果用策略来封装这个攻击算法会如何呢?让我们从头开始来做这个例子,首先是定义我们植物接口:
public interface Plant {
void attack();
}
绿豌豆:
public class GreenPeas implements Plant {
private int strikingDistance;
private PlantBehavior behavior;
public GreenPeas(int strikingDistance, PlantBehavior behavior) {
this.strikingDistance = strikingDistance;
this.behavior = behavior;
}
@Override
public void attack() {
behavior.act(strikingDistance);
}
}
大嘴花:
public class BigMouthFlower implements Plant {
private int strikingDistance;
private PlantBehavior behavior;
public BigMouthFlower(int strikingDistance, PlantBehavior behavior) {
this.strikingDistance = strikingDistance;
this.behavior = behavior;
}
@Override
public void attack() {
behavior.act(strikingDistance);
}
}
炸弹:
public class Bomb implements Plant {
private int strikingDistance;
private PlantBehavior behavior ;
public Bomb(int strikingDistance, PlantBehavior behavior) {
this.strikingDistance = strikingDistance;
this.behavior = behavior;
}
@Override
public void attack() {
behavior.act(strikingDistance);
}
}
然后是我们的行为接口:
public interface PlantBehavior {
void act(int distance);
}
绿豌豆攻击行为:
public class GreenPeasBehavior implements PlantBehavior {
@Override
public void act(int distance) {
System.out.println("发射绿豌豆");
}
}
炸弹行为:
public class BombBehavior implements PlantBehavior {
@Override
public void act(int distance) {
System.out.println("爆炸。。。。");
}
}
大嘴花行为:
public class BigMouthFlowerBehavior implements PlantBehavior {
@Override
public void act(int distance) {
System.out.println("吃掉面前的僵尸");
}
}
开始工作了:
Plant peas = new GreenPeas(10, new GreenPeasBehavior());
Plant bomb = new Bomb(10, new BombBehavior());
Plant bigMouth = new BigMouthFlower(1, new BigMouthFlowerBehavior());
peas.attack();
bomb.attack();
bigMouth.attack();
输出:
发射绿豌豆
爆炸。。。。
吃掉面前的僵尸
接下来,突然公司说要出一个疯狂版的植物大战僵尸,希望绿豌豆可以爆炸:
Plant peas = new GreenPeas(10, new BombBehavior());
peas.attack();
输出:
爆炸。。。。
发现没有,只要是实现了PlantBehavior
接口的任意行为,他们都可以随意替换,哪怕让绿豌豆一口吃掉面前的僵尸也行,这就是策略模式了。
我们可以省略if else大段的代码插入到业务代码中,也许大家会问,我还是要根据植物的类型选择不同的攻击行为,没错,不过我们可以利用一些其他模式来帮助我们做这个动作,比如工厂模式。
定义:
策略模式定义一个算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
三. 状态模式
状态模式和策略模式非常相似,他们都有根据不同情况得到不同的算法,但状态模式更注重状态之间更迭,比如我们的自动贩卖机,当我们投币成功后,可以选择我们要的商品,然后商品会掉出来,当没有投币的时候,选择商品则不会出来,而库存没有的话,选择商品,商品不会出来,钱会自己退出来,那么进行一次归纳,这个贩卖机实际上有以下三种状态:
- 等待投币状态
- 投币完成状态
- 售磬状态
这三种状态应该是一种顺序的更迭,先从等待投币,再到投币,再到售罄(当然,没卖光的话就再次进入等待投币状态),首先定义一个状态,状态只会有两个行为,就是点按商品按钮请求得到商品,和投币动作,在不同状态下动作是不同的,状态的转换也不同:
public interface State {
void insertCoin();
void get();
}
等待投币状态:
public class NoCoinState implements State {
private ShopMachine shopMachine;
public NoCoinState(ShopMachine shopMachine) {
this.shopMachine = shopMachine;
}
@Override
public void insertCoin() {
shopMachine.setCurrentState(shopMachine.getHavecoin());
System.out.println("投币成功,请取商品");
}
@Override
public void get() {
System.out.println("请投币");
}
}
投币完成状态:
public class HaveCoinState implements State {
private ShopMachine shopMachine;
public HaveCoinState(ShopMachine shopMachine) {
this.shopMachine = shopMachine;
}
@Override
public void insertCoin() {
System.out.println("你已经投过币了");
}
@Override
public void get() {
shopMachine.decreaseStock();
System.out.println("得到商品");
if (shopMachine.getStock() > 0) {
shopMachine.setCurrentState(shopMachine.getNocoin());
} else {
shopMachine.setCurrentState(shopMachine.getSoldOut());
}
}
}
售磬状态:
public class SoldOutState implements State {
private ShopMachine shopMachine;
public SoldOutState(ShopMachine shopMachine) {
this.shopMachine = shopMachine;
}
@Override
public void insertCoin() {
System.out.println("商品卖光了,退币");
}
@Override
public void get() {
System.out.println("商品卖光了");
}
}
商品售卖机:
public class ShopMachine {
private int stock;
private State nocoin;
private State havecoin;
private State soldOut;
private State currentState;
public ShopMachine(int stock) {
this.stock = stock;
this.nocoin = new NoCoinState(this);
this.havecoin = new HaveCoinState(this);
this.soldOut = new SoldOutState(this);
this.currentState = nocoin;
}
public void insertCoin() {
this.currentState.insertCoin();
}
public void get() {
this.currentState.get();
}
public void setCurrentState(State currentState) {
this.currentState = currentState;
}
...
}
然后运行我们的代码:
public static void main(String[] args) {
ShopMachine machine = new ShopMachine(2);
machine.insertCoin();
machine.get();
machine.insertCoin();
machine.get();
machine.insertCoin();
machine.get();
}
输出结果:
投币成功,请取商品
得到商品
投币成功,请取商品
得到商品
商品卖光了,退币
商品卖光了
我们只设置了两个库存,当投币了两次以后就跳到了售磬状态,进入这个状态便不能恢复到其他状态了,只能等运维人员来补货了。
定义:
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像改变了它的类。
相信大家也看到了,状态模式和策略模式看起来非常相似,上面的策略模式只是封装了植物的攻击行为,下面的状态模式其实也就是封装了投币行为和获取商品的行为。那么区别在哪呢?
区别就是:
- 状态模式注重状态在内部随着时间流逝的自我改变,调用者不关心现在,将来会是什么状态,有哪些状态
- 而策略模式,需要调用者明确指定使用哪种策略
看起来策略模式更加灵活,但状态模式在有些固定状态更迭流程的场景下可以运行的更简单。
四. 命令模式
家家户户都少不了电视,我很喜欢看电视,现在都流行使用机顶盒,但存在一个问题,使用机顶盒的遥控器只能遥控机顶盒,当想关闭电视,直接控制电视的音量就不行了,这时候大部分机顶盒遥控器都为我们提供了一个学习区,可以操作学习区的按钮学习我们电视遥控器上的功能,比如待机按钮,比如声音调节,我们可以任意学习,那么这是怎么做到的呢?
它的代码怎么写的我不知道,但是如果从java的角度来说,这时候不得不提到命令模式,命令模式允许我们对指令进行封装,然后任意的装载指令,首先定义一个遥控器:
public class RemoteController {
private Commond red;
private Commond up;
private Commond down;
private Commond left;
private Commond right;
public void pressRed() {
red.execute();
}
public void pressUp() {
up.execute();
}
public void pressDown() {
down.execute();
}
public void pressLeft() {
left.execute();
}
public void pressRight() {
right.execute();
}
...
}
定义命令接口:
public interface Commond {
void execute();
}
定义一个电视对象:
public class Television {
/**
* 0 电视处于关机状态
* 1 电视处于开机状态
*/
private int state = 0;
private int vol = 5;
private int channel = 1;
public Television() {
}
public void onOff() {
if (state == 0) {
System.out.println("打开电视");
this.state = 1;
} else {
System.out.println("关闭电视");
this.state = 0;
}
}
public void addVol() {
this.vol++;
System.out.println("音量增加到:" + vol);
}
public void decreaseVol() {
this.vol--;
System.out.println("音量减少到:" + vol);
}
public void addChannel() {
this.channel++;
System.out.println("频道增加到:" + channel);
}
public void decreaseChannel() {
this.channel--;
System.out.println("频道减少到:" + channel);
}
}
定义开机关机命令:
public class CommondOnOff implements Commond {
private Television television ;
public CommondOnOff(Television television) {
this.television = television;
}
@Override
public void execute() {
television.onOff();
}
}
声音增加减少命令:
public class CommondAddVol implements Commond {
private Television television ;
public CommondAddVol(Television television) {
this.television = television;
}
@Override
public void execute() {
television.addVol();
}
}
public class CommondDecreaseVol implements Commond {
private Television television ;
public CommondDecreaseVol(Television television) {
this.television = television;
}
@Override
public void execute() {
this.television.decreaseVol();
}
}
频道增减命令:
public class CommondAddChannel implements Commond {
private Television television ;
public CommondAddChannel(Television television) {
this.television = television;
}
@Override
public void execute() {
television.addChannel();
}
}
public class CommondDecreaseChannel implements Commond {
private Television television;
public CommondDecreaseChannel(Television television) {
this.television = television;
}
@Override
public void execute() {
television.decreaseChannel();
}
}
接下来进行组装:
public static void main(String[] args) {
Television tv = new Television();
RemoteController control = new RemoteController();
control.setRed(new CommondOnOff(tv));
control.setUp(new CommondAddVol(tv));
control.setDown(new CommondDecreaseVol(tv));
control.setLeft(new CommondDecreaseChannel(tv));
control.setRight(new CommondAddChannel(tv));
control.pressRed();
control.pressUp();
control.pressDown();
control.pressRight();
control.pressLeft();
control.pressRed();
}
运行结果:
打开电视
音量增加到:6
音量减少到:5
频道增加到:2
频道减少到:1
关闭电视
这样就实现了一个简单的命令模式,当然这是最简单的。
命令模式的角色分为客户端、执行者、接收者,上面的遥控器对象是执行者和,而电视则是命令接收者。
- 客户端(Client)负责装载发送命令,也只有发送者才清楚自己要执行的是什么命令
- 接收者(Receiver)接收命令发来的请求,做实际的工作
- 而执行者(Invoker)负责执行命令,执行者也不清楚自己执行的是什么命令
命令模式的核心在于将要执行的操作封装成命令,将命令内部操作对执行者透明。
通常我们在使用命令模式可能并不会这么简单,上面的客户端在装载命令后还是要调用遥控器的按钮来执行命令,在实际编程中有些业务场景我们还可以将命令的发送和执行进行解耦,比如我们将命令模式的接受者设计为一个队列,发送者不断将命令加入到队列中,然后设计一个执行者不断去队列中获取命令来执行。
上面这段话有没有很熟悉,仔细思考,我们的线程池不就是这样一种设计嘛?客户端不断把实现了Runnable
接口的命令加入到线程池队列中,而执行者ThreadPoolExecutor
则不断去队列中获取命令来执行。
同样的,命令模式还可以在命令中增加撤销操作,思考一些我们系统编辑的撤销操作时怎么实现的?
定义:
在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合。这就是命令模式。
五. 结语
讲到这里实际上三种设计模式的不同也已经体现出来了:
- 策略模式注重对算法的封装和相互替代
- 状态模式注重向外部请求隐藏对象内部状态流程的自我转换
- 而命令模式则注重命令的请求和执行的解耦
设计模式的不同往往都体现在使用的意图上,注意不要搞混哦?
下一章回聊到策略模式的孪生兄弟:模板方法模式