状态模式和策略模式在结构上有些相似,但是其意图和目的却非常不同。状态模式的目的是实现状态转换系统:对象显而易见的处于一个特定状态,同时某些活动可能驱动它转变到另一个不同的转态。
为了实现这个目的,我们需要一个管理者类或上下文类为状态转换提供一个接口。在内部,这个类包含一个指向当前状态的指针:每个状态都知道可以转换到那些状态并根据如何调用的操作转换到那个状态。
所以我们有两种类型的类:一个上下文类和多个状态类。上下文维护当前状态,并且转发操作到状态类。状态类对其他调用上下文的对象而言通常是隐藏的。它的行为就像是一个黑盒子,在内部执行状态管理。下图是状态模式在UML中的表现形式:状态模式实例
以构建一个XML解析工具为例简单讲解状态实例。这里上下文类就是解析器。其将一个字符串作为输入,并将工具处于初始解析状态。各种解析状态都将通过字符来寻找一个特定的值,但这个值被找到时,它就会变成另一种状态。我们的目的就是为每个标签和它的内容创建一个节点对象树。为了便于管理,我们只解析XML的一个子集;标签和标签名称,我们不处理标签的属性。他将解析标签的文本内容,但是不会视图解析文本中存在标签混合的内容。下面是一个能够被解析的‘简化的XML’文件实例:
<book>
<author>Dusty Phillips</author>
<publisher>Packt Publishing</publisher>
<title>Python3 Object Oriented Programming</title>
<content>
<chapter>
<number>1</number>
<title>Object Oriented Design</title>
</chapter>
<chapter>
<number>2</number>
<title>Object In Pyhton</title>
</chapter>
<content>
</book>
在关注状态和解析器之前,先考虑一下这个程序的输出。我们想要得到一个Node对象树,但Node是什么样子呢?显然,这需要了解正在解析的标签的名称,既然他是一棵树,他就应该有一个指向父节点的指针和一个有序排列的子节点列表。某些子节点可能会包含文本值,但不是全都有。所以从上面的‘XML文件’我们可以知道我们的解析器状态应该有下面几种:
- 起始标签(FirstTag)
- 子节点(ChildNode)
- 开始标签(OpenTag)
- 结束标签(CloseTag)
- 文本(Text)
先看看Node类:
class Node(object):
def __init__(self, tag_name, parent=None):
self.parent = parent
self.tag_name = tag_name
self.children = []
self.text = ""
def __str__(self):
if self.text:
return self.tag_name+": "+self.text
else:
return self.tag_name
这个类在初始化时候就已经为属性设置了默认值。当我们完成后__str__
函数会帮我们实现树结构的可视化。
再回‘XML文档’,我们前面已经提到了我们的解析器可以处于5个状态。即从尚未被处理的状态开始,然后我们还需要知道我们知道处于标签开始状态和结束状态。此外,如果标签中含有文档,我们必须把它当成一种单独的状态(Text)来处理。由此可以得到上面的五种状态。
其实标签的状态切换至子节点,由子节点负责决定切换到接下来的3个状态的哪一个;当这些状态结束时,他们将切换到子节点,下面的状态关系转换图显示了可用的状态变化:
状态类负责取出‘剩下的字符串’,尽可能多的处理他们知道如何处理的,然后告诉解析器来处理剩下的部分。先来构建一个
Parser
类:
class Parser(object):
def __init__(self, parse_string):
self.parse_string = parse_string
self.root = None
self.current_node = None
self.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)
这个类的初始化函数已经设置了个状态将会访问的几个变量,parse_string
正是我们试图解析的文本。在XML结构中,root节点是顶节点。current_node是我们正在添加子节点的节点。
process
函数是这个解析器的重要特性,他可以接收剩余字符串并将其传递到当前状态。解析器本身(self参数)也将被传递到该状态的处理函数以便状态类可以操作他,状态类在完成处理后返回剩余的未解析字符串。接着解析器会递归的调用process
方法处理剩余的字符串来创建树的其他部分。
接着再看看FirstTag
状态类:
class FirstTag(object):
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 = root
parser.state = ChildNode()
return remaining_string[i_end_tag:]
这个状态类找到第一个标签中打开和关闭尖括号的索引(i_代表索引)。你可能认为这种状态不是必要的,因为XML要求开始标签之前不能有文本。然而,这样可能会需要消耗空格,这就是为什么我们需要寻找开始尖括号,而不是假设它是文档中的第一个字符。需要注意的是,这段代码假设输入文件是有效的。
该方法提取标签的名称并将其分配到解析器的根节点。该方法还会指定他作为current-node,因为我们以后会对其添加子节点。
然后接下来的部分很重要,这个方法将解析器对象上的当前状态变成为ChildNode状态。然后返回字符串的其余部分(在开始标签之后)并允许他被处理。
这样,看似很复杂的ChildNode状态变得只需要一个简单的条件:
class ChildNode(object):
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()函数来删除字符串中的空格。然后解析器会决定下一个是开始、关闭还是文本标签。取决于发生什么,他会将解析器设置为一个特定的状态,然后让他解析字符串的其余部分。
除了添加新创建的节点到向前的current-node对象的children中并且将它设置成新的current-node,OpenTag状态与FirstTag状态类似。再继续处理之前,他将状态返回到ChildNode状态:
class OpenTag(object):
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 = node
parser.state = ChildNode()
return remaining_string[i_end_tag+1:]
CloseTag完成的事情基本和OpenTag相反。他将解析器中的current-node设置回父节点,使得外部表前添加更多的子节点:
class CloseTag(object):
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
assert remaining_string[i_start_tag+2:i_end_tag]
tag_name = remaining_string[i_start_tag+2:i_end_tag]
assert tag_name==parser.current_node.tag_name
parser.current_node=parser.current_node.parent
parser.state = ChildNode()
return remaining_string[i_end_tag+1:].strip()
这两个assert
语句帮助我们确保解析字符串是一致的。方法末尾的if语句仅仅确保完成时结束处理。如果这个节点的父节点为None,意味着我们正处于根节点中。
最后,TextNode转态非常简单的提取下一个结束标签之前的文本并将其设置为当前节点的一个属性值:
class TextNode(object):
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
text = remaining_string[:i_start_tag]
parser.current_node.text = text
parser.state = ChildNode()
return remaining_string[i_start_tag:]
现在我们只需要对我们创建的解析器对象设置初始状态。初始状态是FirstTag对象,所以我们需要将下面的语句添加到__init__
方法中:
self.state = FirstTag()
下面对这个类进行测试,从命令行中打开文件然后对其进行解析并打印出节点:
if __name__ == '__main__':
import sys
with 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
# 输出:
book
author: Dusty Phillips
publisher: Packt Publishing
title: Python3 Object Oriented Programming
content
chapter
number: 1
title: Object Oriented Design
chapter
number: 2
title: Objects In Python
状态模式和策略模式对比
状态模式与策略模式看起来非常像,UML也基本相同。实现方式也基本相同,我们甚至可以将状态写成一级函数而不是像策略中建议的那样将它封装进对象中。
虽然两种模式具有相近的结构,目的却大相庭径。策略模式用于在运行时选择一种算法:一般来说,只有其中一个算法将会被选择用作特定用途。另一方面,状态模式被设计成允许在不同状态之间进行动态切换,正如某些过程的发展那样。在代码中,最主要的区别在于,策略模式通常对其他策略对象没有意识。而在状态模式中,状态或上下文需要知道他们将会切换到什么样的状态。
参考:
《Python3 面向对象编程》
网友评论