java 设计模式 —— 浅析状态模式

这天你早早的来到了公司,刚端上热腾腾的热茶,产品经理过来了——

“小颜,咱们项目新增了两个功能,一个是点赞功能,一个是转发功能,还有我们定期会做一个抽奖活动,所以还要新增一个抽奖功能,这个你把它做了吧,有没有问题?”

“经理,没有问题,不就是在点击这些按钮的时候判断一下当前的登录状态么,如果登录了,那么就可以进行这些功能,如果没有登录的话,那么点击这些功能我们就应该跳到登录的界面,对不对?”

经理别有意味的笑了一下,拍了拍你的肩膀。你很疑惑——“我有说错什么么?这不应该就是一个正常的逻辑么”。想归想,你赶紧写起了代码。

好不容易写到了按钮的点击事件那块 ——

@Override
public void onClick(View view) {
    switch(view.getId()) {
        // 点赞按钮
        case R.id.btn_admire:
            if (isLogin) {
                // 已登录
                // 实现点赞的逻辑代码
                // ...
            } else if (!isLogin) {
                // 未登录
                // 跳转到登录界面,便于用户登录
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
            break;

        // 转发按钮
        case R.id.btn_relay:
            if (isLogin) {
                // 已登录
                // 实现转发的逻辑代码
                // ...
            } else if (!isLogin) {
                // 未登录
                // 跳转到登录界面,便于用户登录
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
            break;

        // 抽奖按钮
        case R.id.btn_raffle:
            if (isLogin) {
                // 已登录
                // 实现抽奖的逻辑代码
                // ...
            } else if (!isLogin) {
                // 未登录
                // 跳转到登录界面,便于用户登录
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
            break;

        default:
            break;
    }

你高高兴兴地把代码交给了项目经理,经理一笑,说道:“小颜啊,可能是我之前没跟你说清楚,有些用户在填写注册信息的时候,地址栏那一层是没有填写的,那这样的话,我们的礼品是发不到用户手上的,所以即使是登录状态,我们还需要对用户是否填写了地址栏信息那块做一个判断,如果没有填写地址信息,我们应该让他跳到个人信息页面来填写地址栏信息。”

你拍拍胸脯道,“小意思经理,我再加一个 boolean 值不就可以了么?”经理笑了一笑,没有说什么。

你感叹道:“还好只需要改抽奖功能这一块代码。”没过多久你就写好了 ——

@Override
public void onClick(View view) {
    switch(view.getId()) {
        // 点赞按钮
        // ...

        // 转发按钮
        // ...

        // 抽奖按钮
        case R.id.btn_raffle:
            if (isLogin && isFinishedAddress) {
                // 已登录,并且已经填写地址栏
                // 实现抽奖的逻辑代码
                // ...
            } else if (!isLogin) {
                // 未登录
                // 跳转到登录界面,
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            } else if (isLogin && !isFinishedAddress) {
                // 已登录,但是没有填写状态栏信息
                // 跳转到个人信息页面,方便用户填写地址栏信息
                startActivity(new Intent(MainActivity.this, PersonInfoActivity.class))
            }
            break;

        default:
            break;
    }

这次你信心满满地找到经理,“经理,我做完了!”经理看了你修改之后的代码说道,“小颜啊,你的代码没有问题,但是你不觉得到处都是 if else 的判断逻辑,这样的代码很不便于阅读么?而且目前我们只有三个功能,假如我们后期有第四个第五个功能,假设第四个功能是需要用户登录并且上传了头像才能正常实现,如果登陆了没有上传头像,那么要跳到相应的界面,第五个功能是需要用户验证了邮箱,如果没有验证邮箱,我们会自动打开一个浏览器让用户去进行邮箱验证,那么你想想会有多少个 if else,而且后期需要对原有的代码进行修改,这可是我们的大忌,回去好好看一下状态模式吧。”“好的经理。”你迅速回去打开电脑百度到了一些信息 ——

状态模式 —— 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类

这句话看起来十分的干涩,你决定在实践中来理解它。首先定义一个接口以封装使用上下文环境的的一个特定状态相关的行为 —— 通俗来说,就是这个接口中有上面的点赞功能转发功能抽奖功能,接口如下:

public interface UserState {
    // 点赞功能
    void admire(Context context);

    // 转发功能
    void relay(Context context);

    // 抽奖功能
    void raffle(Context context);
}

接下来就是针对不同的状态实现这个接口了,你啪啪啪起键盘,首先是登录状态的实现:

public class LoginState implements UserState {
    @Override
    public void admire(Context context) {
        // 实现点赞功能的代码
        // ...
    }

    @Override
    public void relay(Context context) {
        // 实现转发功能的代码
        // ...
    }

    @Override
    public void raffle(Context context) {
        // 实现抽奖功能的代码
        // ...
    }
}

其次是非登录状态的实现,代码如下:

public class LogoutState implements UserState {
    @Override
    public void admire(Context context) {
        // 跳转到登录界面
        start2loginActivity(context);
    }

    @Override
    public void relay(Context context) {
        // 跳转到登录界面
        start2loginActivity(context);
    }

    @Override
    public void raffle(Context context) {
        // 跳转到登录界面
        start2loginActivity(context);
    }

    // 跳转到登录界面
    private void start2loginActivity(Context context) {
        gotoLoginActivity(context, LoginActivity.class);
    }
}

接下来就是用户登录但是没有填写地址栏信息的具体实现:

public class AddressState implements UserState {
    @Override
    public void admire(Context context) {
        // 跳转到个人信息页面
        start2personInfoActivity(context);
    }


    @Override
    public void relay(Context context) {
        // 跳转到个人信息页面
        start2personInfoActivity(context);
    }

    @Override
    public void raffle(Context context) {
        // 跳转到个人信息页面
        start2personInfoActivity(context);
    }

    // 跳转到个人信息页面
    private void start2personInfoActivity(Context context) {
        gotoPersonInfoActivity(context, PersonInfoActivity.class);
    }
}

接下来就是创建一个 Context 角色,它是用户的操作对象和状态管理对象,该角色会将相关的操作委托给 UserState 对象,代码如下:

public class LoginContext {
    // 默认为非登录状态
    UserState userState = new LogoutState();
    // 单例模式
    private static LoginContext loginContext = new LoginContext();

    private LoginContext() {
    }

    public static LoginContext getLoginContext() {
        return loginContext;
    }

    public void setUserState(UserState userState) {
        this.userState = userState;
    }

    // 点赞功能
    public void admire(Context context) {
        userState.admire(context);
    }

    // 转发功能
    public void relay(Context context) {
        userState.relay(context);
    }

    // 抽奖功能
    public void raffle(Context context) {
        userState.relay(context);
    }
}

接下来就是在注册的时候或者应用启动的时候获取用户信息,然后设置相应的 UserState 即可,例如检查到用户栏填写完整,那么我们就 LoginContext.getLoginContext().setUserState(new LoginState());,而地址栏我们并没有填写完整,那么我们就LoginContext.getLoginContext().setUserState(new AddressState());。或者我们在应用启动的时候从服务器获取信息,设置相应的 UserState。而 LoginContext 是单例的也很好理解,因为在你的应用中,登录状态应该只有一种,而不存在既有登录状态,又有非登录状态,如果我们不使用单例模式的话,就会出现用户既有登录状态,又有非登录状态,这样是不符合正常逻辑的。

再回到最初的按钮点击事件,你就开始重构代码了——

@Override
    public void onClick(View view) {
        switch(view.getId()) {
            // 点赞按钮
            case R.id.btn_admire:
                // 获取当前登录状态点赞功能的逻辑实现
                LoginContext.getLoginContext().admire(MainActivity.this);
                break;

            // 转发按钮
            case R.id.btn_relay:
                // 获取当前登录状态转发功能的逻辑实现
                LoginContext.getLoginContext().relay(MainActivity.this);
                break;

            // 抽奖按钮
            case R.id.btn_raffle:
                // 获取当前登录状态抽奖功能的逻辑实现
                LoginContext.getLoginContext().raffle(MainActivity.this);
                break;

            default:
                break;
    }

所以即使后期改变需求,针对一个上下文环境有需要新增加不同的行为实现,也仅仅需要实现 UserState 接口就可以了。而并不是对原有代码添加 if else 的判断逻辑,那样完全不符合对修改关闭,对扩展开放的开闭原则。

你将这份代码郑重地交给经理,经理这次高兴地拍着你的肩膀说道“这份代码我很满意,很不错,来,小颜,跟我说说你现在逻辑。”

“好的经理,我首先创建一个 UserState 接口,这个接口里面的方法是需要针对不同的上下文环境有不同的实现,例如点赞功能,假如用户登录了,我们就正常实现,如果没有登录,那么我们就跳转到登录的页面。接下来我们创建一个上下文环境,这个上下文环境 Context 内部持有一个 UserState 对象,然后这个 Context 里除了一个可以改变 UserStatesetter(UserState userstate) 方法之外,剩下的就是我们需要实现的那些功能,且 Context 自身是单例的,这样就能确保应用全局中 UserState 的值是唯一的,它要么是登录的,要么是非登录的,要么是登录状态但是并没有填写地址栏,或者后期应需求新增的其他状态,因为用户的状态只有一种可能性,不存在既登录又不登录的状态。”

“非常好,看来你学到了精髓,你要继续努力啊小伙子!”

“一定会的,经理!”你收拾收拾高高兴兴地继续去写代码了。

UML 类图:

这里写图片描述

状态模式的组成:

  • 环境类(Context): 定义客户感兴趣的接口。维护一个 ConcreteState 子类的实例,这个实例定义当前状态。例如我们上面的 LoginContext

  • 抽象状态类(State): 定义一个接口以封装与 Context 的一个特定状态相关的行为。例如我们上面的 UserState

  • 具体状态类(ConcreteState): 每一子类实现一个与 Context 的一个状态相关的行为。例如我们上面针对 UserState 接口实现的 LoginStateLogoutStateAddressState

状态模式的适用范围:

  • 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。

  • 代码中包含大量与对象状态有关的条件语句:一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State 模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

发布了41 篇原创文章 · 获赞 81 · 访问量 15万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章