设计模式,个人理解是一种微妙的东西,它的存在,就像是衣服搭配一样,要么就是根据形象搭配,要么就是根据环境搭配,要么就是根据其他穿着搭配。美好的搭配,可以奠稳形象,可以锦上添花。
平时我们可以从概念上理解各种设计模式(32种设计模式)的概念以及使用场景,当我们在真实项目中遇到类似场景时,就能有联想到适合的设计模式并加以使用。
在这里,我们讨论一下策略模式和状态模式,因为刚巧在最近的项目中有用到这两种设计模式。
一、策略模式
项目背景:我司产品是智能音箱,该音箱的手势交互是一块触摸板,可以想象为一块手机屏幕,只是没有显示,仅仅响应用户的手势输入,例如,上下滑动表示音量增减,单击表示暂停播放,左右滑动表示切换上下首歌曲等。问题在于,音箱有两个场景,一个作为智能音箱响应云端指令和音乐,一个作为蓝牙音箱响应蓝牙控制端的指令和音乐,场景抽象为,多个类只表现在行为不同,可以在运行时动态切换要执行的行为;
1、音箱的触摸板可以对当前播放进行控制,比如上一首,下一首,播放,暂停,音量增减等;
2、但是由于当前播放的音频分为在线播放和蓝牙播放,如果没有一个好的设计的话,每次滑动触摸板的时候,需要判断当前的音频是来自蓝牙的还是来自网络在线的,会特别的繁琐。
所以,在这里,我想到了使用策略模式去套用这个场景。
使用方式:
1、抽象一个策略角色(Strategy角色),通过一个接口来实现,在接口里面封装具体策略类实现的方法,我们项目在里面封装了play(),stop(),next(),pre(),volume()等方法;
2、环境角色(Context),持有策略(Strategy)的引用,在里面调用策略的方法;
3、具体策略角色(ConcreteStrategy),包含了具体的算法或者行为。
具体实现:1、定义策略接口
public interface MusicStrategy {
void play();
void stop();
void next();
void pre();
void volume()
}
2、定义环境角色
public class MusicControl {
private MusicStrategy musicStrategy;
//传入具体的策略
public MusicControl (MusicStrategy musicStrategy) {
this.musicStrategy = musicStrategy;
}
//具体的策略实现
public void play() {
musicStrategy.play();
}
。。。。。。
}
3、定义具体的策略角色
public class BTStrategy extends MusicStrategy {
@Override
public void play() {
。。。
}
。。。。。。
}
public class OnlineStrategy extends MusicStrategy {
@Override
public void play() {
。。。
}
。。。。。。
}
4、在触摸板调用处使用
MusicStrategy musicStrategy = new BTStrategy();
MusicControl musicControl = new MusicControl(musicStrategy);
musicControl.play()
整个实现流程大概如此(纯手敲的代码,格式有点丑,见谅),这样,就可以省却了每次在调用行为方法时需要一系列的if...else...的判断,结构和代码都有了清晰的展现
二、状态模式
项目背景:整个智能音箱的链路涉及到唤醒--->倾听----->识别---->播报,大概如下图所示
其中,1,2 的过程是串行过程,3过程可并行执行,意思是,必须从唤醒--->倾听--->识别这个流式串行过程,而且一旦唤醒了,必须这个流式执行完才能进行下一个唤醒,不能同时存在两个串行。其实很多语音方案都是如此设计,如果我们不遵循这个原则,可能会导致一些问题(多线程问题,内存泄露等),场景抽象为对象的行为依赖于其所在的状态,当其状态改变时,所有依赖这个对象都得到更新。
基于此状态的场景,我们可以套用状态模式优化这个过程;
使用方式:
1、抽象一个语音的行为接口VoiceState,我们这里使用抽象类表示,在里面定义所有的方法wakeup(),listening(),thinking(),speaking()
2、定义一个环境类Context,其中持有VoiceState的引用,通过该VoiceState的引用调起相应的方法;
3、具体的状态子类,继承语音行为接口VoiceState,进行具体的操作,并且进行状态的转化,该状态的变化体现需体现在Context环境类中
具体实现:
1、定义语音接口VoiceState;
public abstract class VoiceState{
protected Context context;
public void setContext(Context context) {
this.context = context;
}
public abstract void wakeup();
public abstract void listening();
public abstract void thinking();
public abstract void speaking();
}
2、定义环境类Context
public class Context{
//定义所有的状态
public final static WakeUpState wakeupstate = new WakeUpState();
public final static ThinkingState thinkingState = new ThinkingState();
public final static ListeningState listeningState = new ListeningState();
public final static SpeakingState speakingState = new SpeakingState();
private VoiceState voiceState;
public VoiceState getState(){
return this.voiceState;
}
public void setVoiceState(VoiceState voiceState){
this.voiceState = voiceState;
this.voiceState.setContext(this);
}
public void wakeup() {
this.voiceState.wakeup();
}
public void listening() {
this.voiceState.listening();
}
public void thinking() {
this.voiceState.thinking();
}
public void speaking() {
this.voiceState.speaking();
}
}
3、具体的状态子类
public class WakeUp extends VoiceState {
@Override
public void wakeup(){
//不能再次执行唤醒
}
@Override
public void thinking() {
//未到识别阶段
}
public void listening(){
super.context.setVoiceState(Context.listenState);
super.context.getVoiceState().listening();
}
。。。。。。
}
余下的子类省略。。。。。。
4、调用端
Context context = new Context();
//在唤醒成功的回调中,调用
context.setVoliceState(new WakeUpState());
context.wake();
//然后依次在每个回调中(listening,thinking,speaking)通过context调用相应的方法,让状态在其内部转化,这样可以防止在调用的时候由于状态不一致导致的错误
通过这个状态流程,可以很自然的在识别完了之后,可同时进入唤醒状态和播报状态,防止了在某些异常情况下,无法进入唤醒状态的重大问题。
三、策略模式和状态模式的异同
看了上诉两个例子的使用方式,我们会有一种疑惑,觉得策略模式和状态模式非常的相像;确实,他们拥有很多相似点,甚至可以说UML图也差不多是一样的,但是本质上是不同的。
1、策略模式封装了一组行为或者算法,它允许Client在运行时动态的切换;状态模式是帮助一个类在不同的状态下显示不同的行为,依赖于内部的状态;
2、策略模式不持有Context的引用,而是被Context所使用;状态模式的每个状态都持有Context的引用,从而在Context中实现状态的转移;
3、从理论上说,策略模式定义对象应该“怎么做”;状态模式定义了对象“是什么”,“什么时候做”。