本系列共分为三篇文章,其中包含的设计模式如下表:
名称 | 设计模式 |
---|---|
图解设计模式【1】 | Iterator、Adapter、Template Method、Factory Method、Singleton、Prototype、 Builder、Abstract Factory、 Bridge、 Strategy |
图解设计模式【2】 | Composite、 Decorator、 Visitor、 Chain of Responsibility、 Facade、 Mediator、 Observer、 Memento |
图解设计模式【3】 | State、 Flyweight、 Proxy、 Command、 Interpreter、 |
State模式
State模式中,用类来表示状态。以类来表示状态就可以通过切换类来方便地改变对象的状态。当需要增加新的状态时,如何修改代码这个问题也会很明确。
示例
设计一个简单的报警系统,每秒会改变一次状态。
金库报警系统 |
---|
- 有一个金库,金库与报警中心相连,金库里有警铃和正常通话用的电话。金库里有时钟,监视着现在的时间。 |
- 白天的时间范围时9:00-16:59,晚上的时间范围是17:00-23:59和0:00-8:59。 |
- 金库只能在白天使用。白天使用金库的话,会在报警中心留下记录;晚上使用金库的话,会向报警中心发送紧急事态通知。 |
- 任何时候都可以使用警铃。使用警铃的话,会向警报中心发送紧急事态通知。 |
- 任何时候都可以使用警铃。使用警铃的话,会向警报中心发送紧急事态通知。 |
- 任何时候都可以使用电话(晚上只有留言电话)。白天使用电话的话,会呼叫报警中心。晚上使用电话的话,会呼叫报警中心的留言电话。 |
如果不使用State模式,我们可以使用如下的伪码逻辑
警报系统的类{使用金库时被调用的方法(){if (白天) {向警报中心报告使用记录} else if (晚上){向警报中心报告紧急事态}警铃响起时被调用的方法(){向警报中心报告紧急事态}正常通话时被调用的方法(){if (白天){呼叫报警中心} else if (晚上){呼叫警报中心的留言电话}}
}
使用了State模式的伪代码
表示白天的状态的类{使用金库时被调用的方法(){向警报中心报告使用记录}警铃响起时被调用的方法(){向警报中心报告紧急事态}正常通话时被调用的方法(){呼叫警报中心}
}
表示晚上的状态的类{使用金库时被调用的方法(){向警报中心报告紧急事态}警铃响起时被调用的方法(){向警报中心报告紧急事态}正常通话时被调用的方法(){呼叫警报中心的留言电话}
}
两者的区别在于,使用State模式,不需要用if语句判断是白天还是晚上。
State接口时表示金库状态的接口。其中包括设置时间、使用金库、按下警铃、正常通话等API
public interface State {public abstract void doClock(Context context, int hour); // 设置时间public abstract void doUse(Context context); // 使用金库public abstract void doAlarm(Context context); // 按下警铃public abstract void doPhone(Context context); // 正常通话
}
DayState类表示白天的状态。该类实现了State接口,因此还实现了State接口中声明的所有方法。
public class DayState implements State {private static DayState singleton = new DayState();private DayState() { // 构造函数的可见性是private}public static State getInstance() { // 获取唯一实例return singleton;}public void doClock(Context context, int hour) { // 设置时间if (hour < 9 || 17 <= hour) {context.changeState(NightState.getInstance());}}public void doUse(Context context) { // 使用金库context.recordLog("使用金库(白天)");}public void doAlarm(Context context) { // 按下警铃context.callSecurityCenter("按下警铃(白天)");}public void doPhone(Context context) { // 正常通话context.callSecurityCenter("正常通话(白天)");}public String toString() { // 显示表示类的文字return "[白天]";}
}
NightState类表示晚上的状态。它与DateState类一样,也是用了Singleton模式。
public class NightState implements State {private static NightState singleton = new NightState();private NightState() { // 构造函数的可见性是private}public static State getInstance() { // 获取唯一实例return singleton;}public void doClock(Context context, int hour) { // 设置时间if (9 <= hour && hour < 17) {context.changeState(DayState.getInstance());}}public void doUse(Context context) { // 使用金库context.callSecurityCenter("紧急:晚上使用金库!");}public void doAlarm(Context context) { // 按下警铃context.callSecurityCenter("按下警铃(晚上)");}public void doPhone(Context context) { // 正常通话context.recordLog("晚上的通话录音");}public String toString() { // 显示表示类的文字return "[晚上]";}
}
Context接口是负责管理状态和联系警报中心的接口。
public interface Context {public abstract void setClock(int hour); // 设置时间public abstract void changeState(State state); // 改变状态public abstract void callSecurityCenter(String msg); // 联系警报中心public abstract void recordLog(String msg); // 在警报中心留下记录
}
SafeFrame类是使用GUI实现警报系统界面的类,它实现了Context接口。
import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.Button;
import java.awt.TextField;
import java.awt.TextArea;
import java.awt.Panel;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;public class SafeFrame extends Frame implements ActionListener, Context {private TextField textClock = new TextField(60); // 显示当前时间private TextArea textScreen = new TextArea(10, 60); // 显示警报中心的记录private Button buttonUse = new Button("使用金库"); // 金库使用按钮private Button buttonAlarm = new Button("按下警铃"); // 按下警铃按钮private Button buttonPhone = new Button("正常通话"); // 正常通话按钮private Button buttonExit = new Button("结束"); // 结束按钮private State state = DayState.getInstance(); // 当前的状态// 构造函数public SafeFrame(String title) {super(title);setBackground(Color.lightGray);setLayout(new BorderLayout());// 配置textClockadd(textClock, BorderLayout.NORTH);textClock.setEditable(false);// 配置textScreenadd(textScreen, BorderLayout.CENTER);textScreen.setEditable(false);// 为界面添加按钮Panel panel = new Panel();panel.add(buttonUse);panel.add(buttonAlarm);panel.add(buttonPhone);panel.add(buttonExit);// 配置界面add(panel, BorderLayout.SOUTH);// 显示pack();show();// 设置监听器buttonUse.addActionListener(this);buttonAlarm.addActionListener(this);buttonPhone.addActionListener(this);buttonExit.addActionListener(this);}// 按钮被按下后该方法会被调用public void actionPerformed(ActionEvent e) {System.out.println(e.toString());if (e.getSource() == buttonUse) { // 金库使用按钮state.doUse(this);} else if (e.getSource() == buttonAlarm) { // 按下警铃按钮state.doAlarm(this);} else if (e.getSource() == buttonPhone) { // 正常通话按钮state.doPhone(this);} else if (e.getSource() == buttonExit) { // 结束按钮System.exit(0);} else {System.out.println("?");}}// 设置时间public void setClock(int hour) {String clockstring = "现在时间是";if (hour < 10) {clockstring += "0" + hour + ":00";} else {clockstring += hour + ":00";}System.out.println(clockstring);textClock.setText(clockstring);state.doClock(this, hour);}// 改变状态public void changeState(State state) {System.out.println("从" + this.state + "状態变为了" + state + "状态。");this.state = state; // 给代表状态的字段赋予表示当前状态的类的实例,就相当于进行了状态迁移。}// 联系警报中心public void callSecurityCenter(String msg) {textScreen.append("call! " + msg + "\n");}// 在警报中心留下记录public void recordLog(String msg) {textScreen.append("record ... " + msg + "\n");}
}
Main类生成了一个SafeFrame类的实例,并且每秒钟调用一次setClock方法。
public class Main {public static void main(String[] args) {SafeFrame frame = new SafeFrame("State Sample");while (true) {for (int hour = 0; hour < 24; hour++) {frame.setClock(hour); // 设置时间try {Thread.sleep(1000);} catch (InterruptedException e) {}}}}
}
解析
-
State
State表示状态,定义了根据不同状态进行不同处理的API。该API是那些处理内容依赖于状态的方法的集和。
-
ConcreteState
ConcreteState表示各个具体的状态,实现了State接口。
-
Context
Context持有表示当前状态的ConcreteState。它还定义了提供外部调用者使用State模式的API。
编程时,我们经常使用分而治之的方针。这种方针非常适用于大规模的复杂处理。当遇到庞大且复杂的问题,不能用一般的方法解决时,我们会将该问题分解为多个小问题。在State模式中,我们用类来表示状态,并为每一种具体的状态都定义一个相应的类。
State模式易于增加新的状态,但是在State模式中增加其他“依赖于状态的处理”是很困难的。
Flyweight模式
Flyweight是轻量级的意思。关于Flyweight模式,一言以蔽之就是“通过尽量共享实例来避免new出实例“。
示例
BigChar表示大型字符类。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;public class BigChar {// 字符名字private char charname;// 大型字符对应的字符串(由'#' '.' '\n'组成)private String fontdata;// 构造函数public BigChar(char charname) {this.charname = charname;try {BufferedReader reader = new BufferedReader(new FileReader("big" + charname + ".txt"));String line;StringBuffer buf = new StringBuffer();while ((line = reader.readLine()) != null) {buf.append(line);buf.append("\n");}reader.close();this.fontdata = buf.toString();} catch (IOException e) {this.fontdata = charname + "?";}}// 显示大型字符public void print() {System.out.print(fontdata);}
}
BigCharFactory类是生成BigChar类的实例的工厂。它实现了共享实例的功能。
import java.util.HashMap;public class BigCharFactory {// 管理已经生成的BigChar的实例private HashMap pool = new HashMap();// Singleton模式private static BigCharFactory singleton = new BigCharFactory();// 构造函数private BigCharFactory() {}// 获取唯一的实例public static BigCharFactory getInstance() {return singleton;}// 生成(共享)BigChar类的实例public synchronized BigChar getBigChar(char charname) {BigChar bc = (BigChar)pool.get("" + charname);if (bc == null) {bc = new BigChar(charname); // 生成BigChar的实例pool.put("" + charname, bc);}return bc;}
}
BigString类是由BigChar组成的大型字符串的类。
public class BigString {// “大型字符”的数组private BigChar[] bigchars;// 构造函数public BigString(String string) {bigchars = new BigChar[string.length()];BigCharFactory factory = BigCharFactory.getInstance();for (int i = 0; i < bigchars.length; i++) {bigchars[i] = factory.getBigChar(string.charAt(i));}}// 显示public void print() {for (int i = 0; i < bigchars.length; i++) {bigchars[i].print();}}
}
Main类
public class Main {public static void main(String[] args) {if (args.length == 0) {System.out.println("Usage: java Main digits");System.out.println("Example: java Main 1212123");System.exit(0);}BigString bs = new BigString(args[0]);bs.print();}
}
解析
-
Flyweight
按照通常方式编写程序会导致程序变重,所以如果能够共享实例会比较好,而Flyweight表示的就是那些实例会被共享的类。
-
FlyweightFactory
FlyweightFactory是生成Flyweight的工厂。在工厂中生成Flyweight可以实现共享实例。
-
Client
Client使用FlyweightFactory来生成Flyweight。
Flyweight模式的主题是共享。在共享实例时,要想到“如果要改变被共享的对象,就会对多个地方产生影响”。在决定Flyweight中的字段时,需要精挑细选。只将那些真正应该在多个地方共享的字段定义在Flyweight中即可。应当共享的信息叫做Intrinsic信息,不应当共享的信息被称作Extrinsic信息。
Command模式
一个类在进行工作时会调用自己或是其他类的方法,虽然调用结果会反映在对象的状态中,但是并不会留下工作的历史记录。这时,如果有一个类用来表示“请进行这项工作”的”命令“,就会方便很多。每一项想知道的工作就不再是”方法的调用“这种动态处理,而是一个表示命令的类的实例,可以用”物“来表示。想管理工作的历史记录,只需要管理这些事例的集和即可,而且可以随时再次执行过去的命令,或是将多个过去的命令整合为一个新命令并执行。
示例
Command接口时表示命令的接口。在该接口中只定义了一个方法,即execute。至于调用execute方法后具体会进行什么样的处理,则取决于实现了Command接口的类。
package command;public interface Command {public abstract void execute();
}
MacroCommand类表示由多条命令整合成的命令。该类实现了Command接口。
package command;import java.util.Stack;
import java.util.Iterator;public class MacroCommand implements Command {// 命令的集合private Stack commands = new Stack();// 执行public void execute() {Iterator it = commands.iterator();while (it.hasNext()) {((Command)it.next()).execute();}}// 添加命令public void append(Command cmd) {if (cmd != this) {commands.push(cmd);}}// 删除最后一条命令public void undo() {if (!commands.empty()) {commands.pop();}}// 删除所有命令public void clear() {commands.clear();}
}
DrawCommand类实现了Command接口,表示绘制一个点的命令。
package drawer;import command.Command;
import java.awt.Point;public class DrawCommand implements Command {// 绘制对象protected Drawable drawable;// 绘制位置private Point position;// 构造函数public DrawCommand(Drawable drawable, Point position) {this.drawable = drawable;this.position = position;}// 执行public void execute() {drawable.draw(position.x, position.y);}
}
Drawable接口是表示绘制对象的接口。draw方法是用于绘制的方法。
package drawer;public interface Drawable {public abstract void draw(int x, int y);
}
DrawCanvas实现了Drawable接口。
package drawer;import command.*;import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;public class DrawCanvas extends Canvas implements Drawable {// 颜色private Color color = Color.red;// 要绘制的圆点的半径private int radius = 6;// 命令的历史记录private MacroCommand history;// 构造函数public DrawCanvas(int width, int height, MacroCommand history) {setSize(width, height);setBackground(Color.white);this.history = history;}// 重新全部绘制public void paint(Graphics g) {history.execute();}// 绘制public void draw(int x, int y) {Graphics g = getGraphics();g.setColor(color);g.fillOval(x - radius, y - radius, radius * 2, radius * 2);}
}
Main是启动程序的类。
import command.*;
import drawer.*;import java.awt.*;
import java.awt.event.*;
import javax.swing.*;public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener {// 绘制的历史记录private MacroCommand history = new MacroCommand();// 绘制区域private DrawCanvas canvas = new DrawCanvas(400, 400, history);// 删除按钮private JButton clearButton = new JButton("clear");// 构造函数public Main(String title) {super(title);this.addWindowListener(this);canvas.addMouseMotionListener(this);clearButton.addActionListener(this);Box buttonBox = new Box(BoxLayout.X_AXIS);buttonBox.add(clearButton);Box mainBox = new Box(BoxLayout.Y_AXIS);mainBox.add(buttonBox);mainBox.add(canvas);getContentPane().add(mainBox);pack();show();}// ActionListener接口中的方法public void actionPerformed(ActionEvent e) {if (e.getSource() == clearButton) {history.clear();canvas.repaint();}}// MouseMotionListener接口中的方法public void mouseMoved(MouseEvent e) {}public void mouseDragged(MouseEvent e) {Command cmd = new DrawCommand(canvas, e.getPoint());history.append(cmd);cmd.execute();}// WindowListener接口中的方法public void windowClosing(WindowEvent e) {System.exit(0);}public void windowActivated(WindowEvent e) {}public void windowClosed(WindowEvent e) {}public void windowDeactivated(WindowEvent e) {}public void windowDeiconified(WindowEvent e) {}public void windowIconified(WindowEvent e) {}public void windowOpened(WindowEvent e) {}public static void main(String[] args) {new Main("Command Pattern Sample");}
}
解析
-
Command
Command负责定义命令的API。
-
ConcreteCommand
ConcreteCommand负责实现在Command中定义的API。
-
Reciever
Receiver是Command执行命令时的对象,可以称之为命令接收者。
-
Client
Client负责生成ConcreteCommand并分配了Receiver。
-
Invoker
Invoker是开始执行命令的角色,它会调用在Command中定义的API。
命令的目的不同,应该包含的信息也不同。比如DrawCommand中包含了要绘制的点的位置信息,但不包含点的大小、颜色和形状等信息。
示例中,MacroCommand的实例代表了绘制的历史记录。在该字段中保存了之前所有的绘制信息。如果我们将他保存为文件,就可以永久保存历史记录。
Interpreter模式
Interpreter模式中,程序要解决的问题会被用非常简单的”迷你语言“表述出来,即用”迷你语言“编写的”迷你程序“把具体的问题表述出来。迷你程序是无法单独工作的,还需要用java编写一个负责”翻译”的程序。翻译程序会理解迷你语言,并解释和运行迷你程序。这段翻译程序被称为解释器。
示例
Node类是语法树中各个部分中最顶层的类。在Node中只声明了一个parse抽象方法,该方法用于进行语法解析处理。
public abstract class Node {public abstract void parse(Context context) throws ParseException;
}
ProgramNode类表示程序<program>
。
// <program> ::= program <command list>
public class ProgramNode extends Node {private Node commandListNode;public void parse(Context context) throws ParseException {context.skipToken("program");commandListNode = new CommandListNode();commandListNode.parse(context);}public String toString() {return "[program " + commandListNode + "]";}
}
CommandListNode表示<command list>
。
import java.util.ArrayList;// <command list> ::= <command>* end
public class CommandListNode extends Node {private ArrayList list = new ArrayList();public void parse(Context context) throws ParseException {while (true) {if (context.currentToken() == null) {throw new ParseException("Missing 'end'");} else if (context.currentToken().equals("end")) {context.skipToken("end");break;} else {Node commandNode = new CommandNode();commandNode.parse(context);list.add(commandNode);}}}public String toString() {return list.toString();}
}
CommandNode表示<command>
。
// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {private Node node;public void parse(Context context) throws ParseException {if (context.currentToken().equals("repeat")) {node = new RepeatCommandNode();node.parse(context);} else {node = new PrimitiveCommandNode();node.parse(context);}}public String toString() {return node.toString();}
}
RepeatCommandNode表示<repeat command>
。
// <repeat command> ::= repeat <number> <command list>
public class RepeatCommandNode extends Node {private int number;private Node commandListNode;public void parse(Context context) throws ParseException {context.skipToken("repeat");number = context.currentNumber();context.nextToken();commandListNode = new CommandListNode();commandListNode.parse(context);}public String toString() {return "[repeat " + number + " " + commandListNode + "]";}
}
PrimitiveCommandNode表示 <primitive command>
// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {private String name;public void parse(Context context) throws ParseException {name = context.currentToken();context.skipToken(name);if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {throw new ParseException(name + " is undefined");}}public String toString() {return name;}
}
Context类提供了语法解析所必须的方法。
import java.util.*;public class Context {private StringTokenizer tokenizer;private String currentToken;public Context(String text) {tokenizer = new StringTokenizer(text);nextToken();}public String nextToken() {if (tokenizer.hasMoreTokens()) {currentToken = tokenizer.nextToken();} else {currentToken = null;}return currentToken;}public String currentToken() {return currentToken;}public void skipToken(String token) throws ParseException {if (!token.equals(currentToken)) {throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found.");}nextToken();}public int currentNumber() throws ParseException {int number = 0;try {number = Integer.parseInt(currentToken);} catch (NumberFormatException e) {throw new ParseException("Warning: " + e);}return number;}
}
ParseException类是表示语法解析时可能发生异常的类。
public class ParseException extends Exception {public ParseException(String msg) {super(msg);}
}
Main类是启动解释器的程序。
import java.util.*;
import java.io.*;public class Main {public static void main(String[] args) {try {BufferedReader reader = new BufferedReader(new FileReader("program.txt"));String text;while ((text = reader.readLine()) != null) {System.out.println("text = \"" + text + "\"");Node node = new ProgramNode();node.parse(new Context(text));System.out.println("node = " + node);}} catch (Exception e) {e.printStackTrace();}}
}
解析
-
AbstractExpression
AbstractExpression定义了语法树节点的共同APi。
-
TerminalExpression
TerminalExpression对应BNF中的终结符表达式。
-
NonterminalExpression
NonterminalExpression对应BNF中的非终结符表达式。
-
Context
Context为解释器进行语法解析提供了必要的信息。
-
Client
为了推导语法树,Client会调用TerminalExpression和NonteminalExpression。
迷你语言包括:正则表达式、检索表达式、批处理语言等。
Reference
图解设计模式 【日】结成浩 著 杨文轩 译