本系列共分為三篇文章,其中包含的設計模式如下表:
名稱 | 設計模式 |
---|---|
圖解設計模式【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
圖解設計模式 【日】結成浩 著 楊文軒 譯