前言
在近期的公司项目中,需求项目开发一个类沙盒建造功能。我有幸负责项目中的关键部分,这一经历充满了挑战与收获。鉴于这个部分内容的丰富性和技术性,我决定将涉及相关设计以及本人的一部分思绪分成四期内容。本文是系列文章的第三期,将重点讨论建造中的状态模式设计。
运用状态模式主要目的还是为了解耦, 这种大型的系统设计如果还让if-else影响自己的代码判断能力, 那就太得不偿失了. 本篇将从每种状态设计开始, 再到整体的系统设计, 最后还会附上状态模式中一些笔者踩到的坑, 该篇同样为主要讲解篇章之一.
状态模式构成
状态模式(State Pattern)也是行为设计模式的一种, 他的出现通常都跟有限状态机强相关. 通常的需求中状态出现的其实也是非常频繁, 很多地方也能用到该模式. 用该模式会带来一下几个好处:
- 单一职责原则: 将与特定状态相关的代码放在单独的类中。
- 开闭原则: 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
但一定不要滥用, 比如说两个或者三个比较简单的状态下, 一个if-else就可以实现的效果就不要用该模式了, 毕竟是行为设计模式, 它同在前一章讨论的命令模式也会有一样的缺点, 解耦是肯定解耦了, 但是冗杂代码也一定会出现的.
该模式主要构成为三个部分: 上下文(Context)
、状态(State)
、具体状态(Concrete States)
他的命名就没有命令模式那么抽象, 基本能做到见文生意, 下面从概念到具体一一介绍
状态基类
同大部分GOF设计模式, State最基本的一些方法为Init
、Enter
、Exit
、Type
由于笔者这次需求的具体State的种类还是较多的, 达到了6种, 所以本次基类设计中也是加入了CanSwitch
方法
同命令模式所诉内容, 这个内容可以有多种表现形式, 基类、接口、协议.
具体状态
这个部分也是对业务的抽象处理, 部分状态操作功能类似, 但内容完全不一样的方法没有放到基类里去实现, 两个方面, 一个是添加后会大大增加基类的复杂度, 另一个是现在用枚举、范型、类型转换能够很轻易的拿到方法内容现代语言的功能已足够强大.
笔者这里简单分享一下6个状态互相转换关系
当前状态 | 可切换状态 |
---|---|
1 | 2、3、4、5、6 |
2 | 3、4 |
3 | 1 |
4 | 1 |
5 | 1、3 |
6 | 1 |
虽然在这里表格呈现形式不太好, 但是用有限状态图以六边形画出来的画效果其实相当好看. 这里观察可见如3、4、6状态相对独立, 那么他们的Enter
与Exit
可以完全控制对应逻辑管理, 在编写他们详细的业务需求时会相当清晰.
上下文
最后来聊聊上下文内容, 上下文这个名字虽然它表明了意图, 其实实际上不那么清晰, 通常写代码中更喜欢称呼他们叫做Manager
或System
, 他们通常负责所有状态的初始化, 切换, 公共方法的调用入口. 并且实际上状态模式的大部分坑也是这个在上下文这个地方.
初始化
状态的生命周期基本上都是跟上下文同步的, 上下文创建时, 所有的具体状态应该会同时初始化, 并且在初始化这里笔者也是选择了将上下文传入具体命令中
为什么初始化传入上下文
状态切换
这部分比较简单, 有当前命令执行Exit
, currentState更新, 并执行Enter
方法.
笔者这个需求能还多了一个对当前命令能否转为目标命令的检查.
除此之外热烈的期望将DebugLog打到这个地方, 受益匪浅
坑啊坑
这部分内容其实应该算是较为脱离了一些状态模式最基本的范畴, 但是在设计大型项目中又不可忽视的问题.
UI交互设计
不同于命令模式, 状态模式大概率会和UI相关内容打交道. 所以如何优雅的设计与UI组件的交互内容是一个值得思考的问题.
在比较简单的交互下可以将当前的state传入UI组件并将其直接调用, 但是复杂的需要另寻他法.
笔者这里的选择是将所有可能UI的交互事件都在上下文中声明, 声明的方法就是调用当前的state的这个方法, 那么这样的情况下UI就只剩下与上下文交互的内容, 这样虽然相对简易但是依然会存在两个问题:
- 上下文变得fat, 一大堆差不多的变量声明, 陷入了类似
MVC
中的后期controller的困境中, 如果该语言支持分文件夹声明那么将会得到缓解, 但也避免不了变得fat
的事实 - 逻辑条理不够清晰, 有一些接近黑盒的感觉, 有些状态实现有些状态不实现, 一旦出现了问题较难debug.
笔者也曾想过将其写入基类中,但最后也是放弃了(在具体状态那里简单说了下想法). 所以如果读者在这方面如果有更好的想法可以留言或者联系我沟通.
状态切换与监听周期
在调用链路没有那么清晰的大型需求中, 处理事件是非常正常的事情. 说到事件就需要监听与移除, 这个调用时机呼之欲出 -- Enter
和 Exit
. 为了更好的解耦, 笔者也是毫不犹豫的做了这个事情, 但是在项目里却有问题...
有可能在执行事件的流程中发生了添加或移除监听的操作, 那么监听事件队列for循环迭代器就为nil了就执行不下去了....
显然这个是项目中事件队列写的有些问题, 但是没办法项目缺陷, 业务也需要买单.
所以我就讲所有有关监听的内容放到了上下文之中.
让本就雪上加霜的第一条再添沉重一笔.
尾语
同命令模式, 这篇文章主要讲编程思想, 在此过程中也聊一些大型需求中的设计经验, 本篇也应该是该系列中内容最丰富的一集了, 下一篇是本系列的最后一章, 聊一聊寻找系统.