设计模式之状态模式

概述

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致性地处理。

状态模式定义如下:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。

在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构如图所示:

从图中可以看出,在状态模式结构图中包含以下 3 个角色:

  1. Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类 State 的实例,这个实例定义当前状态,在具体实现时,它是一个 State 子类的对象。
  2. State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明各种不同状态对应的方法,而在其子类中实现这些方法。由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
  3. ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。

在状态模式中,将对象在不同状态下的行为封装到不同的状态类中,为了让系统具有更好的灵活性和可扩展性,同时对各状态下的共有行为进行封装,需要对状态进行抽象,引入了抽象状态类角色,其典型代码如下:

class State {
public:
	// 声明抽象业务方法 不同的具体状态类可以以不同的方法实现
	virtual void handle() = 0;
};

在抽象状态类的子类即具体状态类中实现了在抽象状态类中声明的业务方法,不同的具体状态类可以提供完全不同的方法实现。在实际使用时,一个状态类中可能包含多个业务方法,如果在具体状态类中某些业务方法的实现完全相同,可以将这些方法移至抽象状态类,实现代码的复用。典型的具体状态类代码如下:

class ConcreteState : public State {
public:
	virtual void handle() override;
};

环境类维持一个对抽象状态类的引用,通过 setState() 方法可以向环境类注入不同的状态对象,再在环境类的业务方法中调用状态对象的方法,典型代码如下:

class Context {
public:
	// 设置对象状态
	void setState(State* st) { state = st; }

	void request() {
		// ...
		state.handle();	// 调用状态对象的业务方法
		// ...
	}

private:
	State* state;	// 维持一个对抽象状态对象的引用
	int value;		// 其他属性值 该属性值的变化可能会导致对象状态发生变化
};

环境类实际上是真正拥有状态的对象,这里只是将环境类中与状态有关的代码提取出来封装到专门的状态类中。在上面所示的状态模式结构图中,环境类 Context 与抽象状态类 State 之间存在单向关联关系,在 Context 中定义了一个 State 对象。在实际使用时,它们之间可能存在更为复杂的关系,State 与 Context 之间可能也存在依赖或者关联关系。

在状态模式的使用过程中,一个对象的状态之间还可以进行相互转换,通常有以下两种实现状态转换的方式:

方式一: 统一由环境类来负责状态之间的转换。此时,环境类还充当了状态管理器(StateManager)角色。在环境类的业务方法中通过对某些属性值的判断实现状态转换,还可以提供一个专门的方法用于实现属性判断和状态转换,代码片段如下:

void changeState() {
	// 判断属性值 根据属性值进行状态转换
	if (value == 0) {
		setState(new ConcreteStateA());
	} else if (value == 1) {
		setState(new ConcreteStateB());
	}
	...
}

方式二: 由具体状态类来负责状态之间的转换。可以在具体状态类的业务方法中判断环境类的某些属性值再根据情况为环境类设置新的状态对象,实现状态转换;同样,也可以提供一个专门的方法来负责属性值的判断和状态转换。此时,状态类与环境类之间将存在依赖或关联关系,因为状态类需要访问环境类中的属性值,代码片段如下:

void changeState(const Context& ctx) {
	// 根据环境对象中的属性值进行状态转换
	if (ctx.getValue() == 1) {
		ctx.setState(new ConcreteStateA());
	} else if (ctx.getValue() == 2) {
		ctx.setState(new ConcreteStateB());
	}
	...
}

共享状态

在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。

下面通过一个简单实例来说明如何实现共享状态。如果某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。可以使用状态模式来实现开关的设计,其结构如图所示:

总结

状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流、游戏等软件中状态模式都得到了广泛应用,例如公文状态的转换、游戏中角色的升级等。

优点

  1. 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
  2. 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
  3. 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
  4. 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点

  1. 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
  2. 状态模式的程序结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
  3. 状态模式对开闭原则的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

适用场景

  1. 对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化
  2. 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
posted @ 2022-12-04 09:38  Leaos  阅读(29)  评论(0编辑  收藏  举报