状态模式

一、了解状态模式
1、概念

状态模式又称状态对象模式,它允许一个对象在其内部状态改变的时候改变其行为。属于对象行为模式

2、结构
状态模式结构

角色:

  • 抽象状态角色:定义接口,用以封装环境角色对象的一个特定状态所对应的行为
  • 具体状态角色:实现抽象状态角色所定义的接口
  • 环境角色:定义客户端所需要的接口,并持有一个具体状态类的实例。这个具体状态类的实例代表此环境对象的现有状态。
3、简单示例
  • 抽象状态类
package com.example.meitu.myapplication.state;

/**
 * 状态模式—抽象状态角色
 * @author: yongxiang.wei
 * @version: 1.0.0, 2018/7/22 20:32
 * @since: 1.0.0
 */
public interface State {
    /**
     * 抽象状态行为
     */
    public void stateAction();
}

  • 具体状态类
package com.example.meitu.myapplication.state;

/**
 * 状态模式—具体状态角色
 * @author: yongxiang.wei
 * @version: 1.0.0, 2018/7/22 20:32
 * @since: 1.0.0
 */
public class ConcreteState implements State{
    /**
     * 具体状态行为
     */
    @Override
    public void stateAction() {
        System.out.println("concreteState action");
    }
}

  • 环境角色类
package com.example.meitu.myapplication.state;

/**
 * 状态模式—环境角色
 * @author: yongxiang.wei
 * @version: 1.0.0, 2018/7/22 20:31
 * @since: 1.0.0
 */
public class Context {
    private State mCurrentState;

    public State getCurrentState(){
        return mCurrentState;
    }
    public void setCurrentState(State pState){
        mCurrentState = pState;
    }

    public void currentStateAction(){
        if(mCurrentState != null){
            mCurrentState.stateAction();
        }
    }
}

  • 客户端类
package com.example.meitu.myapplication.state;

/**
 * @author: yongxiang.wei
 * @version: 1.0.0, 2018/7/22 20:38
 * @since: 1.0.0
 */
public class Client {
    public static void main(String[] pArgs){
        Context context = new Context();
        State currentState = new ConcreteState();
        context.setCurrentState(currentState);
        context.currentStateAction();
    }
}

4、优势与缺陷
  • 优势:
    (1)可以将将条件转移语句使用状态模式方式实现,如果条件转移语句过于过于庞大和复杂时,使用状态模式可以让程序免于大量的条件转移语句,便于维护
    (2)避免使用一些属性来表称系统状态以及因状态表称属性使用不当带来的问题
    (3)扩展性强,当有新的状态及其行为需要定义是时,可以很方便的通过设立子类的方式加到系统中,不需要改变其他类
  • 缺陷:如果状态比较多,可能会造成大量的小状态类
5、使用场景
  • 一个对象的行为依赖于它所处的状态,对象的行为必须随状态的改变而改变
  • 对象在某个方法里依赖于一重或多重条件转移语句,其中有大量的代码。状态模式把条件转移语句的每个分支都包装到一个单独的类里,这使得条件转移分支能够以类的形式独立存在和演变。维护这些独立的类就不再影响到系统的其他部分。
二、问题及讨论

状态模式并没有规定在什么时间、谁来处理状态的转换、谁来定义状态的变化(下一状态是哪个状态)、状态对象的生命周期如何管理。

  • 什么时间:
    这个我觉得得要根据具体的业务来决定。比如登录模块,开始的时候是未登录状态,只有用户点击登录这个时间才会将状态变为登录中;接着只有在登录成功时才会将状态变为已登录状态

  • 谁来处理状态的转换
    我觉得是在环境角色里比较合适,因为环境角色持有当前的状态,在处理状态改变之前,先将环境角色中的当前状态退出,再将环境角色中的当前状态切换到新的状态

  • 谁来定义状态的变化?
    有三种角色可以决定状态的变化:
    (1)环境角色
    适合使用场景:转换条件固定
    (2)具体状态角色
    可以自行决定在合适使用哪个状态,灵活性较强
    (3)外界事件

  • 状态对象的生命周期(什么时候创建、什么时候销毁)
    (1)动态创建状态对象,状态转换即销毁,减少不必要的对象:适合状态不频繁转换场景
    (2)一开始就创建好所有状态对象,且在状态转换时不销毁:适合状态频繁转换,减少创建对象带来的开销,但是如果状态过多的话会造成内存上的开销
    (3)创建好一种或几种频繁使用的状态对象:减少创建对象带来的开销

三、android中的应用

android源码中用到状态模式的比较少,framework中statemachine采用了状态模式,由于时间准备不充裕,还没时间看这块的的源码,这里先不展开介绍,待续。

四、项目中的应用

App开发基本上都会涉及到页面的开发,而页面大都会有以下几种状态:

  • 加载中


    加载中
  • 加载成功显示页面内容
  • 页面为空


    页面为空

    页面为空2
  • 加载失败。


    加载失败

    如何管理这些不同的页面状态呢?刚开始的时候我这边使用的是构建模式的方式构建一个EmptyView来管理这些不同的状态和行为,但是在我想做分享的时候,感觉使用构建模式的方式不合理,会造成这个EmptyView愈来愈臃肿;综合考虑我觉得使用状态模式进行重构,下面重构页面状态的结构:


    页面状态结构
  • IPageState页面状态抽象角色
public interface IPageState {
    /**
     * 进入当前状态
     * @param pStateContext 环境角色
     */
    void enter(IPageStateContext pStateContext);

    /**
     * 退出当前状态
     * @param pStateContext 环境角色
     */
    void exit(IPageStateContext pStateContext);
}
  • BasePageState页面状态基类
public abstract class BasePageState implements IPageState {
    /**
     * 当前状态所展示的View
     */
    View mPageStateView;

    /**
     * 进入当前状态
     * @param pStateContext 环境角色
     */
    @Override
    public void enter(IPageStateContext pStateContext) {
        ViewGroup pParentView = pStateContext == null ? null : pStateContext.getPageStateParentView();
        if(pParentView == null){
            return;
        }
        if(pParentView instanceof LinearLayout){
            pParentView.addView(getPageStateView(pParentView.getContext()), 0);
        }else{
            int childCount = pParentView.getChildCount();
            pParentView.addView(getPageStateView(pParentView.getContext()), childCount);
        }
    }

    /**
     * 获取当前状态所展示的View
     * @param pContext
     * @return
     */
    private View getPageStateView(Context pContext) {
        if(pContext == null){
            return null;
        }
        mPageStateView = LayoutInflater.from(pContext).inflate(pageStateResLayoutId(), null);
        return mPageStateView;
    }

    @Override
    public void exit(IPageStateContext pStateContext) {
        ViewGroup pParentView = pStateContext == null ? null : pStateContext.getPageStateParentView();
        if(pParentView == null || mPageStateView == null){
            return;
        }
        pParentView.removeView(mPageStateView);
    }

    /**
     * 当前页面状态所展示的View 布局资源id
     * @return
     */
    protected abstract int pageStateResLayoutId();
}

这里定义基类的原因是:因为其他的状态进入和退出状态基本都是将当前状态的view加入页面显示或从页面中移除

  • NormalPageState页面正常状态
public class NormalPageState implements IPageState {
    /**
     * 进入正常状态
     * @param pStateContext 环境角色
     */
    @Override
    public void enter(IPageStateContext pStateContext) {

    }

    /**
     * 退出正常状态
     * @param pStateContext 环境角色
     */
    @Override
    public void exit(IPageStateContext pStateContext) {

    }
}

这里正常的页面状态行为设置为空,1是因为可以不改变当前页面正常状态的逻辑;2是正常的状态行为放在Activity中处理(可讨论是否合理,这个类甚至可以不要)

  • LoadingState页面加载中状态
public class LoadingState extends BasePageState {

    /**
     * 当前页面状态所展示的View 布局资源id
     * @return
     */
    @Override
    protected int pageStateResLayoutId() {
        return R.layout.include_common_loading_state;
    }

    @Override
    public void enter(IPageStateContext pStateContext) {
        super.enter(pStateContext);
        if(pStateContext != null){
            pStateContext.onPageLoading();
        }
    }
}

主要是为了调用环境角色加载页面数据

  • EmptyState页面空白状态
public class EmptyState extends BasePageState {
    /**
     * 当前页面状态所展示的View 布局资源id
     * @return 返回空页面布局资源id
     */
    @Override
    protected int pageStateResLayoutId() {
        return R.layout.include_common_empty_state;
    }

    /**
     * 进入当前状态
     * @param pStateContext 环境角色
     */
    @Override
    public void enter(final IPageStateContext pStateContext) {
        super.enter(pStateContext);
        if(mPageStateView != null){
            mPageStateView.setOnClickListener(new View.OnClickListener(){

                @Override
                public void onClick(View v) {
                    if(pStateContext != null){
                        pStateContext.onPageEmptyClick();
                    }
                }
            });
        }
    }
}

可以定制化空白页面的点击事件,让具体的环境角色去实现对于的业务逻辑

  • PageErrorState页面加载失败状态
public class PageErrorState extends BasePageState {

    /**
     * 当前页面状态所展示的View 布局资源id
     * @return
     */
    @Override
    protected int pageStateResLayoutId() {
        return R.layout.include_common_error_state;
    }

    /**
     * 进入当前状态
     * @param pStateContext 环境角色
     */
    @Override
    public void enter(final IPageStateContext pStateContext) {
        super.enter(pStateContext);
        if(mPageStateView != null){
            Button retryBtn = mPageStateView.findViewById(R.id.include_common_error_retry_btn);
            retryBtn.setOnClickListener(new View.OnClickListener(){

                @Override
                public void onClick(View v) {
                    if(pStateContext != null){
                        pStateContext.onPageErrorRetry();
                    }
                }
            });
        }
    }
}

可以定制化页面加载失败的点击事件,让具体的环境角色去实现对于的业务逻辑

  • IPageStateContext页面状态环境接口
public interface IPageStateContext {
    /**
     * 构建加载中状态
     * @param pMsg
     * @return
     */
    IPageState buildLoadingState(String pMsg);

    /**
     * 构建页面空白状态
     * @return
     */
    IPageState buildEmptyState();

    /**
     * 构建正常状态
     * @return
     */
    IPageState buildNormalPageState();

    /**
     * 构建页面加载失败状态
     * @param pErrorCode
     * @param pErrorMsg
     * @return
     */
    IPageState buildPageErrorState(int pErrorCode, String pErrorMsg);

    /**
     * 转换到新状态
     * @param pNewPageState
     */
    void transformToNewState(IPageState pNewPageState);

    /**
     * 获取页面状态布局所在的父级控件
     * @return
     */
    ViewGroup getPageStateParentView();

    /**
     * 页面加载中
     */
    void onPageLoading();

    /**
     * 页面未空时的点击事件
     */
    void onPageEmptyClick();

    /**
     * 页面重新加载
     */
    void onPageErrorRetry();
}
  • BaseActivity——Activity 基类,环境角色基类
public abstract class BaseActivity extends AppCompatActivity implements IPageStateContext{
    /**
     * 当前页面状态
     */
    IPageState mPageState;
    /**
     * 页面内容容器
     */
    ViewGroup mPageContentViewLayout;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(contentViewLayoutId());
        intView(savedInstanceState);
        initPageState();
    }

    /**
     * 重写contentView结构,改成LinearLayout,配置状态栏样式、标题栏等
     *
     * @param layoutResID contentViewLayoutId
     */
    @Override
    public void setContentView(int layoutResID) {
        //重写ContentView
        resetContentViewStruct();
        if(mPageContentViewLayout != null){
            LayoutInflater.from(this).inflate(layoutResID, mPageContentViewLayout, true);
        }
    }

    /**
     * 重写contentView结构,改成LinearLayout,配置状态栏样式、标题栏等
     */
    private void resetContentViewStruct() {
        ViewGroup viewGroup = (ViewGroup) findViewById(android.R.id.content);
        viewGroup.removeAllViews();
        mPageContentViewLayout = new LinearLayout(this);
        ((LinearLayout)mPageContentViewLayout).setOrientation(LinearLayout.VERTICAL);

        viewGroup.addView(mPageContentViewLayout);
    }

    protected abstract int contentViewLayoutId();
    protected abstract void intView(@Nullable Bundle savedInstanceState);

    protected void initPageState(){
        transformToNewState(buildLoadingState(null));
    }

    @Override
    public ViewGroup getPageStateParentView(){
        return mPageContentViewLayout;
    }

    /**
     * 页面加载中
     */
    @Override
    public void onPageLoading() {

    }

    /**
     * 页面未空时的点击事件
     */
    @Override
    public void onPageEmptyClick(){

    }

    /**
     * 页面重新加载
     */
    @Override
    public void onPageErrorRetry() {
        transformToNewState(buildLoadingState(null));
    }

    /**
     * 转换到新状态
     * @param pNewPageState
     */
    @Override
    public void transformToNewState(IPageState pNewPageState) {
        if(pNewPageState == null){
            return;
        }
        if(mPageState != null){
            mPageState.exit(this);
        }
        mPageState = pNewPageState;
        mPageState.enter(this);
    }

    /**
     * 构建加载中状态
     * @param pMsg
     * @return
     */
    @Override
    public IPageState buildLoadingState(String pMsg){
        return new LoadingState();
    }

    /**
     * 构建页面空白状态
     * @return
     */
    @Override
    public IPageState  buildEmptyState(){
        return new EmptyState();
    }

    /**
     * 构建正常状态
     * @return
     */
    @Override
    public IPageState buildNormalPageState(){
        return new NormalPageState();
    }

    /**
     * 构建页面加载失败状态
     * @param pErrorCode
     * @param pErrorMsg
     * @return
     */
    @Override
    public IPageState buildPageErrorState(int pErrorCode, String pErrorMsg){
        return new PageErrorState();
    }
}

这里将Activity当做环境角色,用一个基类来实现环境角色的接口,在基类中定义基本的实现和状态的转换功能,具体的环境角色可以实现并定义自己的每个状态的不同表现

  • GameContentActivity 具体环境角色
public class GameContentActivity extends BaseActivity implements IGetGameContentPresView {
    TextView mContentTv;
    GetGameContentPresenter mPresenter;
    @Override
    protected int contentViewLayoutId() {
        return R.layout.activity_game_content;
    }

    @Override
    protected void intView(@Nullable Bundle savedInstanceState) {
        mContentTv = findViewById(R.id.get_games_content_tv);
        mPresenter = new GetGameContentPresenter(this, this);
    }

    @Override
    public void onPageLoading() {
        mPresenter.getGameContent();
    }

    @Override
    public void onGetGameContentSuccess(String pContent) {
        if(mContentTv != null){
            mContentTv.setText(pContent);
        }
    }

    @Override
    public void onGetGameContentFailure(int pErrorCode, String pErrorMsg) {

    }
}
  • GetGameContentPresenter 外界环境
public class GetGameContentPresenter {
    private WeakReference<IPageStateContext> mPageStateContextRef;
    private WeakReference<IGetGameContentPresView> mPresViewRef;

    public GetGameContentPresenter(
            IPageStateContext pPageStateContext,
            IGetGameContentPresView pPresView){
        mPageStateContextRef = new WeakReference<>(pPageStateContext);
        mPresViewRef = new WeakReference<>(pPresView);
    }

    private IPageStateContext getPageStateContext(){
        return mPageStateContextRef == null ? null : mPageStateContextRef.get();
    }

    private IGetGameContentPresView getPresView(){
        return mPresViewRef == null ? null : mPresViewRef.get();
    }

    public void getGameContent(){
        final Callback callback = new Callback<String>(){

            @Override
            public void onSuccess(String s) {
                IPageStateContext pageStateContext = getPageStateContext();
                if(pageStateContext != null){
                    if(TextUtils.isEmpty(s)){
                        pageStateContext.transformToNewState(pageStateContext.buildEmptyState());
                    }else {
                        pageStateContext.transformToNewState(pageStateContext.buildNormalPageState());
                    }
                }
                IGetGameContentPresView presView = getPresView();
                if(presView != null){
                    presView.onGetGameContentSuccess(s);
                }
            }

            @Override
            public void onFailure(int pErrorCode, String pErrorMsg) {
                IPageStateContext pageStateContext = getPageStateContext();
                if(pageStateContext != null){
                    pageStateContext.transformToNewState(pageStateContext.buildPageErrorState(pErrorCode, pErrorMsg));
                }
                IGetGameContentPresView presView = getPresView();
                if(presView != null){
                    presView.onGetGameContentFailure(pErrorCode, pErrorMsg);
                }
            }
        };
        //以下是网络请求获取游戏内容,在此不展开
        Handler handler = new Handler(Looper.getMainLooper());
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                callback.onSuccess("true");
            }
        }, 2000);
    }

    public static abstract class Callback<T>{
        public abstract void onSuccess(T t);
        public abstract void onFailure(int pErrorCode, String pErrorMsg);
        public void onComplete(){

        }
    }
}

让外界条件角色去决定什么时候转换状态以及转换成哪个状态

五、参考
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,127评论 25 707
  • 3.4 模拟工作流## 做企业应用的朋友,大多数都接触过工作流,至少处理过业务流程。当然对于工作流,复杂的应用可能...
    七寸知架构阅读 2,013评论 1 53
  • 初识晨曦在她举办的西班牙语班上,沉稳干练是我的初印象。亦师亦友的关系,从彻夜的畅聊到咖啡吧里的分享会,每一次...
    丫丫Rainbow阅读 198评论 0 0
  • 最近好像特别容易感冒,昨天喝了同事给的咖啡,晚上就拉肚子,特别难受,复盘做的有点迟,人总是因为受环境身体健康影...
    duduwa阅读 161评论 0 0
  • 我和你妈同时掉水里,你先救谁 一个千古难题,至今没有人给出正确答案,救妈得罪老婆,救老婆对不起妈的生养之恩。救近的...
    米粥的江湖阅读 244评论 2 0