This is the fourth article in the design Patterns series, which is listed below:

  1. A word to sum up the design model: factory model =? Policy mode =? Template method pattern

  2. Use the combined design mode – decorator mode in beauty camera

  3. Use composite design patterns — remote proxy patterns for girls

  4. Use design patterns to remove unnecessary state variables — state patterns

The business scenario

This is a common scenario in UI development: an interface has two states, each of which corresponds to a different operation for an interface element. For example, in offline state, clicking a big cross will directly exit the application, while in login state, clicking a big cross will log out.

The simplest and most intuitive solution is to store the current state with an int value, which will run different branches depending on the int value.

Option 1: state variable + if-else

public class MainActivity extends AppCompatActivity {
    //' offline '
    private static final int STATE_OFFLINE = 0;
    //' login status'
    private static final int STATE_LOGIN = 1;
    //' current status'
    private int currentState = STATE_OFFLINE;
    // Display status control
    private TextView tvState;

    // Omit setup layout file and setup click listener

    //' What happens when the button is clicked '
    public void onButtonClick(a) {
        if (currentState == STATE_OFFLINE) {
            logIn();
            setStateText("login"); setState(STATE_LOGIN); }}//' What happens when a big cross is clicked '
    public void onCloseClick(a) {
        if (currentState == STATE_OFFLINE) {
            finish();
        } else if (currentState == STATE_LOGIN) {
            logOut();
            setStateText("offline"); setState(STATE_OFFLINE); }}public void setStateText(String state) {
        tvState.setText(state);
    }

    //' Set current state '
    public void setState(int state) {
        this.currentState = state; }}Copy the code

Simple and intuitive, state variables with if-else to achieve the requirements.

New need comes, new group function, when login successfully, click the login button again can join the group. Clicking a big cross while in a group will exit the group.

The new requirement adds a state, and the two action buttons on the interface add two new actions.

Add if-else for small scenes:

public class MainActivity2 extends AppCompatActivity {
    private static final int STATE_OFFLINE = 0;
    private static final int STATE_LOGIN = 1;
    //' Add group status'
    private static final int STATE_IN_GROUP = 2;
    private int currentState = STATE_OFFLINE;
    private TextView tvState;

    public void onButtonClick(a) {
        if (currentState == STATE_OFFLINE) {
            logIn();
            setStateText("login");
            setState(STATE_LOGIN);
        }
        //' button to add group status response code '
        else if (currentState == STATE_LOGIN) {
            joinGroup();
            setStateText("in group"); setState(STATE_IN_GROUP); }}public void onCloseClick(a) {
        if (currentState == STATE_OFFLINE) {
            finish();
        } else if (currentState == STATE_LOGIN) {
            logOut();
            setStateText("offline");
            setState(STATE_OFFLINE);
        } 
        //' add group status response code '
        else if (currentState == STATE_IN_GROUP) {
            quitGroup();
            tvState.setText("login"); setState(STATE_LOGIN); }}Copy the code

It doesn’t look too bad so far, but as the state increases, the if-else branches become more original and the code’s readability continues to decline.

More importantly, it does not comply with the open closed principle, which allows you to change the original code when adding new features. While adding states in demo, onCloseClick() and onButtonClick had to be modified. The logic in the demo is very simple; the two functions have only one caller, the button and the big cross. In a real project, callers might be scattered all over the place. How dare you change a function like this? Bugs can be fixed if you’re not careful.

If requirements change: Add confirmation in offline state, that is, click the button pop-up to confirm whether you need to log in, and click the fork pop-up to confirm whether you need to exit the application. If you use the scheme above, you need to search STATE_OFFLINE globally, find all the places that access it, and make changes one by one (possibly spread across n classes, increasing the possibility of n class bugs).

After teasing the weaknesses, see how the state model solves the problem.

Scheme 2: State mode

In this scenario, what is changing is the state, and adding a layer of abstraction to encapsulate the change is a common tool of design patterns. See how to encapsulate state:

public interface State {
    void onCloseClick(a);
    void onButtonClick(a);
}
Copy the code

A new layer of abstraction is added. Instances of this abstraction represent a concrete state, and methods in the abstraction represent the operations that can be performed by that state.

There are now three states: offline, logged in, and logged in, respectively corresponding to three State instances:

//' offline '
public class OfflineState implements State {
    private MainActivity mainActivity;
    public OfflineState(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

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

    @Override
    public void onButtonClick(a) {
        mainActivity.logIn();
        mainActivity.setState(mainActivity.getLoginState());
        mainActivity.setStateText("login"); }}//' login status'
public class LoginState implements State {
    private MainActivity mainActivity;
    public LoginState(MainActivity activity) {
        this.mainActivity = activity;
    }

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

    @Override
    public void onButtonClick(a) {
        mainActivity.joinGroup();
        mainActivity.setState(mainActivity.getInGroupState());
        mainActivity.setStateText("in group"); }}//' group status'
public class InGroupState implements State {
    private MainActivity mainActivity;
    public InGroupState(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

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

    @Override
    public void onButtonClick(a) {}}Copy the code

The MainActivity page holds instances of each state

public class MainActivity extends AppCompatActivity {
    //' Offline state instance '
    private State offlineState;
    //' login status instance '
    private State loginState;
    //' group status instance '
    private State inGroupState;
    //' current status'
    private State currentState;
    private TextView tvState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Omit layout and setup listeners
        initState();
    }

    //' initialization state '
    private void initState(a) {
        offlineState = new OfflineState(this);
        loginState = new LoginState(this);
        inGroupState = new InGroupState(this);
        setStateText("offline");
        setState(offlineState);
    }

    //' delegate button click to current state '
    public void onButtonClick(a) {
        currentState.onButtonClick();
    }

    //' delegate the big click to the current state '
    public void onCloseClick(a) {
        currentState.onCloseClick();
    }

    //' change current state '
    public void setState(State state) {
        this.currentState = state;
    }
    //' get specified state '
    public State getOfflineState(a) {
        return offlineState;
    }
    public State getLoginState(a) {
        return loginState;
    }
    public State getInGroupState(a) {
        return inGroupState;
    }
    public void setStateText(String state) { tvState.setText(state); }}Copy the code

What’s interesting about this scenario is that it shifts from “handling different states within each method” to “implementing all methods within the same state class.” Why does that sound like the same old thing?

Instead, the state mode makes each state “closed for modification” and the MainActivity “open for extension” when adding a state (since new states do not modify onCloseClick() and onButtonClick())).

Another design pattern of “encapsulating what is changing and responding to change with polymorphism”. (It is the same as the factory pattern, template method pattern, and policy pattern. See Design Patterns part 1.)

State mode vs. policy mode

Analytical design patterns can’t escape comparison because several really look alike. The detailed explanation and application of the policy pattern can be seen here and here, respectively

They both have almost the same implementation and purpose, defining behavior through interfaces, holding behavior instances through composition, and dynamically replacing behavior through polymorphism.

But they apply to slightly different scenarios: Policy patterns define a behavior externally and initiate a one-time behavior replacement externally, whereas state patterns define multiple behaviors internally and continuously replace behaviors for internal reasons.