状态设计模式 vs. 状态机






4.62/5 (31投票s)
如何使用状态设计模式,并与状态机、Switch语句或If语句进行比较。
引言
本文及附带的示例源代码解释并演示了状态设计模式,以及如何使用它来使您的代码更易于维护和阅读,特别是与实现状态机相比。
背景
软件开发中的设计模式是优秀软件创作的重要工具。能够识别代码中的模式,是经过多年面向对象软件开发实践才能获得的必备技能。多年来,我见过一些模式的实现,仅仅是在文件名中带有模式的名称,但实际上并没有很好地体现该模式的预期用法。此外,我还见过使用状态机代替状态设计模式,导致软件极其复杂且难以维护。当您使用面向对象编程语言时,已经没有理由再使用状态机了。
关于软件设计模式的最佳资源之一是“设计模式:可重用面向对象软件要素”一书,由Gang of Four所著。这本书至今仍是设计模式的圣经。虽然还有许多其他资源和书籍,但Gang of Four的这本蓝皮书是所有经验丰富的架构师和开发人员都应该掌握的基础。
设计模式与编程语言无关。它们传达和解决的概念可以应用于任何面向对象编程语言,如C#、C++、Delphi、Java、Objective-C等。正是这些概念,一个人应该去掌握。一旦掌握了这些概念,识别使用和应用它们的机会就相当直接了。届时,这仅仅是语言语法的问题。
在本文中,我将讨论状态设计模式。我将讨论状态设计模式如何在相当复杂的情况下使用,并通过C#代码示例进行演示。我还会讨论使用状态设计模式代替状态机。我不会深入介绍如何创建状态机的细节,而是将重点放在更现代的状态设计模式上。我选择了一个复杂的场景,因为我相信更复杂的场景可以一次教会几件事情。它将演示不同场景的组合,并以此回答更多问题。
状态设计模式
我将状态设计模式总结如下:
“状态设计模式允许对上下文的无限数量的状态进行完全封装,从而实现易于维护和灵活性。”
从业务角度来看,这价值千金。即使在非常简单的状态场景中,现在也没有理由不使用状态设计模式。例如,您可以摆脱switch语句(C#)。它为您带来了灵活性,因为您无法预测未来以及需求的变化(我对此相当确定)。
状态设计模式允许上下文(拥有特定状态的对象)根据当前活动的ConcreteState
实例表现出不同的行为。
让我们更仔细地看看构成状态设计模式的各个部分。
Context对象
Context是拥有(包含)状态的类的实例。Context是一个表示可以具有多种状态的对象的对象。事实上,它可能有很多种不同的状态。数量上真的没有限制。拥有数百种可能的状态对象是完全没问题的。不过,拥有状态对象的情况通常只有几种。
Context
对象至少有一个方法来处理请求,并将这些请求传递给状态对象进行处理。Context对可能的状态一无所知。Context不应了解这些不同状态的含义。重要的是,Context对象不应对状态进行任何操作(不改变状态)。唯一的例外是Context可以在启动时设置一个初始状态,因此必须知道该初始状态的存在。此初始状态可以在代码中设置,也可以来自外部配置。
Context唯一关心的是将请求传递给底层状态对象进行处理。不了解Context可能处于哪种状态的最大优点是,您可以随着时间的推移添加任意数量的新状态。这使得Context的维护超级简单和超级灵活。真正的省时器,离实现你 wildest dreams(几乎)更近一步。
状态
State
类是一个抽象类。它通常是抽象类而不是接口(IInterface
)。此类是所有可能状态的基类。此类通常是抽象类而不是接口的原因是,通常需要对所有状态应用通用操作。这些全局方法可以在此基类中实现。由于您无法在接口中进行任何实现,因此抽象类非常适合这一点。即使您没有任何初始全局基类方法,也请使用抽象类,因为您永远不知道以后是否可能需要基类方法。
State
类定义了所有状态都必须实现的所有可能的方法签名。这对于尽可能保持所有可能状态的维护简单性至关重要。由于所有状态都将实现这些方法签名,如果您忘记实现新方法,编译器将在编译时向您发出警告。一个很棒的安全网。
ConcreteState
ConcreteState
对象为Context对象实现了实际的状态行为。它继承自基类State。ConcreteState
类必须实现抽象基类State中的所有方法。
ConcreteState
对象拥有做出关于其状态行为决策所需的所有业务知识。它决定何时以及如何从一种状态切换到另一种状态。它了解其他可能的ConcreteState对象,以便在需要时切换到另一种状态。
ConcreteState
对象甚至可以检查其他Context对象及其状态以做出业务决策。很多时候,一个对象可能有多个Context对象。在这种情况下,ConcreteState
对象可能需要访问这些不同的状态并根据活动状态做出决策。这允许复杂的场景,但使用状态设计模式实现起来相当容易。您将在本文后面的示例中看到一个展示多个Context对象及其状态以及协同工作需求的示例。
ConcreteState
对象还能够处理状态转换之前和之后的操作。了解即将发生的转换是一项极其强大的功能。例如,这可以用于日志记录、审计记录、安全、触发外部服务、启动工作流等,以及许多其他用途。
与状态机相比,ConcreteState
对象能够充分利用编程语言。在抽象逻辑和条件语句与面向对象相结合方面,没有比计算机编程语言更强大的了,这与状态机及其实现相比。
随着时间的推移,当您向抽象基类添加新方法时,每个ConcreteState
类都需要实现该方法。这迫使您从当前状态的角度进行思考。
“当调用此方法时,状态ConcreteStateA应如何反应?”
当您实现方法的行为时,您可以放心,当ConcreteStateA
是活动状态时,整个系统中只有这个地方会处理此请求。您确切地知道在哪里维护该代码。可维护性是软件开发的关键。
摘要
总而言之,您需要一个Context和几个状态,最好是派生自抽象基类,以创建灵活的状态解决方案。如果您的代码中有switch语句或大量的If语句,您就有机会使用状态设计模式来简化。如果您正在使用状态机,您就有绝佳的机会来简化您的代码并节省时间和金钱。立即行动吧!
示例
我创建的示例演示了状态设计模式的使用以及如何将其与多个协同工作的Context对象一起使用。这是一个虚构的硬件设备,带有一个门。
该设备可以开启或关闭。具体来说,该设备有一个操作模式,可以处于以下状态:
- 空闲
- 忙碌
- 关机中
- 开机中
门代表设备上的物理门。门可以处于以下状态:
- 打开
- Closed
- 锁定
- 解锁
- 损坏
为了使其稍微复杂化,设备可以处于不同的硬件配置中。这些配置可以在设备运行时更改。以下配置可用:
- 生产配置
- 测试配置
设备的操作取决于不同的个体状态以及上述状态的组合。可能的状态组合越多,使用传统的Switch或If语句维护就越复杂。您也可以使用状态机,但与状态设计模式相比,它不会为您带来灵活性和易用性。随时添加全新的状态并进行实验。
分解
无论软件项目有多复杂,成功处理它们的方法是将其分解。这在面向对象软件开发中尤其如此。将事物分解成更小、可管理的部分,可以让我们集中精力理解问题域。这需要我们放大到更小的部分,然后缩小到10,000英尺的视角,反之亦然。您会做很多次。看大局,然后将大图分解成更小的部分,看更小的部分,等等。面向对象是模拟包含事物、人、过程及其相互作用行为的现实场景的天然匹配。
让我们分解一下我们在这个例子中知道的事情。看起来我们有三件事:
- Device
- 门
- 配置
行为
重要的是要认识到,这些事物之间很可能存在一定的行为。这种行为可能由特定的业务或运营规则驱动。面向对象的强大之处在于能够将这种行为捕获在类中。由于一个对象通常由大约50%的数据和50%的行为组成,因此我们必须处理对象的行为部分。随着时间的推移,由于需求的变化,这种行为可能会发生改变。同样,这就是面向对象正确实现时闪光的地方。
所以,我们可以假设设备是独立的。它代表现实世界中的物理设备。门是设备的一部分,不能独立存在。设备有一个门。所以,看起来我们有这个:
- 带门的设备
- 配置
我们还有一组配置。这些配置会改变设备的操作,但并非总是设备本身的一部分。所以,我们可以将配置建模为一个独立的类。然而,由于我们知道设备可以处于测试配置或生产配置中,这些实际上代表了操作状态。我们也知道,门的某些操作或状态可能会根据当前配置而有所不同。因此,没有必要创建一个单独的配置类,而是将配置本身建模为状态。如果我们以后决定添加另一种配置类型,这将很容易添加。
我们有两个主要部分:设备及其配置。我们将用各自的类来建模这两个部分。Device
类将包含一个Door
类(设备有一个门)。
Device
和Door
类都继承自DomainObject
类。DomainObject
类是我多年来习惯使用的约定。一个基本的域对象类包含了所有域对象共享的行为和特性。例如,我的DomainObject
类通常实现一个读/写字符串Name属性。此名称也可以选择性地传递到构造函数中。当您模拟现实世界的事物时,这些事物通常都有名称。因此,我最终在我的基本DomainObject
类中有一个Name属性。您稍后会看到它是如何使用的。代码
让我们开始编写一些代码并实现所有内容。我们正在构建的示例是一个控制台应用程序,它设置了几个状态来测试设备的行为。当输出显示在屏幕上时,它看起来会像这样:
首先,让我们创建DomainClass
,所有域对象的基类。
/// <summary>
/// Base class for domain objects that provides basic
/// functionality across all objects.
/// </summary>
public class DomainObject
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
public DomainObject()
{
}
public DomainObject(string name)
{
Name = name;
}
}
DomainObject
类实现了一个string
类型的Name
属性,该属性允许您方便地为对象命名,因为现实世界中的大多数对象都有名称。这是所有域类的基类。
接下来,我们实现Device
类。Device
类包含一个Door
对象。在此场景中,Device
类是操作模式和可能配置的Context(所有者)。操作模式由ModeState
类表示。不同类型的配置状态由ConfigurationState
类跟踪。
Initialize()
方法用于设置当前设备实例的不同类型状态。这是Context(Device)现在需要了解实际可用的状态的地方。还请注意,在此方法中,我们将操作模式设置为“Powering Up”,并在方法结束时将其设置为“Idle”。
/// <summary>
/// The Device class is the owner of the different states
/// that the Device can be in. The Device is also the
/// owner of actions (methods) that can be applied to the
/// states. In other words, Device is the thing we are
/// trying to manipulate through outside behavior.
/// </summary>
public class Device : DomainObject
{
// Device has a physical door represented by the
// Door class.
private Door _door;
// Device only knows about generic actions on
// certain states. So, we use the base classes of
// these states in order execute these commands.
// The base classes are abstract classes of the
// states.
private ConfigurationState _configurationState;
// The current mode that the device is in.
private ModeState _modeState;
public Device(string name) : base(name)
{
Initialize();
}
public Device()
{
Initialize();
}
private void Initialize()
{
// We are starting up for the first time.
_modeState = new ModePowerUpState(this);
_door = new Door(this);
// The initial configuration setting for the
// device. This initial configuration can come
// from an external configuration file, for
// example.
_configurationState = new ProductionConfigurationState(this);
// The door is initially closed
_door.DoorState = new DoorClosedState(_door);
// We are ready
_modeState.SetModeToIdle();
}
public Door Door
{
get { return _door; }
set { _door = value; }
}
public ConfigurationState Configuration
{
get { return _configurationState; }
set { _configurationState = value; }
}
public ModeState Mode
{
get { return _modeState; }
set { _modeState = value; }
}
}
ModePowerUpState
类是状态设计模式的ConcreteClass
实现之一。让我们更仔细地看看它的实现。
public class ModePowerUpState : ModeState
{
public ModePowerUpState(ModeState modeState)
{
Initialize();
this.Device = modeState.Device;
}
public ModePowerUpState(Device device)
{
Initialize();
this.Device = device;
}
private void Initialize()
{
Name = "Powering Up";
}
public override void SetModeToPowerUp()
{
// We're in powerup state already
}
public override void SetModeToIdle()
{
// Switch to Idle state
this.Device.Mode = new ModeIdleState(this);
}
public override void SetModeToBusy()
{
// Can't set mode to busy, we're still powering up
}
public override void SetModeToPowerDown()
{
// We're busy, but we allow to power down.
// Cleanup any resources and then set the state
this.Device.Mode = new ModePowerDownState(this);
}
}
首先要注意的是,它继承自ModeState
抽象基类。这将是状态设计模式中所有模式状态的抽象基类。此设备可能的操作模式如下:
- 开机中
- 关机中
- 忙碌
- 空闲
每种可能的模式都表示为单独的ConcreteState
类。
一个重要的事实是,其中一个构造函数接受模式的抽象表示。
public ModePowerUpState(ModeState modeState)
这个构造函数非常重要,因为它将允许对象通过多态来设置Context(所有者)。通过以下方式设置所有者:
this.Device = modeState.Device;
允许传入不同类型的模式,并且始终可以访问Context。一旦我们能够访问Context,此实例就可以操纵Device的模式。它还可以操纵任何其他属性或调用Device上的方法。
由于ModePowerUpState
类继承自抽象ModeState
类,因此它需要实现ModeState
类中声明的所有抽象方法。抽象ModeState
类声明了以下抽象方法:
public abstract void SetModeToPowerUp();
public abstract void SetModeToIdle();
public abstract void SetModeToBusy();
public abstract void SetModeToPowerDown();
ConcreteClass
ModePowerUpState
只需要实际实现有意义的方法。这里Idle
和PowerDown
状态是有意义的。
让我们看看设备门的 States。请记住,门可以处于以下状态:
- 打开
- Closed
- 锁定
- 解锁
- 损坏
门的抽象State
类如下所示:
public abstract class DoorState : DomainObject
{
protected Door _door;
public Door Door
{
get { return _door; }
set { _door = value; }
}
public abstract void Close();
public abstract void Open();
public abstract void Break();
public abstract void Lock();
public abstract void Unlock();
/// <summary>
/// Fix simulates a repair to the Door and resets
/// the initial state of the door to closed.
/// </summary>
public void Fix()
{
_door.DoorState = new DoorClosedState(this);
}
}
可能的状态在抽象方法中表示:
public abstract void Close();
public abstract void Open();
public abstract void Break();
public abstract void Lock();
我们还可以找到一个名为Fix()
的全局基方法。
public void Fix();
这个Fix()
方法旨在由任何派生的ConcreteState
类调用,以便将Door
带到初始的Closed状态(当它在损坏后被修复时)。
当您下载此示例源代码时,您可以更仔细地查看所有文件。但是,让我们看看更有趣的DoorUnlockedState
具体状态类。
public class DoorUnlockedState : DoorState
{
public DoorUnlockedState(DoorState doorState)
{
Initialize();
this.Door = doorState.Door;
}
public DoorUnlockedState(Door door)
{
Initialize();
this.Door = door;
}
private void Initialize()
{
Name = "Unlocked";
}
public override void Close()
{
// We can't close an already locked door.
}
public override void Open()
{
// Can't open a locked door.
}
public override void Break()
{
// To simulate production vs test configuration
// scenarios, we can't break a door in test
// configuration. So, we need to check the
// Device's ConfigurationState. We also want to
// make sure this is only possible while the
// device is Idle.
//
// Important:
// ==========
// As you can see in the If statement, we can
// now use a combination of different states to
// check business rules and conditions by simply
// combining the existence of certain class
// types. This is allows for super easy
// maintenance as it 100% encapsulates these
// rules in one place (in the Break() method in
// this case).
if ((this.Door.Device.Configuration is ProductionConfigurationState) &&
(this.Door.Device.Mode is ModeIdleState))
{
this.Door.DoorState = new DoorBrokenState(this);
}
}
public override void Lock()
{
this.Door.DoorState = new DoorLockedState(this);
}
public override void Unlock()
{
// We are already unlocked
}
}
仔细看看Break()
方法。这才是真正有趣的地方,它演示了如何使用多个不相关的状态集。在这种情况下,状态需要检查设备是否处于特定配置以及处于特定操作模式,然后才能设置门的状态。为了访问这些条件,状态需要访问两个Context:
由于此场景仅允许在设备处于生产配置且操作模式为空闲时才能打破门,因此通过使用状态类定义来验证这两个条件。
if ((this.Door.Device.Configuration is ProductionConfigurationState) &&
(this.Door.Device.Mode is ModeIdleState))
{
this.Door.DoorState = new DoorBrokenState(this);
}
通过简单地将类定义链接在比较中,您就可以获得干净的编译时验证,这与字符串或类似比较相比。代码易于阅读和扩展。
请记住,当您使用面向对象时,您的目标之一是封装。您有一个中心位置来维护您需要修改的特定状态的代码,或者在您需要创建全新状态时进行维护。
结论
使用状态设计模式而不是Switch和If语句以及状态机是一个强大的工具,可以使您的生活更轻松,为您的雇主节省时间和金钱。就这么简单。
您可以在这里下载源代码。(https://s3.amazonaws.com/StateDesignPattern/DeviceWithStateDesign.zip)
历史
- 版本 1.0 - 初始发布。