状态(state)模式在实现上类似于策略模式,但是它们的目标非常的不同。状态模式呈现的是一个状态转换系统:系统中的对象处在一个特定的状态当中,经过某些操作之后可以抵达另外的状态。
为了实现这一目标,需要一个状态管理器manager,或者一个上下文context来提供交换状态的接口。在实现上,这个类需要包含一个指向状态的指针,每个状态需要知道下一个状态是什么,并且在触发之后达到哪种状态。
状态设计模式主要包含状态管理器以及多种状态两中设计类。管理器维护当前状态,推动操作然后抵达目标状态。而状态类相对于调用者来说是个黑箱,需要内部进行自管理。
一个状态模式的例子
为了展示状态模式设计,我们构建一个XML的分析工具。上下文管理器就是分析工具本身。参数是文本输入,寻找指定的值然后进入不同的状态进行操作,最终目标是创建每个标签(Tag)节点的对象树。为了能够完成任务,我们需要对XML的Tag进行分析。为了简化,我们不处理Tag的属性,以及Tag嵌套在Tag内容中的情形,下面是一个用于分析的XML文件:
<book><author>Kelvin Gao</author><publisher>Packet Publishing</publisher><title>Python3 Programming</title><content><chapter><number>1</number><title>Object Oriented Design</title></chapter><chapter><number>2</number><title>Objects In Python</title></chapter></content></book>
我们先来看一下程序的输出是什么,目标是输出一个节点对象树,节点到底什么样呢?我们需要Tag的名字,一个指向父节点的指针,按顺序排列的子节点列表,注意某些节点有文本内容,而有些没有。下面是设计的节点类:
class Node:def __init__(self, tag_name, parent=None):self.parent = parentself.tag_name = tag_nameself.children = []self.text=""def __str__(self):if self.text:return self.tag_name + ": " + self.textelse:return self.tag_name
现在,我们看一下前面的文本,我们需要定义可以进入什么样的状态。首先,需要一个没有处理过任何节点的状态,我们还需要一个处理打开的和关闭的tags状态,另外需要一个状态用处理具有文本内容的tag。
问题是我们如何知道下一个节点是打开的tag,关闭的tag或者是一个文本节点?这需要在每个状态中增加一点逻辑来表示,但更合理的是创建一个新的状态,仅仅负责指出下一个状态是什么,我们称这种状态转换类叫做ChildNode,最后我们需要以下几种状态:
FirstTagChildNodeOpenTagCloseTagText
FirstTag状态将会转换到ChildNode,然后决定去往哪一个状态,当其他的状态都完成时,返回ChildNode,状态操作尽可能处理“字符中有什么”然后告诉分析器(上下文管理器)处理“剩余部分”。下面给出分析器类的定义:
class Parser:def __init__(self, parse_string):self.parse_string = parse_stringself.root = Noneself.current_node = Noneself.state = FirstTag()def process(self, remaining_string):remaining = self.state.process(remaining_string, self)if remaining:self.process(remaining)def start(self):self.process(self.parse_string)
root
属性代表XML结构的最顶层节点,current_node
变量代表子节点。分析器种最重要的部分是process
方法,将其传递给当前的状态,处理”剩余的字符串“。分析器自己也会传递给状态的处理方法,这样状态便可以对其进行操作。状态会返回没有处理的字符串,随后分析器递归的调用process
方法对剩余的字符串进行操作,最终构建起整个树结构。
下面是FirstTag状态类的实现:
class FirstTag:def process(self, remaining_string, parser):i_start_tag = remaining_string.find('<')i_end_tag = remaining_string.find('>')tag_name = remaining_string[i_start_tag+1:i_end_tag]root = Node(tag_name)parser.root = parser.current_node = rootparser.state = ChildNode()return remaining_string[i_end_tag+1:]
注意这里没有进行验证输入的是否为有效的文件,正确的做法应该严格的进行有效性验证,能够在错误中恢复,或者给出描述性错误信息。
这里的process
方法提取Tag的名字,赋给分析器的根节点,同时赋给current_node
子节点,之后改变分析器的状态进入ChildNode,并返回剩余字符作进一步处理。
下面是中控状态ChildNode的实现:
class ChildNode:def process(self, remaining_string, parser):stripped = remaining_string.strip()if stripped.startswith("</"):parser.state = CloseTag()elif stripped.startswith("<"):parser.state = OpenTag()else:parser.state = TextNode()return stripped
strip()
方法用以清除whitespace,之后分析器查看是元素是open标签,close标签,或者是一个字符串文本。根据不同的可能性,配置分析器到不同的状态,对字符串进行进一步分析。
OpenTag状态类似于FirstTag状态,除了添加一个新创建的node赋给current_node之外
class OpenTag:def process(self, remaining_string, parser):i_start_tag = remaining_string.find('<')i_end_tag = remaining_string.find('>')tag_name = remaining_string[i_start_tag+1:i_end_tag]node = Node(tag_name, parser.current_node)parser.current_node.children.append(node)parser.current_node = nodeparser.state = ChildNode()return remaining_string[i_end_tag+1:]
CloseTag状态则相反,他设置分析器的current_node回到父节点
class CloseTag:def process(self, remaining_string, parser):i_start_tag = remaining_string.find('<')i_end_tag = remaining_string.find('>')assert remaining_string[i_start_tag+1] == "/"tag_name = remaining_string[i_start_tag+2:i_end_tag]assert tag_name == parser.current_node.tag_nameparser.current_node = parser.current_node.parentparser.state = ChildNode()return remaining_string[i_end_tag+1:].strip()
最后,TextNode状态仅仅是提取文本内容,并且赋给当前节点:
class TextNode:def process(self, remaining_string, parser):i_start_tag = remaining_string.find('<')text = remaining_string[:i_start_tag]parser.current_node.text = textparser.state = ChildNode()return remaining_string[i_start_tag:]
下面我们创建一个主脚本,从命令行打开文件,分析内容,并且输出节点:
if __name__ == "__main__":import syswith open(sys.argv[1]) as file:contents = file.read()p = Parser(contents)p.start()nodes = [p.root]while nodes:node = nodes.pop(0)print(node)nodes = node.children + nodes
打印所有Tag及子Tag相关信息,输出的内容如下:
bookauthor: Kelvin Gaopublisher: Packet Publishingtitle: Python3 Programmingcontentchapternumber: 1title: Object Oriented Designchapternumber: 2title: Objects In Python
状态模式对比策略模式
实际上,我们可以将状态写成first-class函数,而不必包装到类中;在结构上看,状态模式和策略模式两类设计具有类似的结构,但是他们解决完全不同的问题。策略模式在运行时选择一个算法,通常情况下只有一个算法被执行。然而状态模式允许处理过程在不同的状态之间进行切换,这里的主要区别是策略模式不关心彼此的策略,而在状态模式下,上下文管理器需要知道能够切换进的状态是什么。