状态模式 是一种行为设计模式,允许对象根据其内部状态的变化来改变其行为。简单来说,状态模式可以让对象根据当前的状态表现不同,而无需在代码中使用大量的 if/else
或 switch
语句。
在我们上一篇文章中探讨了如何将操作封装成对象,从而使代码更加灵活。状态模式也采取了类似的方法,但重点是将状态及其行为封装成对象。就像命令模式一样,状态模式同样帮助我们消除冗长的条件判断,并遵循良好的设计原则——但它解决的是不同类型的问题。
状态模式
一个现实世界的类比:手机通知方式比如说,你有一部智能手机,它有多种通知 模式 : 正常 、 震动 和 静音 。在正常模式下,来电会发出响铃声。在震动模式下,手机不会发出响铃声但会震动提醒。在静音模式下,手机既不发出响铃声也不震动,会记录未接来电。你可以根据不同的场合手动切换这些模式(比如在工作、开会或看电影时),而手机的行为会相应地调整,而不需要你每次调整内部设置。
这也是一种状态模式的容易理解的类比。
- 手机是行为会根据模式变化的对象。
- 当前模式是正常的、振动的或静音的。
- 每种状态定义了手机在接收来电等特定操作时应该如何反应。
- 切换模式就像是改变手机的内部状态,从而改变手机的行为。
为什么不直接用if-else或枚举呢? 你可以用简单的if
或switch
来实现手机的行为逻辑。
if (mode == NORMAL) {
响铃声音大
} else if (mode == VIBRATE) {
是震动模式
} else if (mode == SILENT) {
是静音模式
}
这在几个模式下工作得很好。但如果手机有十几种模式,每个模式都会影响多种功能,比如电话、短信、闹钟和通知。条件分支会变得更多,每个模式的逻辑会分散到代码中的许多 if
语句里。这样维护起来就会变得非常容易出错。
状态模式提供了一种更干净的方法:将每种模式视为一个单独的状态类,每个类都有自己的逻辑。手机会持有某一个状态对象的引用(例如,一个 SilentState
或 VibrateState
类的实例),并将行为委托给这个状态对象。当你切换模式时,实际上会将状态对象替换为另一个对象。这样,就无需使用复杂的条件判断,转而依赖于多态特性——每个状态类都知道如何正确处理该状态下的操作。
状态模式有这些几个关键部分相互配合工作:
- 具有动态内部状态的主要对象是上下文。在我们的类比中,
Phone
是上下文。 - 状态接口:定义不同状态行为的公共接口。它声明了上下文希望处理的方法。例如,
PhoneState
接口可能会声明一个方法如handleIncomingCall()
。 - 具体状态类:这些是状态对象。每个具体状态类表示一个特定的状态,并实现了状态接口,提供在该状态下需要的行为。例如,
NormalState
、VibrateState
、SilentState
类各自实现了处理来电的不同方式。 - 状态转换:状态转换通常由上下文中的一个方法来执行。这可以通过外部触发器(如用户更改模式)或内部逻辑(如状态对象决定转换到不同状态)来实现。
当接收到一个请求(如 phone.receiveCall()
)时,并不会直接处理。而是将处理委托给当前状态(比如 currentState.handleIncomingCall()
)。因为每个状态对象的实现不同,结果也会不同。在代码中,这就是多态性的表现:一个方法调用,根据当前状态对象,会表现出不同的行为。
使用状态模式的主要动机是消除散布在代码各处的重复条件逻辑。如果一个对象的行为随状态变化,你可能会倾向于使用枚举或标志来追踪状态,然后在每个需要行为不同的方法中使用switch
/if
语句。这会导致冗长且难以维护的代码。状态模式通过将特定于状态的逻辑拆分到独立的类中来解决这一问题。
- 每个状态的逻辑都存在于它自己的类中(例如,静音模式的所有逻辑都在
SilentState
类中)。 - 上下文代码变得更加简单;不再需要大型的条件块来处理特定状态下的行为。
- 添加新状态或修改现有状态不需要在多个地方编辑巨大的
switch
语句——你只需要创建一个新的状态类或更新现有的类。
根据经典的定义,"操作行为包含依赖于对象状态的大型多分支条件语句……状态模式将条件语句的每个分支放在一个单独的类中,将状态作为独立对象来处理". 这种封装使代码遵循开闭原则:我们可以在不更改上下文或其他状态代码的情况下引入新的状态。这也符合单一职责原则,因为每个状态类只负责一种行为。
何时使用状态模式,何时不使用。状态模式是指在软件工程中,一种允许对象在其内部状态改变时改变行为的模式。当需要使用状态模式时:
- 一个对象的行为依赖于它的当前状态,并且在运行时需要根据此状态改变。如果你发现自己在多个地方写“如果状态是X则做这个,如果状态是Y则做那个”这样的代码,说明状态模式可能会有所帮助,或者你发现你经常需要在代码中添加类似的状态检查。
- 对象具有可以干净地分离的多种行为。例如,电话的响铃、震动、静默记录等行为是独立的。
- 你希望避免重复的状态检查逻辑。与其在多个方法中复制粘贴相同的基于状态的开关,状态模式将行为集中到状态类中。
- 你预料到未来可能会添加新的状态或每种状态的逻辑会变得更复杂。该模式使得添加新状态或修改现有状态逻辑变得更简单,扩展(只需添加一个新的状态类),或者修改(只需修改一个类的代码)。
以下情况不宜使用(或者要小心):
- 如果一个对象只有几个状态且行为差异非常简单,使用状态模式可能有些小题大做。在这种简单的情况下,直接使用条件判断可能更易读。
- 如果状态变化很少或逻辑不太可能增长,额外的类可能会显得有些多余。
- 如果状态的数量是固定的且不会改变且每个状态的逻辑都很简单,一个简单的枚举和 switch 可能已经足够。在复杂情况下,随着状态和行为的增加或变化,该模式在这种情况下就显示出它的优势。
这么想吧:一个小的状态机只有两个状态,用一个 if
来维护就简单多了。但一个有十个状态且转换复杂的状态机,用状态模式来管理就简单得多。
通常我们会用 enum
或一组布尔标志来表示状态,例如:你可以有一个状态来表示某个功能是否启用(如:enum Feature {Enabled, Disabled}
)。
枚举模式 {正常, 振动, 无声}
然后你可以这样写逻辑
if (mode == Mode.normal) {
// 响亮的铃声
} else if (mode == Mode.vibrate) {
// 振动模式
} else if (mode == Mode.silent) {
// 静音模式
}
这种方法可行,但随着软件变得越来越大,可能就会遇到一些麻烦。
- 分散逻辑: 如果多个行为依赖于模式状态,你将在许多方法 (
handleCall()
,notifyMessage()
,alarmRing()
等) 中有相似的if/else
或switch
块。任何模式行为的改变都需要在每个条件判断中进行相应的更新。 - 违反开闭原则: 要添加一个新模式,比如“请勿打扰”模式,你必须修改所有那些
switch
语句。每次修改都可能引入错误并影响现有代码的功能。 - 难以维护: 随着状态和条件的增加,代码会变得越来越难以阅读和维护。它会变成一个庞大且复杂的 状态机,与业务逻辑交织在一起。
状态模式通过封装特定状态下的行为来解决这些问题。不再是一个拥有多个分支的大函数,而是有许多小类,每个小类负责一个分支。这使得分离更加清晰。
- Phone 类(上下文)不再需要知道每种模式的行为细节。它只需委托给当前的状态对象。
- 比如添加 静音模式,意味着创建一个新的
DoNotDisturbState
类来实现所需的行为。Phone 类可能只需要做很小的修改(甚至根本无需修改,如果状态可以通过setter或某个工厂来设置的话)。 - 移除或更改某个状态的行为只会影响那个状态类,从而减少对其他代码部分的影响。
简而言之:在复杂场景中,状态模式比使用枚举/标志配合条件判断提供了更稳健、更灵活的解决方案。它保持了代码的模块化性,并遵循设计原则,使得前端、后端、移动端等不同领域的开发人员更容易理解逻辑,而无需在复杂的条件中筛选。
状态模式的好处和坏处:和任何设计模式一样,状态模式也有其优缺点。我们来看看这些:
好的方面:
- 状态特定的代码被隔离到单独的类中: 这满足了单一职责原则,因为每个状态类专注于一个行为集(对象的一个“模式”)。
- 消除复杂的条件判断: 上下文代码从冗长的
if/else
链或不同状态的 switch 语句中解脱出来。这通常意味着上下文类(如Phone
)变得更简单,更容易维护。 - 符合开闭原则的要求: 你可以添加新的状态,而无需修改现有的状态或上下文,这符合开闭原则的要求。例如,添加一个新的电话模式不需要修改其他模式的逻辑。
- 状态转换可以由上下文或者状态对象来处理: 从一个状态转换到另一个状态的逻辑可以在一个地方控制。这使状态的流转更容易管理和理解。
- 多态的行为: 该模式利用了多态性。你可以在运行时通过交换状态对象来引入新的行为。这可以减少错误,因为上下文只需调用一个方法,而这个方法现在执行了不同的操作。
坏处:
- 更多的类的增多: 引入状态模式意味着要创建多个类(每个状态一个类)。对于简单的情况,这可能会感觉像是过度工程。额外的间接层理解成本可能不值得,如果一个简单的条件判断可以解决问题的话。
- 状态爆炸问题: 如果一个对象可以有很多状态,你将会有大量的类。管理大量状态间的转换本身也会变得相当复杂。(缓解措施:将相关状态分组或使用分层状态模式,或者考虑是否真的需要所有这些状态。)
- 状态与其上下文的紧密耦合: 状态对象通常需要了解它们的上下文(以改变状态或查询上下文数据),有时还需要了解其他状态(如果一个状态决定切换到另一个状态)。这可能会引入状态类之间的耦合。然而,这通常是受控且局部化的耦合。对于消除全局复杂性来说,这是一个可以接受的权衡,但这需要注意。
- 学习曲线: 对于一些开发人员(尤其是不熟悉设计模式的开发人员),理解“对象拥有一个对象来做它的工作”这种间接性可能会在一开始感到困惑。在熟悉这种模式之前,阅读代码时可能会感到不直观。
- 内存/性能开销: 在某些语言中,为状态创建对象可能会有一定的性能成本(虽然在大多数情况下这是微不足道的)。如果状态对象存储了大量从上下文复制的数据,这可能会效率低下。实际上,状态对象通常是轻量级的,甚至是单例的,所以这很少是一个大问题。
减少不利影响: 如果你觉得类太多,你可以将状态对象设为内部类甚至匿名类(如果语言支持的话),这样可以将它们与上下文联系起来。如果你担心对象创建的代价,你可以重复利用状态实例(状态模式并不强制每次都要创建新的对象;你可以使用单例或无状态实例)。此外,良好的命名和文档可以帮助学习曲线,通过明确每个状态类的职责。
在Dart中实现状态模式(State Pattern)——手机模式示例为了更好地掌握这些概念,让我们用 Dart 语言实现我们的智能手机通知模式的示例。我们将创建一个简单的模拟,展示手机在不同模式下接收来电的简单模拟。代码是自包含的,并输出到控制台上的信息(因此你可以在在线 Dart 环境(如在线 Dart 编辑器)中运行它)。
这个示例的设计:
- 我们有抽象的
PhoneState
类,定义了当收到电话呼叫时(onReceiveCall
方法)会发生什么。 - 我们有具体的状态:
NormalState
、VibrateState
和SilentState
,这些类继承自PhoneState
类,并各自实现了onReceiveCall
方法。 Phone
类是我们的上下文类。它有一个类型为PhoneState
的state
属性。它将receiveCall()
操作委托给当前状态对象,并提供了一个切换状态的方法(setState()
)。- 我们将模拟电话在不同模式下接收到呼叫的情况,看看它会如何响应。
下面就是Dart代码。
// 状态接口(Dart中的抽象类)
abstract class PhoneState {
void onReceiveCall(Phone context);
}
// 具体状态:正常模式(响铃)
class NormalState implements PhoneState {
@override
void onReceiveCall(Phone context) {
print("来电:铃铃!📢 (正常模式)");
// 在正常模式下,电话会响铃。
// (此模式不会自动切换。)
}
}
// 具体状态:振动模式
class VibrateState implements PhoneState {
int _vibrateCount = 0; // 例如,计数来电
@override
void onReceiveCall(Phone context) {
_vibrateCount++;
print("来电:嗡嗡嗡… 🤫 (振动模式)");
// 如果在振动模式下收到太多来电,可能会自动切换到静音模式:
if (_vibrateCount >= 3) {
print("在$_vibrateCount次振动后未接听,切换到静音模式。");
context.setState(SilentState());
// 注意:这只是演示状态触发转换的示例。
// 实际生活中,手机通常不会如此自行切换!
}
}
}
// 具体状态:静音模式
class SilentState implements PhoneState {
@override
void onReceiveCall(Phone context) {
print("来电:(静音模式,无声音) 🤐");
print("电话保持静音,你稍后可能会看到未接来电。");
// 在静音模式下,可能在上下文中记录未接来电(此处略去)
}
}
// 上下文:电话
class Phone {
// 默认从正常模式开始
PhoneState _state = NormalState();
void setState(PhoneState newState) {
_state = newState;
// 如果需要,您也可以在此处打印或记录模式切换。
}
void receiveCall() {
// 将行为委托给当前状态对象
_state.onReceiveCall(this);
}
// (可选)一个辅助函数,显示当前状态的字符串形式,用于日志记录
String get modeName => _state.runtimeType.toString();
}
void main() {
Phone phone = Phone();
print("电话现在处于${phone.modeName}模式下。");
// 模拟正常模式下的来电
phone.receiveCall(); // 应该响铃提醒
// 切换到振动模式
phone.setState(VibrateState());
print("\n电话现在处于${phone.modeName}模式下。");
phone.receiveCall(); // 振动
phone.receiveCall(); // 再次振动
phone.receiveCall(); // 第三次振动,触发切换到静音模式
// 现在电话应该自动切换到静音模式
print("\n电话现在处于${phone.modeName}模式下。");
phone.receiveCall(); // 静音,无声音
// 手动切换回正常模式
phone.setState(NormalState());
print("\n电话现在处于${phone.modeName}模式下。");
phone.receiveCall(); // 再次响铃提醒
}
在上述代码中,需要注意几点重要事项。
Phone
上下文并不知道接收到电话时会怎样;它只是调用_state.onReceiveCall(this)
。当前状态对象来处理。这就是状态模式的精髓。- 每个状态类专注于一种模式的行为。例如,
SilentState
会打印一条消息,说明电话处于静音状态,并且可能记录一个未接来电(我们仅通过打印来模拟)。 - 我们在
VibrateState
中加入了一些有趣的逻辑:如果你在振动模式下连续三次没有接听电话,手机将自动切换到静音模式(以此展示状态可以内部改变上下文状态)。这展示了状态对象如何触发状态切换。 - 切换状态就像调用
phone.setState(SomeState())
一样简单。这可能由用户操作或程序中的某个条件触发。
运行这段代码会得到如下所示的输出:
手机现在处于正常状态。
来电:铃铃铃!📢(正常模式)
手机现在处于振动状态。
来电:嗡嗡嗡... 🤫(振动模式)
来电:嗡嗡嗡... 🤫(振动模式)
来电:嗡嗡嗡... 🤫(振动模式)
振动三次后仍未接听,转为静音模式。
手机现在处于静音状态。
来电:(静音模式,无声) 🤐
手机保持静默。你可能稍后会看到漏接电话的提示。
手机现在处于正常状态。
来电:铃铃铃!📢(正常模式)
你可以看到,随着状态(模式)的变化,行为也随之改变;甚至振动状态还会导致内部切换到静音状态。所有这一切都是自动发生的,无需该Phone
类自身处理响铃声、振动或静音的逻辑。手机只需根据当前状态进行操作。
虽然状态模式很强大,但它并不是万能的,不能解决所有问题:
复杂 vs 简单——总是评估你问题的复杂性。当状态模式可以简化整体复杂性时,请使用状态模式。如果感觉它增加了不必要的复杂性,考虑更简单的替代方案。一般来说,如果你有超过两三种可能变化的行为,考虑使用状态模式会是个不错的选择。
状态转换管理一个棘手的地方可能是决定过渡逻辑应该放在哪里。在我们的例子中,我们展示了一个状态类触发了一个转换(从振动模式切换到静音模式)。在其他设计中,Phone
上下文可能决定转换(例如,如果用户切换了一个模式,或者某些条件被达成)。这里有一定的灵活性,但这也意味着你应该精心设计以避免混淆。这种模式并不强制这样做;你根据什么能更清晰地组织代码来选择。如果转换过程变得难以理解,记录下来或简化规则。
如果你预见到了状态爆炸,可以考虑一下是否所有状态都是必须的,或者是否可以将它们合并。有时候看似很多状态实际上可以通过数据而不是完整的类来处理,或者分解成更细的状态。比如说,如果电话有10个不同的音量级别,你不需要为此创建10个状态类——音量可以作为“正常”响铃状态中的一个数据参数。只将状态类保留给具有质的区别行为的状态,而不是简单的数值变化。
尽管有这些考虑,状态模式仍然是一种久经考验的工具。它让代码保持灵活且易于扩展。很多框架和库内部也采用了状态模式或类似的设计。例如,UI组件经常有启用、禁用或悬停等状态,这些状态往往是通过内部的状态对象或模式实现的。
最后,结论状态模式通过将特定状态的行为委托给专门的类,帮助你的对象变得更加灵活和易于维护的。我们的手机的例子说明了设备如何通过简单地切换内部状态来改变其行为(例如响铃、振动、静音),而不是通过复杂的条件判断。
如果你在任何具有模式、阶段或影响其行为的条件或状态的系统上工作,状态模式很值得放入你的工具箱。它可能一开始看起来需要额外的工作,但随着你的应用程序的发展,你将会更欣赏它带来的职责分离和可扩展性。
这段对状态模式的解释是否让你有所共鸣?如果你觉得这个类比和例子对你有帮助,不妨给这篇文章点个赞(👏),并将它分享给你的朋友或同事,他们可能会喜欢。你可以留下你的想法,或者分享一下你在项目中是如何使用(或计划使用)状态模式的?让我们继续交流,一起学习吧!
[MCP]——模型上下文协议
[LLM]——大型语言模型
[RAG]——检索增强生成
[SSE]——服务器发送事件
共同學習,寫下你的評論
評論加載中...
作者其他優質文章