5.3 面向可维护性的构造技术
一. 基于状态的编程
基于状态的编程使用有限状态机来定义程序的行为、使用状态来控制程序的执行。
根据当前状态,决定下一步要执行什么操作、执行操作之后要转移到什么新的状态。
1. 基于自动机的编程
核心思想:将程序看作是一个有限状态自动机,侧重于对“状态”及“状态转换”的抽象和编程。
程序的执行被分解为一组自动执行的步骤,各步骤之间的通讯通过“状态变量”进行。
程序执行就可看作是各自动步骤的不断循环,使用枚举类型 enum
定义状态,使用二维数组定义状态转换表。这种实现方式一般会使用 is/else
结构。
State transition[][] = {
{ State.Initial, State.Final, State.Error },
{ State.Final, State.Initial, State.Error }
};
然而这种实现方式使得对程序进行修改的工程量变大。
2. 状态 State
模式
最好不要使用 if/else
结构在 ADT
内部实现状态转换(考虑将来的扩展和修改)。可以使用使用 delegation
,将状态转换的行为委派到独立的 state
对象去完成。
下图是状态转换的 UML
图:
如下程序是一种状态转换模式的应用,其状态转换图如下:
class Context {
State state;//保存对象的状态
//设置初始状态
public Context(State s) {state = s;}//设置 delegation 关系
//接收外部输入,开启状态转换
//每次状态转换之后,形成新状态,替换原状态
public void move(char c) { state = state.move(c); }//将改变状态的“动作” delegate 到 state 对象
//判断是否达到合法的最终状态
public boolean accept() { return state.accept(); }//Delegate 到当前状态的 accept() 方法,判断是否达到最终状态
public State getState() { return this.state; }
}
//状态接口
public interface State {
State move(char c);
boolean accept();
}
class State1 implements State {
static State1 instance = new State1(); //singleton 模式(单例模式)
private State1() {}
public State move (char c) {
switch (c) {
case 'a': return State2.instance;//返回新状态的 singleton 实例
case 'b': return State1.instance;
default: throw new IllegalArgumentException();
}
}
public boolean accept() {
return false;
} //该状态非可接受状态
}
class State2 implements State {
static State2 instance = new State2();
private State2() {}
public State move(char c) {
switch (c) {
case 'a': return State1.instance;
case 'b': return State1.instance;
default: throw new IllegalArgumentException();
}
}
public boolean accept() {return true;}
}
public static void main(String[] args) {
Context context = new Context(State1.instance);//给 ADT 初始状态
for (int i = 0; i < args.length; i++) {
context.move(args[i]);
if(context.accept())
break;
//根据输入的一组字符,每次用一个字符使状态发生变迁,直到达到最终状态,程序结束。
}
}
上述程序使用了单例模式 ( singleton
模式)。State1
类中,无法通过私有的( private
)构造函数构造它的对象。单例模式就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。
单例模式要求:
- 构造函数私有 (
private
) - 有静态方法(对外提供获取实例的静态方法,如静态工厂方法)、成员变量(持有自己类型的属性),最好不要把成员变量变为
public
类型
State
一般需要使用单例模式。
3. Memento Pattern
备忘录模式
记住对象的历史状态,以便于“回滚”。
备忘录模式需要实现:
- 需要“备忘”的类
- 添加
originator
的备忘记录和恢复 - 备忘录,记录
originator
对象的历史状态
//Memento:非常简单的类,只记录一个历史状态
class Memento {
private State state;
public Memento(State state) {
this.state = state;
}
public State getState() {
return state;
}
}
class Originator {
private State state;
// ADT 原本的状态转换功能,可能更复杂(例如 State 模式)
public void setState(State state) {
System.out.println("Originator: Setting state to " + state.toString());
this.state = state;
}
//保存历史状态, delegate 到 memento 去实现
public Memento save() {
System.out.println("Originator: Saving to Memento.");
return new Memento(state);
}
//利用传入的 Memento 对象来恢复历史状态
public void restore(Memento m) {
state = m.getState();
System.out.println("Originator: State after restoring from Memento: " + state);
}
}
class Caretaker {
private List<Memento> mementos = new ArrayList<>();//保留一系列历史状态
//添加一个新的历史状态
public void addMemento(Memento m) {
mementos.add(m);
}
//取出需要回滚的状态
public Memento getMemento() {
return mementos.get(?);//如果要恢复最近备份的状态,这里应该是?
}
}
public class Demonstration {
public static void main(String[] args) {
Caretaker caretaker = new Caretaker();
Originator originator = new Originator();
originator.setState("State1");
originator.setState("State2");
caretaker.addMemento(originator.save());
originator.setState("State3");
caretaker.addMemento(originator.save());
originator.setState("State4");
originator.restore(caretaker.getMemento());
//如何 rollback 两步、三步、...
}
}
/*
Originator: Setting state to State1
Originator: Setting state to State2
Originator: Saving to Memento.
Originator: Setting state to State3
Originator: Saving to Memento.
Originator: Setting state to State4
Originator: State after restoring from
Memento: State3
*/
4. 设计模式共性样式 2
State
与 Memento
模式均为两棵继承树,两层次 delegation
的设计样式。
对于 State
模式,左为 context
:
Memento
模式:
二. 语法驱动的构造
有一类应用,从外部读取文本数据,在应用中做进一步处理。
文本的出现情况:
- 输入文件有特定格式,程序需读取文件并从中抽取正确的内容
- 从网络上传输过来的消息,遵循特定的协议
- 用户在命令行输入的指令,遵循特定的格式
- 内存中存储的字符串,也有格式需要
匹配方法:
- 使用
grammar
判断字符串是否合法,并解析成程序里使用的数据结构 - 通常是递归的数据结构
程序能接受的语法就是正则表达式。Java
根据语法,开发一个它的解析器,用于后续的解析。
1. 语法
用语法定义一类字符串。
语法包含两类型的节点。将语法表示为树形的结构:
- 终止节点是语法解析树的叶子结点,无法再向下扩展
- 产生式结点是非终止节点,可以被扩展。最初的非终结符叫根
2. 语法操作符
- 连接:二元操作符
x ::= y z
- 重复:一元操作符
x ::= y*
- 选择:二元操作符
x ::= y | z
二元操作符优先级低于一元操作符。
更简便的表示方法:
?
:代表0
或1
+
:代表1
或多个[...]
:选择。从方括号中选择适合的字符。[^...]
:不从方括号包含的字符选择。
3. 递归
url ::= 'http://' hostname (':' port)? '/'
hostname ::= word '.' hostname | word '.' word
port ::= [0-9]+
word ::= [a-z]+
也可以不递归实现:
url ::= 'http://' hostname (':' port)? '/'
hostname ::= (word '.')+ word
port ::= [0-9]+
word ::= [a-z]+
但语法树不一样。
4. Parse
语法树
对字符串 http://mit.edu/
,
url ::= 'http://mit.edu/'
其语法树为:
url ::= 'http://' hostname '/'
hostname ::= word '.' word
word ::= [a-z]+
其语法树为:
对字符串 http://didit.csail.mit.edu:4949/
,语法:
url ::= 'http://' hostname (':' port)? '/'
hostname ::= word '.' hostname | word '.' word
port ::= [0-9]+
word ::= [a-z]+
其语法树为:
5. Markdown
和 HTML
利用特定的语法对文本进行解析。
6. 正则语法与正则表达式
正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点。
正则表达式:去除引号和空格,从而表达更简洁但更难懂。
特殊符号:
.
:单个字母\d
:任意数字,同[0-9]
\s
:任何空格字符,包括空格,制表符,换行符\w
:任何单词字符,包括下划线,同[a-zA-Z_0-9]
\.
\(
\)
\*
\+
...
因此单单使用 <([{\^-=$!|]})?*+.>
时,需要添加转义字符 \
。
7*. Parsers
parser
:输入一段文本,与特定的语法规则建立匹配,输出结果。
parser
:将文本转化为 parse tree
利用产生的 parse tree
,进行下一步的处理
Parser generator
是一个工具,根据语法规则生成一个 parser
程序。
8. Java
中的正则表达式
常用的类 String.split
, String.matches
, java.util.regex.Pattern
。
对于 java.util.regex
:
Pattern
:是对regex
正则表达式进行编译之后得到的结果Matcher
:利用Pattern
对输入字符串进行解析匹配
public boolean matches(String regex);//查看是否与正则表达式能否匹配上
Pattern.matches(regex, str);
public String[] split(String regex, int limit) limit);//切分的次数
Pattern.compile(regex).split(str, n);
public String[] split(String regex);
public String replace(CharSequence target,CharSequence replacement);