用设计模式去掉没必要的状态变量 —— 状态模式

简介: 这是在UI开发中经常会遇到的场景:界面有两种状态,每一种状态下界面元素对应的操作都不同。比如在 offline 状态下点击大叉会直接退出应用,而在 login 状态下点击大叉会退出登录。 最简单直观的

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy

贡献主题:https://github.com/xitu/juejin-markdown-themes

theme: github

highlight: github

这是设计模式系列的第四篇,系列文章目录如下:

  1. 一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式
  2. 使用组合的设计模式 —— 美颜相机中的装饰者模式
  3. 使用组合的设计模式 —— 追女孩要用的远程代理模式
  4. 用设计模式去掉没必要的状态变量 —— 状态模式

业务场景

这是在UI开发中经常会遇到的场景:界面有两种状态,每一种状态下界面元素对应的操作都不同。比如在 offline 状态下点击大叉会直接退出应用,而在 login 状态下点击大叉会退出登录。

最简单直观的方案就是用 int 值来保存当前状态,根据 int 值不同会运行不同分支的操作。

方案一:状态变量 + if-else

public class MainActivity extends AppCompatActivity {
    //'离线状态'
    private static final int STATE_OFFLINE = 0;
    //'登陆状态'
    private static final int STATE_LOGIN = 1;
    //'当前状态'
    private int currentState = STATE_OFFLINE;
    //显示状态的控件
    private TextView tvState;

    //省略了设置布局文件和设置点击监听

    //'当按钮点击时执行的操作'
    public void onButtonClick() {
        if (currentState == STATE_OFFLINE) {
            logIn();
            setStateText("login");
            setState(STATE_LOGIN);
        }
    }

    //'当大叉被点击时执行的操作'
    public void onCloseClick() {
        if (currentState == STATE_OFFLINE) {
            finish();
        } else if (currentState == STATE_LOGIN) {
            logOut();
            setStateText("offline");
            setState(STATE_OFFLINE);
        } 
    }

    public void setStateText(String state) {
        tvState.setText(state);
    }

    //'设置当前状态'
    public void setState(int state) {
        this.currentState = state;
    }
}

简单直观,状态变量配合 if-else 就能实现需求。

新需要来了,新增群组功能,当登陆成功后,再次点击登陆按钮就能加入群组。在群组时点击大叉会退出群组。

新需求增加了一种状态,界面上的两个操作按钮也因此增加了两种新的操作。

小场面,只需要新增 if-else 就能搞定:

public class MainActivity2 extends AppCompatActivity {
    private static final int STATE_OFFLINE = 0;
    private static final int STATE_LOGIN = 1;
    //'新增群组状态'
    private static final int STATE_IN_GROUP = 2;
    private int currentState = STATE_OFFLINE;
    private TextView tvState;

    public void onButtonClick() {
        if (currentState == STATE_OFFLINE) {
            logIn();
            setStateText("login");
            setState(STATE_LOGIN);
        }
        //'按钮新增对群组状态的响应代码'
        else if (currentState == STATE_LOGIN) {
            joinGroup();
            setStateText("in group");
            setState(STATE_IN_GROUP);
        }
    }

    public void onCloseClick() {
        if (currentState == STATE_OFFLINE) {
            finish();
        } else if (currentState == STATE_LOGIN) {
            logOut();
            setStateText("offline");
            setState(STATE_OFFLINE);
        } 
        //'大叉新增对群组状态的响应代码'
        else if (currentState == STATE_IN_GROUP) {
            quitGroup();
            tvState.setText("login");
            setState(STATE_LOGIN);
        }
    }

目前看起来还不是太糟,但随着状态的增加,if-else 分支就会原来越多,代码可读性会持续下降。

更关键的是这不符合开闭原则,即当新增功能的时候不允许修改原有代码。而在 demo 中新增状态的时候,不得不修改onCloseClick()onButtonClick。demo 中的逻辑非常简单,这两个函数的调用者只有一个,分别是按钮和大叉。真实项目中调用者可能分布在各个角落,对于这种函数,你敢轻易改吗?一不小心就可能修改出 bug 。

如果需求变更:在离线状态增加确认,即离线时点击按钮弹框确认是否需要登录,点击大叉弹框确认是否需要退出应用。如果使用上述方案,就需要全局搜索STATE_OFFLINE,找到所有访问它的地方,一个个的做修改(可能散布在 n 个类中,增加了 n 个类出 bug 的可能性)。

吐槽完缺点后,看看状态模式是怎么解决问题的。

方案二:状态模式

在这个场景中,变化的是状态,增加一层抽象把变化封装起来是设计模式的惯用手段。看下如何把状态封装起来:

public interface State {
    void onCloseClick();
    void onButtonClick();
}

新增一层抽象,这层抽象的实例表示一个具体的状态,抽象中的方法表示该状态可以执行的操作。

现在有离线、登陆、进群组这三个状态,分别对应着三个State实例:

//'离线状态'
public class OfflineState implements State {
    private MainActivity mainActivity;
    public OfflineState(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    public void onCloseClick() {
        mainActivity.finish();
    }

    @Override
    public void onButtonClick() {
        mainActivity.logIn();
        mainActivity.setState(mainActivity.getLoginState());
        mainActivity.setStateText("login");
    }
}

//'登陆状态'
public class LoginState implements State {
    private MainActivity mainActivity;
    public LoginState(MainActivity activity) {
        this.mainActivity = activity;
    }

    @Override
    public void onCloseClick() {
        mainActivity.logOut();
        mainActivity.setState(mainActivity.getOfflineState());
        mainActivity.setStateText("offline");
    }

    @Override
    public void onButtonClick() {
        mainActivity.joinGroup();
        mainActivity.setState(mainActivity.getInGroupState());
        mainActivity.setStateText("in group");
    }
}

//'进群组状态'
public class InGroupState implements State {
    private MainActivity mainActivity;
    public InGroupState(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    public void onCloseClick() {
        mainActivity.quitGroup();
        mainActivity.setState(mainActivity.getLoginState());
        mainActivity.setStateText("login");
    }

    @Override
    public void onButtonClick() {}
}

MainActivity页面持有各个状态的实例

public class MainActivity extends AppCompatActivity {
    //'离线状态实例'
    private State offlineState;
    //'登陆状态实例'
    private State loginState;
    //'进群组状态实例'
    private State inGroupState;
    //'当前状态'
    private State currentState;
    private TextView tvState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //省略了布局和设置监听器
        initState();
    }

    //'初始化状态'
    private void initState() {
        offlineState = new OfflineState(this);
        loginState = new LoginState(this);
        inGroupState = new InGroupState(this);
        setStateText("offline");
        setState(offlineState);
    }

    //'将点击按钮操作委托给当前状态'
    public void onButtonClick() {
        currentState.onButtonClick();
    }

    //'将点击大叉操作委托给当前状态'
    public void onCloseClick() {
        currentState.onCloseClick();
    }

    //'变更当前状态'
    public void setState(State state) {
        this.currentState = state;
    }
    //'获取指定状态'
    public State getOfflineState() {
        return offlineState;
    }
    public State getLoginState() {
        return loginState;
    }
    public State getInGroupState() {
        return inGroupState;
    }
    public void setStateText(String state) {
        tvState.setText(state);
    }
}

这个方案的有趣之处在于:将“在每个方法内处理不同状态” 转变成 “在同一个状态类内部实现所有方法”。怎么听上去有种换汤不换药的感觉?

其实不然,状态模式在新增状态时,让原本的每一个状态“对修改关闭”,让MainActivity“对扩展开放”(因为新增状态不要修改onCloseClick()onButtonClick()

又是一个“把变的东西封装起来,用多态来应对变化”的设计模式。(它和工厂模式,模版方法模式,策略模式殊途同归,详见设计模式第一篇

状态模式 vs 策略模式

分析设计模式总是逃不掉相互比较,因为有几个长的真的很像。策略模式的详细讲解和应用可以分别移步这里这里

它们俩的实现方式和目的可以说几乎相同,都是通过接口定义行为,通过组合持有行为实例,通过多态动态地替换行为。

但它们的适用场景略有区别:策略模式是在外部定义了一个行为,并由外部发起一次性的行为替换,而状态模式在内部定义了多个行为,并由内部原因持续地发生行为替换。

目录
相关文章
|
6月前
|
设计模式
设计模式之 State(状态模式)
设计模式之 State(状态模式)
42 0
|
2月前
|
设计模式 Java 测试技术
Java设计模式-状态模式(18)
Java设计模式-状态模式(18)
|
3月前
|
设计模式 网络协议 Java
【十五】设计模式~~~行为型模式~~~状态模式(Java)
文章详细介绍了状态模式(State Pattern),这是一种对象行为型模式,用于处理对象在其内部状态改变时的行为变化。文中通过案例分析,如银行账户状态管理和屏幕放大镜工具,展示了状态模式的应用场景和设计方法。文章阐述了状态模式的动机、定义、结构、优点、缺点以及适用情况,并提供了Java代码实现和测试结果。状态模式通过将对象的状态和行为封装在独立的状态类中,提高了系统的可扩展性和可维护性。
【十五】设计模式~~~行为型模式~~~状态模式(Java)
|
4月前
|
设计模式 JavaScript Go
js设计模式【详解】—— 状态模式
js设计模式【详解】—— 状态模式
80 7
|
5月前
|
设计模式
状态模式-大话设计模式
状态模式-大话设计模式
|
5月前
|
设计模式 存储
行为设计模式之状态模式
行为设计模式之状态模式
|
6月前
|
设计模式 Go
[设计模式 Go实现] 行为型~状态模式
[设计模式 Go实现] 行为型~状态模式
|
6月前
|
设计模式 Java
23种设计模式,状态模式的概念优缺点以及JAVA代码举例
【4月更文挑战第9天】状态模式是一种行为设计模式,允许一个对象在其内部状态改变时改变它的行为,这个对象看起来似乎修改了它的类。
58 4
|
6月前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
|
6月前
|
设计模式 Java
【设计模式系列笔记】状态模式
在Java中,状态模式是一种行为设计模式,它允许对象在其内部状态改变时改变其行为。状态模式的关键思想是将对象的状态封装成独立的类,并将对象的行为委托给当前状态的对象。这样,当对象的状态发生变化时,其行为也会相应地发生变化。
73 0