状态模式
作为java设计模式中常见的行为型设计模式,一问到大家就说
知道嘛,就是上下文里面切换状态嘛,不同状态干不同事情嘛
那具体呢,怎样个落地呢,又是这样的说法
这个没法用在我们项目里,我们项目太大了,一改很麻烦。很多问题的,不适合
就巴拉巴拉一堆不知道或者不想落地到生产环境里。平时学是学了,但是大家都知道技术这种东西,特别是程序员的事情,没得投入生产环境进行有效产出,都是假技术。
好,我们上来百度搜一下“java 设计模式”,上网一搜,嘿嘿,一堆结果,大家都能找到,我们上一张图先,哇,一看亲妈爆炸还没穿复活甲,这什么玩意。
来,我给大家分析一下。演示类或者上下文类持有一个状态的引用state,但是这个引用的类型是接口类型,该引用的具体赋值交给set方法进行。而这个接口到底定义了什么,doAction行为,那其实就是交给不同的扩展类进行扩展,不同的扩展类对于行为方法有不同的实现。
有的人一听这不就是多态么,又想到里氏替换。不,这是多态的一种体现,但不是里氏替换,这个叫依赖倒转,因为这里不强调一个子类扩展后对父类原有功能的扩展。好扯远了,说回现在这个类图。
总的来看,这是一种典型的行为型模式。什么意思?即类的行为是基于它的状态改变的,但平时在生产环境中的应用又真的没见到太多,今天在这里我给大家深入浅出地分享一哈,便于大家改善一下自己的代码质量
实际应用场景
谈完了前面的理论让我们现在进入生产环境,接下来我将模拟大家项目中常见情况,做咱们状态模式的实际应用介绍.包括双重状态和多状态模式的管理.掌声有请,pia pia pia,为什么不是papapa?因为水多,水生财。讲了这些铺垫,总算可以上代码了
双重状态下绑定和解绑
在开发过程中,不知道大家是怎么去防止一个服务或者广播被重复绑定注册的。或者说别的场景,应用在用户未登录的情况下,要使用某些功能需要跳转到登录界面,而不是具体的功能页。这里大家脑海里已经有个大概的思路了,但我想在座的各位肯定见过这种代码
private boolean mReceiverTag = false;
private void initBroadcastReceiver(Context context) {
if (context == null || mReceiverTag) {
return;
}
mNetworkConnectChangedReceiver = new NetworkConnectChangedReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(mNetworkConnectChangedReceiver, filter);
mReceiverTag = true;
}
是不是觉得似曾相识?用一个变量来作为标记位。那么同样道理,怎么防止重复解绑呢?键盘一顿操作
public void unRegister() {
if (mReceiverTag) {
getActivity().unregisterReceiver(mNetworkConnectChangedReceiver);
mReceiverTag = false;
}
}
爽不爽,是挺爽的,代码简单易懂
场景分析
现在我们来看下这种方案的一个优点,绑定与否只向一个标记位变量进行get获取,不需要关注太多。但是随之的问题就来,我注册绑定时候改变,注销解绑的时候也需要做对应改变。如果中间有其他操作的话,我也需要改变,那变来变去,就出现个问题,万一我哪天发现出问题了,我就一个一个情况去断点,看什么时候会导致这个变量的标记变了,导致重复绑定解绑。是能解决问题,但是,太慢了。那原本今天的活怎么办,没办法加班,一天又那么过去自己也累。那么怎么解决这种问题,状态模式!!!
解决方案
定义一个状态基类或者接口
推荐用接口方便解耦。从这里看就有一个注册和绑定的行为。我们将这个接口对象交给上下文去持有,也就是上下文中会持有一个IRegisteredState类型的变量
public interface IRegisteredState {
void registerReceiver(Context context, BroadcastReceiver receiver);
void unregisterReceiver(Context context, BroadcastReceiver receiver);
}
到这里我们可以开始相关的行为,在该执行注册的时候调用对应registerReceiver方法,注销同理。但是这里会问,不是没赋值么,NPE了。别急,这里我们就来说说赋值,前面提到我们依靠set方法对这个引用进行赋值,那赋的值就应该是IRegisteredState接口的的扩展对象。再定义一个RegistedState跟UnRegistedState类,对对应的行为方法进行实现。
状态接口扩展
public class UnRegistedState implements IRegisteredState {
@Override
public void registerReceiver(Context context, BroadcastReceiver receiver) {
IntentFilter filter = new IntentFilter();
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(receiver, filter);
}
@Override
public void unregisterReceiver(Context context, BroadcastReceiver receiver) {
LogUtil.log("unregisterReceiver","未注册状态下对解注操作忽略");
}
}
public class RegistedState implements IRegisteredState {
@Override
public void registerReceiver(Context context, BroadcastReceiver receiver) {
LogUtil.log("unregisterReceiver","注册状态下对绑定操作忽略");
}
@Override
public void unregisterReceiver(Context context,BroadcastReceiver receiver) {
context.unregisterReceiver(receiver);
}
}
那么到这里我想你猜的七七八八了,就是在未绑定的情况下对IRegisteredState引用赋值UnRegistedState对象,如果已经绑定了,就赋值RegistedState对象。由不同的状态对象去做该状态的对应行为。对于上下文对象本身而言,就只知道IRegisteredState引用进行了替换,对外的方法无论是registerReceiver还是unregisterReceiver,都是交给IRegisteredState引用去执行。具体行为交给具体类。
对应时机替换状态
public void registerReceiver(Context context) {
if (context != null) {
mReceiverState.registerReceiver(context, mNetworkConnectChangedReceiver);
setReceiverState(new RegistedState());
}
}
public void unregisterReceiver(Context context) {
if (context !=null){
mReceiverState.unregisterReceiver(context, mNetworkConnectChangedReceiver);
setReceiverState(new UnRegistedState());
}
}
那么从上面的逻辑看,当我们完成一次注册以后,多次执行绑定,由于处理对象已经发生改变,所以会得到的结果就是“注册状态下对绑定操作忽略”。多次注销同理得到"未注册状态下对解注操作忽略"。这样依赖我们只需要关注State引用的值是否是对应当前状态,该值内部是否做的是对应当前状态的行为即可。不需要再使用标记变量去引导当前的逻辑判断。毕竟状态模式的特点就是帮助消除多余的if…else 等条件选择语句
多重状态下界面更新
一样回归到开发过程中,当我们不再只是上面只管理开关状态,而是需要大量的状态并同时根据状态的改变来更新UI时候。状态模式也是个好的方案。比如说,当我们从文章或者个人资料的阅读模式,变成编辑模式,有的人会选择启动一个新的activity或者fragment,或者稍加考虑会选择打开一个大点的dialog来解决。又比如说,项目里面常常有加载中,加载完成,空状态和错误状态的做法。怎么做的呢,来我晒一下大概的代码,具体大家脑部。
override fun showLoading() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showLoading")
(viewModel as BaseViewModel<*>).loadingState.postValue(true)
(viewModel as BaseViewModel<*>).loadedState.postValue(false)
(viewModel as BaseViewModel<*>).emptyState.postValue(false)
(viewModel as BaseViewModel<*>).errorState.postValue(false)
}
override fun showContentView() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showContentView")
(viewModel as BaseViewModel<*>).loadingState.postValue(false)
(viewModel as BaseViewModel<*>).loadedState.postValue(true)
(viewModel as BaseViewModel<*>).emptyState.postValue(false)
(viewModel as BaseViewModel<*>).errorState.postValue(false)
}
override fun showError() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showError")
(viewModel as BaseViewModel<*>).loadingState.postValue(false)
(viewModel as BaseViewModel<*>).loadedState.postValue(false)
(viewModel as BaseViewModel<*>).emptyState.postValue(false)
(viewModel as BaseViewModel<*>).errorState.postValue(true)
}
override fun showEmpty() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showEmpty")
(viewModel as BaseViewModel<*>).loadingState.postValue(false)
(viewModel as BaseViewModel<*>).loadedState.postValue(false)
(viewModel as BaseViewModel<*>).emptyState.postValue(true)
(viewModel as BaseViewModel<*>).errorState.postValue(false)
}
场景分析
方案本身没有问题,终归还是有更好的解决方案。乍看是不是也觉得很正常,当展示时候切换对应状态的控件展示或者隐藏,leader一问起来还振振有词
没办法就是状态太多了我们得想办法优化
但是,如果我状态多了起来,我有10个,那是不是我得有10个方法,而这10个方法里要对10个状态下的view做处理,复杂度10*10?或者说我不小心写漏了show或者hide,我就要去一个个看,是写漏了还是写错了。
解决方案
要知道,我们是面向对象的思想,一切皆对象。怎么解决,这里我用前阵子重构公司项目的一个方案作为例子给大家分析一下。多重状态的管理,核心是依赖十六进制进行状态集管理,切换模式使用状态集,子状态判断是否属于该状态来进行子状态的对应行为。拿UI管理来说,就是根据当前状态集是否包含当前状态来做对应的UI切换。前言铺垫完,再次上代码
为什么是十六进制
这里借鉴掘金上的一篇文章对十六进制应用的实践讲解就算不去火星种土豆,也请务必掌握的 Android 状态管理最佳实践。因为当时就是因为看了这篇才有了对项目实践的冲动,十分感谢大佬的分享。
十六进制可以做到
- 通过状态集的注入,一行代码即可完成模式的切换。
- 无论再多的状态,都只需要一个字段来存储。状态被存放在 int 类型的状态集中,可以直接向数据库写入或读取。
例如 0x0001,0x0002,而十六进制的计算,我们可以借助二进制的 “按位计算” 方式来理解,即 与、或、异或、取反等
a & b,a | b,a ^ b,~a
十六进制数 0x0004 | 0x0008,可以理解为
0100
|
1000
=
1100
十六进制 (0x0004 | 0x0008) & 0x0004 可以得到
1100
&
0100
=
0100
所以状态集中包含某状态时,再与上该状态,就会得到非 0 的结果,从而利用这个特性来完成状态管理
定义状态并加入状态集
首先我们需要定义各个状态,用刚学一阵子的kotlin作为示范,肯定会写的不太优雅,但是如果写得不好我将在后面的博客中将这段代码作为重构的一个例子,希望大家可以看到一起交流,最后实现代码质量的优化。好回归正题。
例如说这里我有4个基本状态,对应的控件有展示或者隐藏,那么这里有8个子状态。
companion object {
const val SHOW_LOADING = 0x00000001
const val HIDE_LOADING: Int = 0x00000001.shl(1)
const val SHOW_ERROR: Int = 0x00000001.shl(2)
const val HIDE_ERROR: Int = 0x00000001.shl(3)
const val SHOW_EMPTY: Int = 0x00000001.shl(4)
const val HIDE_EMPTY: Int = 0x00000001.shl(5)
const val SHOW_CONTENT: Int = 0x00000001.shl(6)
const val HIDE_CONTENT: Int = 0x00000001.shl(7)
const val LOADING: Int = (SHOW_LOADING or HIDE_ERROR or HIDE_EMPTY or HIDE_CONTENT)
const val LOADED: Int = (HIDE_LOADING or HIDE_ERROR or HIDE_EMPTY or SHOW_CONTENT)
const val EMPTY: Int = (HIDE_LOADING or HIDE_ERROR or SHOW_EMPTY or HIDE_CONTENT)
const val ERROR: Int = (HIDE_LOADING or SHOW_ERROR or HIDE_EMPTY or HIDE_CONTENT)
}
这里对于状态集的操作上,添加子集使用或,此处kotlin的or对应java的|,移除使用取反,即kotlin的inv对应java的&~。
定义各个子状态下所需要的行为
如果是控件操作,那么就是根据各个子状态对应来做UI的隐藏显示。大概是这样的,这里用了jetpack里的livedata来实现UI更新,但是换成mvp模式里的也是对应的V层回调,细节忽略,咱们注意一下思路就可以了
internal val loadingState: MutableLiveData<Boolean> = MutableLiveData()
internal val errorState: MutableLiveData<Boolean> = MutableLiveData()
internal val emptyState: MutableLiveData<Boolean> = MutableLiveData()
internal val loadedState: MutableLiveData<Boolean> = MutableLiveData()
(viewModel as BaseViewModel<*>).apply {
loadingState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
shouldShow?.run {
activity?.runOnUiThread {
loadingView?.apply {
visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
}
})
loadedState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
shouldShow?.run {
activity?.runOnUiThread {
dataBinding.root.apply {
visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
}
})
emptyState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
shouldShow?.run {
activity?.runOnUiThread {
emptyView?.apply {
visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
}
})
errorState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
shouldShow?.run {
activity?.runOnUiThread {
errorView?.apply {
visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
}
})
}
在完成了各子状态对应的控件操作之后。开始留下问题,这里还是跟大的状态集没什么联系呀。来,这里开始进行对状态集的管理
管理状态集
同样,我们管理这个状态。什么意思,当状态改变的时候,我们判断子状态是否属于这个状态集,来确定该状态集对应的子状态操作。
internal val pageState: MutableLiveData<Int> = MutableLiveData()
(viewModel as BaseViewModel<*>).apply {
pageState.observe(this@AbsMvvmFragment, observerStatus())
}
fun observerStatus(): Observer<Int> = Observer { status ->
status?.run {
loadingState.postValue(statusEnabled(status, SHOW_LOADING))
loadedState.postValue(statusEnabled(status, SHOW_CONTENT))
emptyState.postValue(statusEnabled(status, SHOW_EMPTY))
errorState.postValue(statusEnabled(status, SHOW_ERROR))
}
}
private fun statusEnabled(statuses: Int, status: Int): Boolean = (statuses and status) != 0
上面代码意思就是状态集本身发生改变的时候,如下面操作showLoading,showContentView方法对状态集进行修改
override fun showLoading() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showLoading")
(viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.LOADING)
}
override fun showContentView() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showContentView")
(viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.LOADED)
}
override fun showError() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showError")
(viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.ERROR)
}
override fun showEmpty() = activity?.runOnUiThread {
LogUtils.i("${this::class.java.simpleName} showEmpty")
(viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.EMPTY)
}
对各个负责子状态UI的对象进行回调,回调传回的内容是什么呢,就是子状态的行为是否在这个状态集内。如加载状态的展示与否(下面附上状态集内容)取决于展示加载这个子状态是否在当前发生变化的状态集里面。
const val LOADING: Int = (SHOW_LOADING or HIDE_ERROR or HIDE_EMPTY or HIDE_CONTENT)
如果是这个状态集里的子状态,传true,触发前面的回调,loadingView显示。其余状态完成判断后也将判断结果传给对应的回调方法,以此完成各个视图控件的更新。
loadingState.postValue(true)
?
(viewModel as BaseViewModel<*>).apply {
loadingState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
shouldShow?.run {
activity?.runOnUiThread {
loadingView?.apply {
visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
}
})
这里所体现出来的状态模式思想,通过一个状态集,来控制多个子状态。相较以往的设置true跟false有什么区别呢,从代码上,肯定是逃不开set(true)或者set(false)的原理。不同的是,我们将各个view的设置归于各个状态,展示与否跟其他view的展示与否互不关联。以前会出现在某个状态下set(true)和set(false)混了的情况,现在我们只关注这个状态下view该怎么做即可。就算删除某个子状态的操作,也只需要完成子状态的移除,不需要去每个操作view的地方进行移除。
这里使用简单的页面状态来进行演示,如果是有更多的状态,如网络失败,服务器失败要求另外的展示,同理,将这个对应状态添加到状态集中或者创建新的状态集,紧接着绑定该子状态对应的UI操作即可。
优势及缺点
经过前面的一番演示,可能对大家实现如何在生产环境中应用状态模式有了一个更清楚的了解,知道如何落地,毕竟学了要有产出这个是我学习一切基础的目的,也是巩固所学的一个好方式。那现在我们来复盘一下状态模式有什么好处呢。
根据在菜鸟教程上所搜索到的下面这几个点,大家都可以在这个讲解中找到对照。
优势
- 封装了转换规则
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为(两个例子都有体现)
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块(根据前面第一个例子)
缺点
- 状态模式的使用必然会增加系统类和对象的个数(状态增加)
- 对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码(状态集改动)
好,希望上面的介绍能对大家有些许用户,如果有哪些不够清楚的地方也欢迎跟我讨论,让我做的更好,感谢。
本篇参考内容有,如果有侵权冒犯的地方请与我联系
https://www.runoob.com/design-pattern/state-pattern.html
https://juejin.im/post/5d1a148e6fb9a07ea6488ba3?utm_source=gold_browser_extension#heading-0