软件构造第五章 第三节 面向可维护性的构造技术
一、基于状态的构造技术
- 基于状态编程
- 使用有限状态机来定义程序的行为、使用状态来控制程序的执行
- 根据当前状态,决定下一步要执行什么操作、执行操作之后要转移到什么新的状态
1.1 基于自动机的编程
- 定义:
基于自动机的编程是一种编程范例,其中该程序或程序的一部分被认为是有限状态机(FSM)或任何其他形式自动机的模型。- 将程序视为有限自动机。
- 每个自动机一次只能执行一个“步骤”,并且程序的执行分为多个步骤。
- 这些步骤通过更改代表“状态”的变量的值相互通信。
- 程序的控制流程由该变量的值确定。
- 应用程序设计方法应类似于控制系统(自动机系统)的设计。
- 核心思想:
将程序视为是一个有限状态自动机,侧重于对“状态”及“状态转换”的抽象和编程 - 执行步骤:
程序执行的时间段明显分为自动机的各个步骤。 程序的执行被分解为一组自动执行的步骤。- 每个步骤实际上是一个代码段的执行(所有步骤相同),该代码段具有单个入口点。 这样的部分可以是函数或其他例程,也可以只是循环体。
- 步骤之间的任何通信都只能通过显式记录的名为状态的变量集进行。 各步骤之间的通讯通过“状态变量”进行
- 在任何两个步骤之间,程序都不能具有其状态的隐式组件,例如局部(堆栈)变量的值,返回地址,当前指令指针等。
- 在进入自动机步骤的任何两个时刻获取的整个程序的状态只能在被视为自动机状态的变量值上有所不同。
- 使用方法:
- 程序执行就可看作是各自动步骤的不断循环
- 使用枚举类型enum定义状态
- 使用二维数组定义状态转换表
1.2 状态模式(State Pattern)
- 意图:
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。 - 主要解决:
对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。 - 何时使用:
代码中包含大量与对象状态有关的条件语句。 - 如何解决
将各种具体的状态类抽象出来。 - 关键代码:
通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。 - 应用实例:
- 打篮球的时候运动员可以有正常状态、不正常状态和超常状态。
- 曾侯乙编钟中,’钟是抽象接口’,’钟A’等是具体状态,’曾侯乙编钟’是具体环境(Context)。
- 优点:
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
- 缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对”开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
- 使用场景:
- 行为随状态改变而改变的场景。
- 条件、分支语句的代替者。
- 注意事项:
在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
1.3 备忘录模式(Memento Pattern)
- 意图:
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。 - 主要解决:
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。 - 何时使用:
很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有”后悔药”可吃。 - 如何解决:
通过一个备忘录类专门存储对象状态。 - 关键代码:
客户不与备忘录类耦合,与备忘录管理类耦合。 - 应用实例:
- 游戏的存档
- Windows 里的 ctri + z
- 浏览器中的后退
- 数据库的事务管理
- 优点:
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
- 实现了信息的封装,使得用户不需要关心状态的保存细节
- 缺点:
消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。 - 使用场景:
- 需要保存/恢复数据的相关状态场景
- 提供一个可回滚的操作
- 注意事项:
- 为了符合迪米特原则,还要增加一个管理备忘录的类
- 为了节约内存,可使用原型模式+备忘录模式



二、语法驱动的构造
- 基于I/O的输入流
应用需要从外部读取文本数据, 在应用中做进一步处理。 具体来说,读取的一个字节或字符序列可能是:- 输入文件有特定格式,程序需读取文件并从中抽取正确的内容。
- 从网络上传输过来的消息,遵循特定的协议。
- 用户在命令行输入的指令,遵顼特定的格式。
- 内存中存储的字符串,也有格式需要。
- 语法
- 使用grammar判断字符串是否合法,并解析成程序里使用的数据结构
- 正则表达式
- 通常是递归的数据结构
2.1 语法的组成成分
- 终止节点:语法中的文字字符串
- 用语法定义一个“字符串”
- terminals 终止节点、叶节点
- nonterminal 非终止节点(遵循特定规则,利用操作符、终止节点和其他非终止节点,构造新的字符串)
- 语法中的产生式节点和非终止节点
- 语法是由一系列产生式节点组成,每一个产生式节点定义了一个非终止节点
- 非终止节点就像代表一组字符串的变量,而产生式节点是根据其他变量(非终结符),运算符和常量(终结符)来定义该变量。 遵循特定规则,利用操作符,终止中断和其他非终止中断,构造新的字符串
- 非终结符是树的内部节点,代表一个字符串。
- 语法中的产生式节点具有以下形式:
- nonterminal :: =产生式节点、非终止节点和运算符的表达式
- 语法的非终结符之一被指定为词根
- 语法识别的字符串集是与根非终结符匹配的字符串
- 该非终结符通常称为root或start
2.2 语法中的操作符
- 三个基本语法的操作符
- 连接,不是通过一个符号,而是一个空间:
x ::= y z //x等价于y后跟一个z - 重复,以\表示:
x ::= y\ // x等价于0个或更多个y - 联合,也称为交替,用|表示 :
x ::= y | z //x等价于一个y或者一个z
- 连接,不是通过一个符号,而是一个空间:
- 三个基本操作符的组合:
- 可选(0或1次出现),由?表示:
x ::= y? //x等价于一个y或者一个空串 - 出现1次或多次:以+表示:
x ::= y+ //x等价于一个或者更多个y, 等价于 x ::= y y* - 字符类[…],表示长度的字符类,包含方括号中列出的任何字符的1个字符串:
x ::= [abc] //等价于 x ::= ‘a’ | ‘b’ | ‘c’ - 否定的字符类[^…],表示长度,包含未在括号中列出的任何字符的1个字符串:
x ::= [^abc] //等价于 x ::= ‘d’ | ‘e’ | ‘f’ | … (all other characters in Unicode)
- 可选(0或1次出现),由?表示:
- 例子:
x ::= (y z | a b)* //an x is zero or more y z or a b pairs
m ::= a (b|c) d //an m is a, followed by either b or c, followed by d
2.3 语法分析树
- 语法分析树:
将语法与字符串匹配可以生成一棵分析树,该树显示字符串的各个部分与语法的各个部分如何对应。- 解析树的叶子用终端标记,表示已解析的字符串部分。
- 没有孩子,无法继续扩大。
- 如果将叶子串联在一起,则返回原始字符串。
- 解析URL示例:
url ::= protocol ‘\: //‘ hostname (‘:’port)? ‘/‘
protocol ::= (‘f’ | ‘ht’) ‘tp’ ‘s’?
hostname ::= word ‘.’ hostname |
word ‘.’ word
port ::= [0-9]+
word ::= [a-z]+
2.4 正则语法
- 正则语法:
简化之后可以表达为一个产生式而不包含任何非终止节点。 - 正则语法示例:
图2-1 正则语法
2.5 正则表达式
- 正则表达式:
可以用一种更紧凑的形式来编写简化的终端和运算符表达式。 - 正则表达式消除了终端周围的引号以及终端和操作符之间的空格,因此它仅由终端字符,分组括号和操作符组成。 去除引号和空格,从而表达更简洁(更难懂),正则表达式也简称为regex。
- 正则表达式比原始语法的可读性差得多,因为它缺少记录每个子表达式含义的非终结符名称。
- 但是正则表达式可以快速实现,并且许多编程语言中都有支持正则表达式的库。
- 正则表达式中的特殊字符

- 上下文无关文法
与课程形式语言与自动机关联
2.6 JAVA中的正则表达式
- 适用场合:我们用正则表达式匹配字符串(例如 String.split , String.matches , java.util.regex.Pattern)
- 用一个空格代替所有的多个空格
- 匹配一个URL:
- 提取HTML标签的一部分

- JAVA中的正则表达式语法
JAVA中的正则表达式语法