《Android源码设计模式》之状态模式

####状态模式介绍
  状态模式中的行为是由状态来决定的,不同的状态下有不同的行为。状态模式和策略模式的结构几乎完全一样,但它们的目的、本质却完全不一样。状态模式的行为是平行的、不可替换的,策略模式的行为是彼此独立的、可相互替换的。用一句话来表述,状态模式把对象的行为包装在不同的状态对象里,每一个状态对象都有一个共同的抽象状态基类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之变化。
####状态模式的定义
  当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
####状态模式的使用场景
  (1)一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。
  (2)代码中包含大量与对象状态有关的条件语句,例如,一个操作中含有庞大的多分支语句(if-else或switch-case),且这些分支依赖于该对象的状态。
  状态模式将每一个条件分支放入一个独立的类中,这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化,这样通过多态来去除过多的、重复的if-else等分支语句。
####状态模式的UML类图

![这里写图片描述](//img-blog.csdn.net/20180427094916604?watermark/2/text/Ly9ibG9nLmNzZG4ubmV0L3FxXzE2MjQwMzkz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
  • Context:环境类,定义客户感兴趣的接口,维护一个State子类的实例,这个实例定义了对象的当前状态。
  • State:抽象状态类或者状态接口,定义一个或者一组接口,表示该状态下的行为。
  • ConcreteStateA、ConcreteStateB:具体状态类,每一个具体的状态类实现抽象State中定义的接口,从而达到不同状态下的不同行为。
    ####状态模式的简单示例
    下面我们以电视遥控器为例来演示一下状态模式的实现。我们先将电视的状态简单分为开机状态和关机状态,在开机状态下可以通过遥控器进行频道切换、调整音量等操作,但是,此时重复按开机键是无效的;而在关机状态下,频道切换、调整音量、关机都是无效的操作,只有按开机按钮时会生效。也就是说电视的内部状态决定了遥控器的行为,我们看看第一版实现:
package com.guifa.statedemo;

/**
 * 电视遥控器,含有开机、关机、下一频道、上一频道、调高音量、调低音量这几个功能
 */
public class TvController {
    /**
     * 开机状态
     */
    private static final int POWER_ON = 1;
    /**
     * 关机状态
     */
    private static final int POWER_OFF = 2;
    private int mState = POWER_OFF;

    public void powerOn() {
        mState = POWER_ON;
        if (mState == POWER_OFF) {
            System.out.println("开机啦");
        }
    }

    public void powerOff() {
        mState = POWER_OFF;
        if (mState == POWER_ON) {
            System.out.println("关机啦");
        }
    }

    public void nextChannel() {
        if (mState == POWER_ON) {
            System.out.println("下一频道");
        } else {
            System.out.println("两个红灯提示没有开机");
        }
    }

    public void prevVhannel() {
        if (mState == POWER_ON) {
            System.out.println("上一频道");
        } else {
            System.out.println("两个红灯提示没有开机");
        }
    }

    public void turnUp() {
        if (mState == POWER_ON) {
            System.out.println("调高音量");
        } else {
            System.out.println("两个红灯提示没有开机");
        }
    }

    public void turnDown() {
        if (mState == POWER_ON) {
            System.out.println("调低音量");
        } else {
            System.out.println("两个红灯提示没有开机");
        }
    }
}

可以看到,在TvController类中,通过mState字段存储了电视机的状态,并在各个操作中根据状态来判断是否应该执行。这就导致了在每个功能中都需要用到if-else,代码重复、相对较为混乱,如果当状态变成5个、功能函数变为10个,每个函数中都需要用到if-else进行判断,而这些代码都充斥在一个类中,使得这个类变得越来越难以维护。
  状态模式就是为解决这类问题而出现的,我们将这些状态用对象来代替,将这些行为封装到对象中,使得在不同的状态下有不同的实现,这样就将这些if-else从TVController类中去掉,整个结构也变得清晰起来。我们看看代码实现:

package com.guifa.statedemo;

/**
 * 电视状态接口,定义了电视操作函数
 */
public interface TvState {
    public void nextChannel();

    public void prevChannel();

    public void turnUp();

    public void turnDown();
}
package com.guifa.statedemo;

/**
 * 关机状态,此时只有开机功能是有效的
 */
public class PowerOffState implements TvState {

    @Override
    public void nextChannel() {

    }

    @Override
    public void prevChannel() {

    }

    @Override
    public void turnUp() {

    }

    @Override
    public void turnDown() {

    }
}
package com.guifa.statedemo;

/**
 * 开机状态,此时再触发开机功能不做任何操作
 */
public class PowerOnState implements TvState {

    @Override
    public void nextChannel() {
        System.out.println("下一频道");
    }

    @Override
    public void prevChannel() {
        System.out.println("上一频道");
    }

    @Override
    public void turnUp() {
        System.out.println("调高音量");
    }

    @Override
    public void turnDown() {
        System.out.println("调低音量");
    }
}
package com.guifa.statedemo;

/**
 * 电源操作接口
 */
public interface PowerController {
    public void powerOn();

    public void powerOff();
}
package com.guifa.statedemo;

/**
 * 电视遥控器,类似于经典状态模式种的Context
 */
public class TvController implements PowerController {

    TvState mTvState;

    public void setTvState(TvState mTvState) {
        this.mTvState = mTvState;
    }

    @Override
    public void powerOn() {
        setTvState(new PowerOnState());
        System.out.println("开机啦");
    }

    @Override
    public void powerOff() {
        setTvState(new PowerOffState());
        System.out.println("关机啦");
    }

    public void nextChannel() {
        mTvState.nextChannel();
    }

    public void prevChannel() {
        mTvState.prevChannel();
    }

    public void turnUp() {
        mTvState.turnUp();
    }

    public void turnDown() {
        mTvState.turnDown();
    }
}

下面是客户端的调用代码:

package com.guifa.statedemo;

/**
 * 客户端调用代码
 */
public class Client {
    public static void main(String[] args) {
        TvController tvController = new TvController();
        // 设置开机状态
        tvController.powerOn();
        // 下一频道
        tvController.nextChannel();
        // 调高音量
        tvController.turnUp();
        // 设置关机状态
        tvController.powerOff();
        // 调高音量,此时不会生效
        tvController.turnUp();
    }
}

输出结果如下:

![这里写图片描述](https://img-blog.csdn.net/20180502165030999?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzE2MjQwMzkz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

上述实现中,我们抽象了一个TvState接口,该接口中有操作电视的所有函数,该接口有两个实现类,即开机状态(PowerOnState)和关机状态(PowerOffState)。开机状态下只有开机功能是无效的,也就是说在已经开机的时候用户再按开机键不会产生任何反应;而在关机状态下,只有开机功能是可用的,其他功能都不会生效。同一个操作,如调高音量的turnUp函数,在关机状态下无效,在开机状态下就会将电视的音量调高,也就是说电视的内部状态影响了电视遥控器的行为。状态模式将这些行为封装到状态类中,在进行操作时将这些功能转发给状态对象,不同的状态有不同的实现,这样就通过多态的形式去除了重复、杂乱的if-else语句,这也正是状态模式的精髓所在。
####状态模式实战
  在开发过程中,我们用到状态模式最常见的地方应该是用户登录系统。在用户已登录和未登录的情况下,对于同一事件的处理行为是不一样的,例如,在新浪微博中要转发一条内容,用户在未登录的情况下点击转发按钮,此时会先让用户登录,然后再执行转发操作;如果是已登录的情况下,那么用户输入转发的内容后就可以直接进行操作。可见,在这两种状态下,对于转发这个操作的处理动画完全不一样,当状态改变时对于转发操作的行为发生了改变。
  下面我们用状态模式来简单实现这个过程,首先创建一个Android项目,里面含有两个Activity,分别为MainActivity、LoginActivity,MainActivity是应用第一个Activity,有转发和注销用户功能,LoginActivity则为用户登录界面。
  用户默认状态为未登录状态,此时用户再MainActivity界面点击转发时会先跳转到登录界面,然后在登录界面登录成功后再回到MainActivity页面,此时,用户再进行转发操作就可以实现真正的转发功能。
  先定义UserState接口,并定义了两个方法,即转发和评论。

package com.guifa.statedemo;

import android.content.Context;

/**
 * 用户状态
 */
public interface UserState {
    /**
     * 转发
     *
     * @param context context
     */
    public void forward(Context context);

    /**
     * 评论
     *
     * @param context context
     */
    public void comment(Context context);
}

新建两个类并实现UserState接口,即用户已登录和未登录状态。

package com.guifa.statedemo;

import android.content.Context;
import android.widget.Toast;

/**
 * 已登录状态
 */
public class LoginedState implements UserState {

    @Override
    public void forward(Context context) {
        Toast.makeText(context, "转发微博", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void comment(Context context) {
        Toast.makeText(context, "评论微博", Toast.LENGTH_SHORT).show();
    }
}
package com.guifa.statedemo;

import android.content.Context;
import android.content.Intent;

/**
 * 注销状态,即未登录状态
 */
public class logoutState implements UserState {

    @Override
    public void forward(Context context) {
        gotoLoginActivity(context);
    }

    @Override
    public void comment(Context context) {
        gotoLoginActivity(context);
    }

    private void gotoLoginActivity(Context context) {
        Intent intent = new Intent(context, LoginActivity.class);
        context.startActivity(intent);
    }
}

这里的LoginContext就是状态模式中的Context角色,是用户的操作对象和状态管理对象,LoginContext将相关操作委托给状态对象,这样状态发生改变时,LoginContext的行为就发生了改变。LoginContext通过setState来对状态进行修改,相关代码如下:

package com.guifa.statedemo;

import android.content.Context;

/**
 * LoginContext,用户接口和状态管理类
 */
public class LoginContext {
    /**
     * 用户状态,默认为未登录状态
     */
    UserState mState = new logoutState();
    /**
     * 单例
     */
    static LoginContext sLoginContext = new LoginContext();

    public LoginContext() {
    }

    public static LoginContext getLoginContext() {
        return sLoginContext;
    }

    public void setState(UserState mState) {
        this.mState = mState;
    }

    /**
     * 转发
     *
     * @param context context
     */
    public void forward(Context context) {
        mState.forward(context);
    }

    /**
     * 评论
     *
     * @param context context
     */
    public void comment(Context context) {
        mState.comment(context);
    }
}

MainActivity的代码如下:

package com.guifa.statedemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 转发按钮
        findViewById(R.id.forward_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 调用LoginContext的转发函数
                LoginContext.getLoginContext().forward(MainActivity.this);
            }
        });
        // 注销函数
        findViewById(R.id.logout_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 设置为注销状态
                LoginContext.getLoginContext().setState(new logoutState());
                Toast.makeText(MainActivity.this, "注销成功", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

LoginActivity则是在用户输入用户名和密码之后执行登录,成功之后将LoginActivity的状态设置为已登录状态,并且返回MainActivity页面,具体代码如下:

package com.guifa.statedemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

public class LoginActivity extends AppCompatActivity {

    private EditText userNameEditText;
    private EditText pwdEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        userNameEditText = findViewById(R.id.username_edittext);
        pwdEditText = findViewById(R.id.pwd_edittext);
        findViewById(R.id.login_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                login();
                finish();
            }
        });
    }

    private void login() {
        String userName = userNameEditText.getText().toString().trim();
        String pwd = pwdEditText.getText().toString().trim();
        // 执行网络请求,进行登录...

        // 登录成功后修改为已登录状态
        LoginContext.getLoginContext().setState(new LoginedState());
        Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
    }
}

Activity的XML文件很简单就不写了。最终的效果如下:

![这里写图片描述](//img-blog.csdn.net/20180504164935162?watermark/2/text/Ly9ibG9nLmNzZG4ubmV0L3FxXzE2MjQwMzkz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

####总结
  状态模式的关键点在于不同的状态下对于同一行为有不同的响应,这其实就是一个将if-else用多态来实现的一个具体示例。在if-else或者switch-case形式下根据不同的状态进行判断,如果是状态A那么执行方法A、状态B执行方法B,但这种实现使得逻辑耦合在一起,易于出错,通过状态模式能够很好地消除这类“丑陋”的逻辑处理,当然并不是任何出现if-else的地方都应该通过状态模式重构,模式的运用一定要考虑所处的情景以及你要解决的问题,只有符合特定的场景才建议使用对应的模式。
#####优点
  State模式将所有与一个特定的状态相关的行为都放入一个状态对象中,它提供了一个更好的方法来组织与特定状态相关的代码,将烦琐的状态判断转换成结构清晰的状态类族,在避免代码膨胀的同时也保证了可扩展性与可维护性。
#####缺点
状态模式的使用必然会增加系统类和对象的个数。

你可能感兴趣的:(《Android源码设计模式》之状态模式)