Java 23種設計模式之行為型模式-全例子詳解
設計模式的三個分類
-
建立型模式(5):物件例項化的模式,建立型模式用於解耦物件的例項化過程。
-
結構型模式(7):把類或物件結合在一起形成一個更大的結構。
-
行為型模式(11):類和物件如何互動,及劃分責任和演算法。
其實還有兩類:
- 併發型模式
- 執行緒池模式
用一個圖片來整體描述一下:
上一篇文章主要說明了結構型模式。下面繼續說明行為型模式
行為型模式的關鍵點
行為型模式用來識別物件之間的常用交流模式並加以實現。如此,可在進行這些交流活動時增強彈性。
範圍 | 模式 | 特點 |
---|---|---|
類模式 | 直譯器模式 | 給定一個語言,定義它的文法的一種表示,並定義一個直譯器。 |
類模式 | 模板模式 | 定義一個演算法結構,而將一些步驟延遲到子類實現。 |
物件模式 | 責任鏈模式 | 將請求的傳送者和接收者解耦,使的多個物件都有處理這個請求的機會。 |
物件模式 | 命令模式 | 將命令請求封裝為一個物件,使得可以用不同的請求來進行引數化。 |
物件模式 | 迭代器模式 | 一種遍歷訪問聚合物件中各個元素的方法,不暴露該物件的內部結構。 |
物件模式 | 中介者模式 | 用一箇中介物件來封裝一系列的物件互動。 |
物件模式 | 備忘錄模式 | 不破壞封裝的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。 |
物件模式 | 觀察者模式 | 定義了物件之間的一對多依賴關係。 |
物件模式 | 狀態模式 | 允許一個物件在其物件內部狀態改變時改變它的行為。 |
物件模式 | 策略模式 | 定義一系列演算法,把他們封裝起來,並且使它們可以相互替換。 |
物件模式 | 訪問者模式 | 在不改變資料結構的前提下,增加作用於一組物件元素的新功能。 |
概說行為型設計模式
直譯器模式
所謂直譯器模式就是定義語言的文法,並且建立一個直譯器來解釋該語言中的句子。直譯器模式描述瞭如何構成一個簡單的語言直譯器,主要應用在使用面嚮物件語言開發的編譯器中。它描述瞭如何為簡單的語言定義一個文法,如何在該語言中表示一個句子,以及如何解釋這些句子。
模式結構
直譯器模式包含如下角色:
- AbstractExpression: 抽象表示式
- TerminalExpression: 終結符表示式
- NonterminalExpression: 非終結符表示式
- Context: 環境類
- Client: 客戶類
用大多數人舉過的例子:簡化的字尾表示式直譯器(僅支援對整數的加法和乘法)
/**
* 定義抽象表示式介面
*/
public interface Interpreter {
int interpret();
}
/**
* 非終結符表示式,對整數進行解釋
*/
public class NumberInterpreter implements Interpreter {
private int number;
public NumberInterpreter(int number) {
this.number = number;
}
public NumberInterpreter(String number) {
this.number = Integer.parseInt(number);
}
@Override
public int interpret() {
return this.number;
}
}
/**
* 加法的終結符表示式
*/
public class AddInterpreter implements Interpreter {
private Interpreter firstExpression, secondExpression;
public AddInterpreter(Interpreter firstExpression, Interpreter secondExpression) {
this.firstExpression = firstExpression;
this.secondExpression = secondExpression;
}
@Override
public int interpret() {
return this.firstExpression.interpret() + this.secondExpression.interpret();
}
@Override
public String toString() {
return "+";
}
}
/**
* 乘法的終結符表示式
*/
public class MultiInterpreter implements Interpreter {
private Interpreter firstExpression, secondExpression;
public MultiInterpreter(Interpreter firstExpression, Interpreter secondExpression) {
this.firstExpression = firstExpression;
this.secondExpression = secondExpression;
}
@Override
public int interpret() {
return this.firstExpression.interpret() * this.secondExpression.interpret();
}
@Override
public String toString() {
return "*";
}
}
由上面可以知道,如果需要擴充套件的話就需要不斷新增新的解釋類
/**
* 使用工具類裝起來
*/
public class OperatorUtil {
public static boolean isOperator(String symbol) {
return (symbol.equals("+") || symbol.equals("*"));
}
public static Interpreter getExpressionObject(Interpreter firstExpression, Interpreter secondExpression, String symbol) {
if ("+".equals(symbol)) { // 加法
return new AddInterpreter(firstExpression, secondExpression);
} else if ("*".equals(symbol)) { // 乘法
return new MultiInterpreter(firstExpression, secondExpression);
} else {
throw new RuntimeException("不支援的操作符:" + symbol);
}
}
}
/**
* 轉移器類
*/
public class MyExpressionParser {
private Stack<Interpreter> stack = new Stack<Interpreter>();
public int parser(String str) {
String[] strItemArray = str.split(" ");
for (String symbol : strItemArray) {
if (!OperatorUtil.isOperator(symbol)) {
Interpreter numberExpression = new NumberInterpreter(symbol);
stack.push(numberExpression);
System.out.println(String.format("入棧:%d", numberExpression.interpret()));
} else {
// 是運算子,可以計算
Interpreter firstExpression = stack.pop();
Interpreter secondExpression = stack.pop();
System.out.println(String.format("出棧:%d 和 %d", firstExpression.interpret(), secondExpression.interpret()));
Interpreter operator = OperatorUtil.getExpressionObject(firstExpression, secondExpression, symbol);
System.out.println(String.format("應用運算子:%s", operator));
int result = operator.interpret();
NumberInterpreter resultExpression = new NumberInterpreter(result);
stack.push(resultExpression);
System.out.println(String.format("階段結果入棧:%d", resultExpression.interpret()));
}
}
int result = stack.pop().interpret();
return result;
}
}
測試使用
public class Test {
public static void main(String[] args) {
String inputStr = "6 100 11 + *";
MyExpressionParser expressionParser = new MyExpressionParser();
int result = expressionParser.parse(inputStr);
System.out.println("直譯器計算結果: " + result);
}
// 執行結果
// 入棧: 6
// 入棧: 100
// 入棧: 11
// 出棧: 11 和 100
// 應用運算子: +
// 階段結果入棧: 111
// 出棧: 111 和 6
// 應用運算子: *
// 階段結果入棧: 666
// 直譯器計算結果: 666
}
總結
意圖:給定一個語言,定義它的文法表示,並定義一個直譯器,這個直譯器使用該標識來解釋語言中的句子。
主要解決:對於一些固定文法構建一個解釋句子的直譯器。
何時使用:如果一種特定型別的問題發生的頻率足夠高,那麼可能就值得將該問題的各個例項表述為一個簡單語言中的句子。這樣就可以構建一個直譯器,該直譯器通過解釋這些句子來解決該問題。
如何解決:構建語法樹,定義終結符與非終結符。
關鍵程式碼:構建環境類,包含直譯器之外的一些全域性資訊,一般是 HashMap。
應用例項:編譯器、運算表示式計算。
優點:
- 可擴充套件性比較好,靈活。
- 增加了新的解釋表示式的方式。
- 易於實現簡單文法。
缺點:
- 可利用場景比較少。
- 對於複雜的文法比較難維護。
- 直譯器模式會引起類膨脹。
- 直譯器模式採用遞迴呼叫方法。
使用場景:
- 可以將一個需要解釋執行的語言中的句子表示為一個抽象語法樹。
- 一些重複出現的問題可以用一種簡單的語言來進行表達。
- 一個簡單語法需要解釋的場景。
注意事項:可利用場景比較少,JAVA 中如果碰到可以用 expression4J 代替。
模板模式
模板方法模式,可以從模板的角度考慮,就是一個對模板的應用,就好比老師出試卷,每個人的試卷都是一樣的,即都是從老師的原版試卷影印來的,這個原版試卷就是一個模板,可每個人寫在試卷上的答案都是不一樣的,這就是模板方法模式,是不是很好理解。它的主要用途在於將不變的行為從子類搬到超類,去除了子類中的重複程式碼。
所謂模板方法模式就是在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。
模板方法模式就是基於繼承的程式碼複用技術的。在模板方法模式中,我們可以將相同部分的程式碼放在父類中,而將不同的程式碼放入不同的子類中。也就是說我們需要宣告一個抽象的父類,將部分邏輯以具體方法以及具體建構函式的形式實現,然後宣告一些抽象方法讓子類來實現剩餘的邏輯,不同的子類可以以不同的方式來實現這些邏輯。所以模板方法的模板其實就是一個普通的方法,只不過這個方法是將演算法實現的步驟封裝起來的。
模式結構
模板方法模式包含如下角色:
- AbstractClass: 抽象類
- ConcreteClass: 具體子類
/**
* 抽象模板類
*/
public abstract class AbstractClass {
public abstract void PrimitiveOperation1();
public abstract void PrimitiveOperation2();
public void TemplateMethod() {
PrimitiveOperation1();
PrimitiveOperation2();
}
}
/**
* 具體類A
*/
public class ConcreteClassA extends AbstractClass {
@Override
public void PrimitiveOperation1() {
System.out.println("具體方法A方法1實現");
}
@Override
public void PrimitiveOperation2() {
System.out.println("具體方法A方法2實現");
}
}
/**
* 具體類B
*/
public class ConcreteClassB extends AbstractClass {
@Override
public void PrimitiveOperation1() {
System.out.println("具體方法B方法1實現");
}
@Override
public void PrimitiveOperation2() {
System.out.println("具體方法B方法2實現");
}
}
/**
* client呼叫
*/
public class Client {
public static void main(String[] args) {
AbstractClass abstractClass;
abstractClass = new ConcreteClassA();
abstractClass.TemplateMethod();
abstractClass = new ConcreteClassB();
abstractClass.TemplateMethod();
}
// 執行結果:
// 具體方法A方法1實現
// 具體方法A方法2實現
// 具體方法B方法1實現
// 具體方法B方法2實現
}
總結
意圖:定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。
主要解決:一些方法通用,卻在每一個子類都重新寫了這一方法。
何時使用:有一些通用的方法。
如何解決:將這些通用演算法抽象出來。
關鍵程式碼:在抽象類實現,其他步驟在子類實現。
應用例項:
- 在造房子的時候,地基、走線、水管都一樣,只有在建築的後期才有加壁櫥加柵欄等差異。
- 西遊記裡面菩薩定好的 81 難,這就是一個頂層的邏輯骨架。
- spring 中對 Hibernate 的支援,將一些已經定好的方法封裝起來,比如開啟事務、獲取 Session、關閉 Session 等,程式設計師不重複寫那些已經規範好的程式碼,直接丟一個實體就可以儲存。
優點:
- 封裝不變部分,擴充套件可變部分。
- 提取公共程式碼,便於維護。
- 行為由父類控制,子類實現。
缺點:
- 每一個不同的實現都需要一個子類來實現,導致類的個數增加,使得系統更加龐大。
使用場景:
- 有多個子類共有的方法,且邏輯相同。
- 重要的、複雜的方法,可以考慮作為模板方法。
注意事項:為防止惡意操作,一般模板方法都加上 final 關鍵詞。
責任鏈模式
職責鏈模式描述的請求如何沿著物件所組成的鏈來傳遞的。它將物件組成一條鏈,傳送者將請求發給鏈的第一個接收者,並且沿著這條鏈傳遞,直到有一個物件來處理它或者直到最後也沒有物件處理而留在鏈末尾端。
避免請求傳送者與接收者耦合在一起,讓多個物件都有可能接收請求,將這些物件連線成一條鏈,並且沿著這條鏈傳遞請求,直到有物件處理它為止,這就是職責鏈模式。在職責鏈模式中,使得每一個物件都有可能來處理請求,從而實現了請求的傳送者和接收者之間的解耦。同時職責鏈模式簡化了物件的結構,它使得每個物件都只需要引用它的後繼者即可,而不必瞭解整條鏈,這樣既提高了系統的靈活性也使得增加新的請求處理類也比較方便。但是在職責鏈中我們不能保證所有的請求都能夠被處理,而且不利於觀察執行時特徵。
模式結構
職責鏈模式包含如下角色:
- Handler: 抽象處理者
- ConcreteHandler: 具體處理者
- Client: 客戶類
/**
* 抽象的記錄器
*/
public abstract class AbstractLogger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;
protected int level;
//責任鏈中的下一個元素
protected AbstractLogger nextLogger;
public void setNextLogger(AbstractLogger nextLogger){
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message){
if(this.level <= level){
write(message);
}
if(nextLogger !=null){
nextLogger.logMessage(level, message);
}
}
abstract protected void write(String message);
}
/**
* 擴充套件了記錄器類的實體類:Console
*/
public class ConsoleLogger extends AbstractLogger {
public ConsoleLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}
/**
* 擴充套件了記錄器類的實體類:Error
*/
public class ErrorLogger extends AbstractLogger {
public ErrorLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}
/**
* 擴充套件了記錄器類的實體類:File
*/
public class FileLogger extends AbstractLogger {
public FileLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("File::Logger: " + message);
}
}
public class ChainPatternDemo {
/**
* 建立不同型別的記錄器。
* 賦予它們不同的錯誤級別,並在每個記錄器中設定下一個記錄器。
* 每個記錄器中的下一個記錄器代表的是鏈的一部分。
*/
private static AbstractLogger getChainOfLoggers(){
AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
return errorLogger;
}
/**
* 呼叫者
*/
public static void main(String[] args) {
AbstractLogger loggerChain = getChainOfLoggers();
loggerChain.logMessage(AbstractLogger.INFO, "This is an information.");
loggerChain.logMessage(AbstractLogger.DEBUG,
"This is a debug level information.");
loggerChain.logMessage(AbstractLogger.ERROR,
"This is an error information.");
}
}
最後也可以在實體類中構造的時候初始化下一個nextLogger,這樣可以只需建立最前面的物件,暴露最小級別的等級就可以了
總結
意圖:避免請求傳送者與接收者耦合在一起,讓多個物件都有可能接收請求,將這些物件連線成一條鏈,並且沿著這條鏈傳遞請求,直到有物件處理它為止。
主要解決:職責鏈上的處理者負責處理請求,客戶只需要將請求傳送到職責鏈上即可,無須關心請求的處理細節和請求的傳遞,所以職責鏈將請求的傳送者和請求的處理者解耦了。
何時使用:在處理訊息的時候以過濾很多道。
如何解決:攔截的類都實現統一介面。
關鍵程式碼:Handler 裡面聚合它自己,在 HandlerRequest 裡判斷是否合適,如果沒達到條件則向下傳遞,向誰傳遞之前 set 進去。
應用例項:
- 紅樓夢中的"擊鼓傳花"。
- JS 中的事件冒泡。
- JAVA WEB 中 Apache Tomcat 對 Encoding 的處理,Struts2的攔截器,jsp servlet 的 Filter。
優點:
- 降低耦合度。它將請求的傳送者和接收者解耦。
- 簡化了物件。使得物件不需要知道鏈的結構。
- 增強給物件指派職責的靈活性。通過改變鏈內的成員或者調動它們的次序,允許動態地新增或者刪除責任。
- 增加新的請求處理類很方便。
缺點:
- 不能保證請求一定被接收。
- 系統性能將受到一定影響,而且在進行程式碼除錯時不太方便,可能會造成迴圈呼叫。
- 可能不容易觀察執行時的特徵,有礙於除錯。
使用場景:
- 有多個物件可以處理同一個請求,具體哪個物件處理該請求由執行時刻自動確定。
- 在不明確指定接收者的情況下,向多個物件中的一個提交一個請求。
- 可動態指定一組物件處理請求。
注意事項:在 JAVA WEB 中遇到很多應用。
命令模式
有些時候我們想某個物件傳送一個請求,但是我們並不知道該請求的具體接收者是誰,具體的處理過程是如何的,們只知道在程式執行中指定具體的請求接收者即可,對於這樣將請求封裝成物件的我們稱之為命令模式。所以命令模式將請求封裝成物件,以便使用不同的請求、佇列或者日誌來引數化其他物件。同時命令模式支援可撤銷的操作。
命令模式可以將請求的傳送者和接收者之間實現完全的解耦,傳送者和接收者之間沒有直接的聯絡,傳送者只需要知道如何傳送請求命令即可,其餘的可以一概不管,甚至命令是否成功都無需關心。同時我們可以非常方便的增加新的命令,但是可能就是因為方便和對請求的封裝就會導致系統中會存在過多的具體命令類。
模式結構
命令模式包含如下角色:
- Command: 抽象命令類
- ConcreteCommand: 具體命令類
- Invoker: 呼叫者
- Receiver: 接收者
- Client:客戶類
/**
* 命令介面
*/
public interface Order {
void execute();
}
/**
* 請求類
*/
public class Stock {
private String name = "ABC";
private int quantity = 10;
public void buy(){
System.out.println("Stock [ Name: "+name+",
Quantity: " + quantity +" ] bought");
}
public void sell(){
System.out.println("Stock [ Name: "+name+",
Quantity: " + quantity +" ] sold");
}
}
/**
* Order 介面的購買實體類
*/
public class BuyStock implements Order {
private Stock abcStock;
public BuyStock(Stock abcStock){
this.abcStock = abcStock;
}
public void execute() {
abcStock.buy();
}
}
/**
* Order 介面的銷售實體類
*/
public class SellStock implements Order {
private Stock abcStock;
public SellStock(Stock abcStock){
this.abcStock = abcStock;
}
public void execute() {
abcStock.sell();
}
}
/**
* 命令呼叫類
*/
public class Broker {
private List<Order> orderList = new ArrayList<Order>();
public void takeOrder(Order order){
orderList.add(order);
}
public void placeOrders(){
for (Order order : orderList) {
order.execute();
}
orderList.clear();
}
}
/**
* 使用 Broker 類來接受並執行命令。
*/
public class CommandPatternDemo {
public static void main(String[] args) {
Stock abcStock = new Stock();
BuyStock buyStockOrder = new BuyStock(abcStock);
SellStock sellStockOrder = new SellStock(abcStock);
Broker broker = new Broker();
broker.takeOrder(buyStockOrder);
broker.takeOrder(sellStockOrder);
broker.placeOrders();
}
// 執行結果
// Stock [ Name: ABC, Quantity: 10 ] bought
// Stock [ Name: ABC, Quantity: 10 ] sold
}
總結
意圖:將一個請求封裝成一個物件,從而使您可以用不同的請求對客戶進行引數化。
主要解決:在軟體系統中,行為請求者與行為實現者通常是一種緊耦合的關係,但某些場合,比如需要對行為進行記錄、撤銷或重做、事務等處理時,這種無法抵禦變化的緊耦合的設計就不太合適。
何時使用:在某些場合,比如要對行為進行"記錄、撤銷/重做、事務"等處理,這種無法抵禦變化的緊耦合是不合適的。在這種情況下,如何將"行為請求者"與"行為實現者"解耦?將一組行為抽象為物件,可以實現二者之間的鬆耦合。
如何解決:通過呼叫者呼叫接受者執行命令,順序:呼叫者→命令→接受者。
關鍵程式碼:定義三個角色:1、received 真正的命令執行物件 2、Command 3、invoker 使用命令物件的入口
應用例項:struts 1 中的 action 核心控制器 ActionServlet 只有一個,相當於 Invoker,而模型層的類會隨著不同的應用有不同的模型類,相當於具體的 Command。
優點:
- 降低了系統耦合度。
- 新的命令可以很容易新增到系統中去。
缺點:使用命令模式可能會導致某些系統有過多的具體命令類。
使用場景:認為是命令的地方都可以使用命令模式,比如: 1、GUI 中每一個按鈕都是一條命令。 2、模擬 CMD。
注意事項:系統需要支援命令的撤銷(Undo)操作和恢復(Redo)操作,也可以考慮使用命令模式,見命令模式的擴充套件。
迭代器模式
對於迭代在程式設計過程中我們經常用到,能夠遊走於聚合內的每一個元素,同時還可以提供多種不同的遍歷方式,這就是迭代器模式的設計動機。在我們實際的開發過程中,我們可能會需要根據不同的需求以不同的方式來遍歷整個物件,但是我們又不希望在聚合物件的抽象介面中充斥著各種不同的遍歷操作,於是我們就希望有某個東西能夠以多種不同的方式來遍歷一個聚合物件,這時迭代器模式出現了。
何為迭代器模式?所謂迭代器模式就是提供一種方法順序訪問一個聚合物件中的各個元素,而不是暴露其內部的表示。迭代器模式是將迭代元素的責任交給迭代器,而不是聚合物件,我們甚至在不需要知道該聚合物件的內部結構就可以實現該聚合物件的迭代。
通過迭代器模式,使得聚合物件的結構更加簡單,它不需要關注它元素的遍歷,只需要專注它應該專注的事情,這樣就更加符合單一職責原則了。
模式結構
迭代器模式包含如下角色:
- Iterator: 抽象迭代器
- ConcreteIterator: 具體迭代器
- Aggregate: 抽象聚合類
- ConcreteAggregate: 具體聚合類
介面
public interface Iterator {
public boolean hasNext();
public Object next();
}
public interface Container {
public Iterator getIterator();
}
/**
* 實現了 Container 介面的實體類。
* 該類有實現了 Iterator 介面的內部類 NameIterator。
*/
public class NameRepository implements Container {
public String names[] = {"Robert" , "John" ,"Julie" , "Lora"};
@Override
public Iterator getIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator {
int index;
@Override
public boolean hasNext() {
if(index < names.length){
return true;
}
return false;
}
@Override
public Object next() {
if(this.hasNext()){
return names[index++];
}
return null;
}
}
}
/**
* 使用 NameRepository 來獲取迭代器,並列印名字。
*/
public class IteratorPatternDemo {
public static void main(String[] args) {
NameRepository namesRepository = new NameRepository();
for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
String name = (String)iter.next();
System.out.println("Name : " + name);
}
}
// Name : Robert
// Name : John
// Name : Julie
// Name : Lora
}
這個只是個簡單舉例,實際上可以優化成公有迭代類,用陣列傳參進去。
不過這個模式Java中已經實現,我們很少自己單獨實現。
總結
意圖:提供一種方法順序訪問一個聚合物件中各個元素, 而又無須暴露該物件的內部表示。
主要解決:不同的方式來遍歷整個整合物件。
何時使用:遍歷一個聚合物件。
如何解決:把在元素之間遊走的責任交給迭代器,而不是聚合物件。
關鍵程式碼:定義介面:hasNext, next。
應用例項:JAVA 中的 iterator。
優點:
- 它支援以不同的方式遍歷一個聚合物件。
- 迭代器簡化了聚合類。
- 在同一個聚合上可以有多個遍歷。
- 在迭代器模式中,增加新的聚合類和迭代器類都很方便,無須修改原有程式碼。
缺點:由於迭代器模式將儲存資料和遍歷資料的職責分離,增加新的聚合類需要對應增加新的迭代器類,類的個數成對增加,這在一定程度上增加了系統的複雜性。
使用場景:
- 訪問一個聚合物件的內容而無須暴露它的內部表示。
- 需要為聚合物件提供多種遍歷方式。
- 為遍歷不同的聚合結構提供一個統一的介面。
注意事項:迭代器模式就是分離了集合物件的遍歷行為,抽象出一個迭代器類來負責,這樣既可以做到不暴露集合的內部結構,又可讓外部程式碼透明地訪問集合內部的資料。
中介者模式
所謂中介者模式就是用一箇中介物件來封裝一系列的物件互動,中介者使各物件不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的互動。在中介者模式中,中介物件用來封裝物件之間的關係,各個物件可以不需要知道具體的資訊通過中介者物件就可以實現相互通訊。它減少了物件之間的互相關係,提供了系統可複用性,簡化了系統的結構。
在中介者模式中,各個物件不需要互相知道了解,他們只需要知道中介者物件即可,但是中介者物件就必須要知道所有的物件和他們之間的關聯關係,正是因為這樣就導致了中介者物件的結構過於複雜,承擔了過多的職責,同時它也是整個系統的核心所在,它有問題將會導致整個系統的問題。所以如果在系統的設計過程中如果出現“多對多”的複雜關係群時,千萬別急著使用中介者模式,而是要仔細思考是不是您設計的系統存在問題。
模式結構
中介者模式包含如下角色:
- Mediator: 抽象中介者
- ConcreteMediator: 具體中介者
- Colleague: 抽象同事類
- ConcreteColleague: 具體同事類
/**
* 中介類
*/
public class ChatRoom {
public static void showMessage(User user, String message){
System.out.println(new Date().toString()
+ " [" + user.getName() +"] : " + message);
}
}
/**
* 具體使用者類
*/
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name){
this.name = name;
}
public void sendMessage(String message){
ChatRoom.showMessage(this,message);
}
}
/**
* 使用 User 物件來顯示他們之間的通訊。
*/
public class MediatorPatternDemo {
public static void main(String[] args) {
User robert = new User("Robert");
User john = new User("John");
robert.sendMessage("Hi! John!");
john.sendMessage("Hello! Robert!");
}
// 執行結果:
// Thu Jan 31 16:05:46 IST 2013 [Robert] : Hi! John!
// Thu Jan 31 16:05:46 IST 2013 [John] : Hello! Robert!
}
總結
意圖:用一箇中介物件來封裝一系列的物件互動,中介者使各物件不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的互動。
主要解決:物件與物件之間存在大量的關聯關係,這樣勢必會導致系統的結構變得很複雜,同時若一個物件發生改變,我們也需要跟蹤與之相關聯的物件,同時做出相應的處理。
何時使用:多個類相互耦合,形成了網狀結構。
如何解決:將網狀結構分離為星型結構,中間為中介者。
關鍵程式碼:物件 Colleague 之間的通訊封裝到一個類中單獨處理。
應用例項:
- 中國加入 WTO 之前是各個國家相互貿易,結構複雜,現在是各個國家通過 WTO 來互相貿易。
- 機場排程系統。
- MVC 框架,其中C(控制器)就是 M(模型)和 V(檢視)的中介者。
優點:
- 降低了類的複雜度,將一對多轉化成了一對一。
- 各個類之間的解耦。
- 符合迪米特原則。
缺點:
- 中介者會龐大,變得複雜難以維護。
使用場景:
- 系統中物件之間存在比較複雜的引用關係,導致它們之間的依賴關係結構混亂而且難以複用該物件。
- 想通過一箇中間類來封裝多個類中的行為,而又不想生成太多的子類。
注意事項:不應當在職責混亂的時候使用。
備忘錄模式
備忘錄模式就是在不破壞封裝的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,這樣可以在以後將物件恢復到原先儲存的狀態。
與之相關的最常見的應用有許多,比如遊戲存檔,我們玩遊戲的時候肯定有存檔功能,旨在下一次登入遊戲時可以從上次退出的地方繼續遊戲,或者對復活點進行存檔,如果掛掉了則可以讀取復活點的存檔資訊重新開始。與之相類似的就是資料庫的事務回滾,或者重做日誌redo log等。
備忘錄模式實現了對資訊的封裝,使得客戶不需要關心狀態儲存的細節。儲存就要消耗資源,所以備忘錄模式的缺點就在於消耗資源。如果類的成員變數過多,勢必會佔用比較大的資源,而且每一次儲存都會消耗一定的記憶體。
模式結構
備忘錄模式包含如下角色:
- Originator: 原發器
- Memento: 備忘錄
- Caretaker: 負責人
/**
* 備忘錄角色
*/
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
/**
* 備忘錄管理員角色
*/
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
/**
* 發起人角色
*/
public class Originator {
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public Memento createMento() {
return (new Memento(state));
}
public void setMemento(Memento memento) {
state = memento.getState();
}
public void show() {
System.out.println("state = " + state);
}
}
/**
* Client客戶端
*/
public class Client {
public static void main(String[] args) {
Originator originator = new Originator();
originator.setState("On"); //Originator初始狀態
originator.show();
Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.createMento());
originator.setState("Off"); //Originator狀態變為Off
originator.show();
originator.setMemento(caretaker.getMemento()); //回覆初始狀態
originator.show();
}
// state = On
// state = Off
// state = On
}
總結
意圖:在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。
主要解決:所謂備忘錄模式就是在不破壞封裝的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,這樣可以在以後將物件恢復到原先儲存的狀態。
何時使用:很多時候我們總是需要記錄一個物件的內部狀態,這樣做的目的就是為了允許使用者取消不確定或者錯誤的操作,能夠恢復到他原先的狀態,使得他有"後悔藥"可吃。
如何解決:通過一個備忘錄類專門儲存物件狀態。
關鍵程式碼:客戶不與備忘錄類耦合,與備忘錄管理類耦合。
應用例項:
- 後悔藥。
- 打遊戲時的存檔。
- Windows 裡的 ctri + z。
- IE 中的後退。
- 資料庫的事務管理。
優點:
- 給使用者提供了一種可以恢復狀態的機制,可以使使用者能夠比較方便地回到某個歷史的狀態。
- 實現了資訊的封裝,使得使用者不需要關心狀態的儲存細節。
缺點:消耗資源。如果類的成員變數過多,勢必會佔用比較大的資源,而且每一次儲存都會消耗一定的記憶體。
使用場景:
- 需要儲存/恢復資料的相關狀態場景。
- 提供一個可回滾的操作。
注意事項:
- 為了符合迪米特原則,還要增加一個管理備忘錄的類。
- 為了節約記憶體,可使用原型模式+備忘錄模式。
觀察者模式
觀察者模式,又叫釋出-訂閱模式,本質上是一樣的,釋出訂閱模式可能中間還有管理者進行解耦,但是實際內部用的還是觀察者模式。這是定義物件間一種一對多的依賴關係,使得每當一個物件改變狀態,則所有依賴於它的物件都會得到通知並自動更新。
一般可以看做是第三者,比如在學校上自習的時候,大家肯定都有過交頭接耳、各種玩耍的經歷,這時總會有一個“放風”的小夥伴,當老師即將出現時及時“通知”大家老師來了。再比如,拍賣會的時候,大家相互叫價,拍賣師會觀察最高標價,然後通知給其它競價者競價,這就是一個觀察者模式。
觀察者之間沒有相互聯絡,所以麼可以根據需要增加和刪除觀察者,使得系統更易於擴充套件。所以觀察者提供了一種物件設計,讓主題和觀察者之間以鬆耦合的方式結合。
模式結構
觀察者模式包含如下角色:
- Subject: 目標
- ConcreteSubject: 具體目標
- Observer: 觀察者
- ConcreteObserver: 具體觀察者
/**
* 主題Subject
* 它的職責很簡單,就是定義誰能觀察,誰不能觀察
*/
public class Subject {
//觀察者陣列
//用Vector是執行緒同步的,比較安全,也可以使用ArrayList,是執行緒非同步的,但不安全。
private Vector<Observer> oVector = new Vector<>();
//增加一個觀察者
public void addObserver(Observer observer) {
this.oVector.add(observer);
}
//刪除一個觀察者
public void deleteObserver(Observer observer) {
this.oVector.remove(observer);
}
//通知所有觀察者
public void notifyObserver() {
for(Observer observer : this.oVector) {
observer.update();
}
}
}
/**
* 繼承Subject類,在這裡實現具體業務,在具體專案中,該類會有很多變種。
*/
public class ConcreteSubject extends Subject {
//具體業務
public void doSomething() {
//...
super.notifyObserver();
}
}
/**
* 抽象觀察者Observer
*/
public interface Observer {
//更新
public void update();
}
/**
* 具體觀察者
*/
public class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("收到訊息,進行處理");
}
}
/**
* 測試客戶端
*/
public class Client {
public static void main(String[] args) {
//建立一個主題
ConcreteSubject subject = new ConcreteSubject();
//定義一個觀察者
Observer observer = new ConcreteObserver();
//觀察
subject.addObserver(observer);
//開始活動
subject.doSomething();
}
// 執行結果:
// 收到訊息,進行處理
}
總結
意圖:定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。
主要解決:一個物件狀態改變給其他物件通知的問題,而且要考慮到易用和低耦合,保證高度的協作。
何時使用:一個物件(目標物件)的狀態發生改變,所有的依賴物件(觀察者物件)都將得到通知,進行廣播通知。
如何解決:使用面向物件技術,可以將這種依賴關係弱化。
關鍵程式碼:在抽象類裡有一個 觀察者陣列 存放觀察者們。
應用例項:
- 拍賣的時候,拍賣師觀察最高標價,然後通知給其他競價者競價。
- 西遊記裡面悟空請求菩薩降服紅孩兒,菩薩灑了一地水招來一個老烏龜,這個烏龜就是觀察者,他觀察菩薩灑水這個動作。
優點:
- 觀察者和被觀察者是抽象耦合的。
- 建立一套觸發機制。
缺點:
- 如果一個被觀察者物件有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。
- 如果在觀察者和觀察目標之間有迴圈依賴的話,觀察目標會觸發它們之間進行迴圈呼叫,可能導致系統崩潰。
- 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標物件是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
使用場景:
- 一個抽象模型有兩個方面,其中一個方面依賴於另一個方面。將這些方面封裝在獨立的物件中使它們可以各自獨立地改變和複用。
- 一個物件的改變將導致其他一個或多個物件也發生改變,而不知道具體有多少物件將發生改變,可以降低物件之間的耦合度。
- 一個物件必須通知其他物件,而並不知道這些物件是誰。
- 需要在系統中建立一個觸發鏈,A物件的行為將影響B物件,B物件的行為將影響C物件……,可以使用觀察者模式建立一種鏈式觸發機制。
注意事項:
- JAVA 中已經有了對觀察者模式的支援類。
- 避免迴圈引用。
- 如果順序執行,某一觀察者錯誤會導致系統卡殼,一般採用非同步方式。
狀態模式
在很多情況下我們物件的行為依賴於它的一個或者多個變化的屬性,這些可變的屬性我們稱之為狀態,也就是說行為依賴狀態,即當該物件因為在外部的互動而導致他的狀態發生變化,從而它的行為也會做出相應的變化。對於這種情況,我們是不能用行為來控制狀態的變化,而應該站在狀態的角度來思考行為,即是什麼狀態就要做出什麼樣的行為。這個就是狀態模式。
所以狀態模式就是允許物件在內部狀態發生改變時改變它的行為,物件看起來好像修改了它的類。
在狀態模式中我們可以減少大塊的if…else語句,它是允許態轉換邏輯與狀態物件合成一體,但是減少if…else語句的代價就是會換來大量的類,所以狀態模式勢必會增加系統中類或者物件的個數。
同時狀態模式是將所有與某個狀態有關的行為放到一個類中,並且可以方便地增加新的狀態,只需要改變物件狀態即可改變物件的行為。但是這樣就會導致系統的結構和實現都會比較複雜,如果使用不當就會導致程式的結構和程式碼混亂,不利於維護。
模式結構
狀態模式包含如下角色:
- Context: 環境類
- State: 抽象狀態類
- ConcreteState: 具體狀態類
/**
* Context類為環境角色,具有兩個職責:
* 1:處理本狀態必須完成的任務
* 2:決定是否可以過渡到其它狀態
*/
public class Context {
//定義狀態
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();
//當前狀態
private State currentState;
//獲得當前狀態
public State getCurrentState() {
return currentState;
}
//設定當前狀態
public void setCurrentState(State currentState) {
this.currentState = currentState;
// System.out.println("當前狀態:" + currentState);
//切換狀態
this.currentState.setContext(this);
}
public void handle1() {
this.currentState.handle1();
}
public void handle2() {
this.currentState.handle2();
}
}
對於環境角色,有幾個不成文的約束:
- 把狀態物件宣告為靜態常量,有幾個狀態物件就宣告幾個狀態常量
- 環境角色具有狀態抽象角色定義的所有行為,具體執行使用委託方式
/**
* 抽象狀態類
* 抽象環境中宣告一個環境角色,提供各個狀態類自行訪問,
* 並且提供所有狀態的抽象行為,由各個實現類實現。
*/
public abstract class State {
protected Context context;
public void setContext(Context context) {
this.context = context;
}
//行為1
public abstract void handle1();
//行為2
public abstract void handle2();
}
/**
* 具體狀態
*/
public class ConcreteState1 extends State {
@Override
public void handle1() {
//...
System.out.println("ConcreteState1 的 handle1 方法");
}
@Override
public void handle2() {
super.context.setCurrentState(Context.STATE2);
System.out.println("ConcreteState1 的 handle2 方法");
}
}
public class Client {
public static void main(String[] args) {
//定義環境角色
Context context = new Context();
//初始化狀態
context.setCurrentState(new ConcreteState1());
//行為執行
context.handle1();
context.handle2();
}
// 執行結果:
// ConcreteState1 的 handle1 方法
// ConcreteState1 的 handle2 方法
}
從執行結果可見,我們已經隱藏了狀態的變化過程,它的切換引起了行為的變化。對外來說,我們只看到了行為的改變,而不用知道是狀態變化引起的。
總結
意圖:允許物件在內部狀態發生改變時改變它的行為,物件看起來好像修改了它的類。
主要解決:物件的行為依賴於它的狀態(屬性),並且可以根據它的狀態改變而改變它的相關行為。
何時使用:程式碼中包含大量與物件狀態有關的條件語句。
如何解決:將各種具體的狀態類抽象出來。
關鍵程式碼:通常命令模式的介面中只有一個方法。而狀態模式的介面中有一個或者多個方法。而且,狀態模式的實現類的方法,一般返回值,或者是改變例項變數的值。也就是說,狀態模式一般和物件的狀態有關。實現類的方法有不同的功能,覆蓋介面中的方法。狀態模式和命令模式一樣,也可以用於消除 if...else 等條件選擇語句。
應用例項:
- 打籃球的時候運動員可以有正常狀態、不正常狀態和超常狀態。
- 曾侯乙編鐘中,'鍾是抽象介面','鍾A'等是具體狀態,'曾侯乙編鐘'是具體環境(Context)。
優點:
- 封裝了轉換規則。
- 列舉可能的狀態,在列舉狀態之前需要確定狀態種類。
- 將所有與某個狀態有關的行為放到一個類中,並且可以方便地增加新的狀態,只需要改變物件狀態即可改變物件的行為。
- 允許狀態轉換邏輯與狀態物件合成一體,而不是某一個巨大的條件語句塊。
- 可以讓多個環境物件共享一個狀態物件,從而減少系統中物件的個數。
缺點:
- 狀態模式的使用必然會增加系統類和物件的個數。
- 狀態模式的結構與實現都較為複雜,如果使用不當將導致程式結構和程式碼的混亂。
- 狀態模式對"開閉原則"的支援並不太好,對於可以切換狀態的狀態模式,增加新的狀態類需要修改那些負責狀態轉換的原始碼,否則無法切換到新增狀態,而且修改某個狀態類的行為也需修改對應類的原始碼。
使用場景:
- 行為隨狀態改變而改變的場景。
- 條件、分支語句的代替者。
注意事項:在行為受狀態約束的時候使用狀態模式,而且狀態不超過 5 個。
策略模式
我們有很多種方法來實現一個功能,但是我們需要一種簡單、高效的方式來實現它,使得系統能夠非常靈活,這就是策略模式。
所以策略模式就是定義了演算法族,分別封裝起來,讓他們之前可以互相轉換,此模式然該演算法的變化獨立於使用演算法的客戶。
比如我們去逛商場,商場現在正在搞活動,有打折的、有滿減的、有返利的等等,其實不管商場如何進行促銷,說到底都是一些演算法,這些演算法本身只是一種策略,並且這些演算法是隨時都可能互相替換的。
在策略模式中它將這些解決問題的方法定義成一個演算法群,每一個方法都對應著一個具體的演算法,這裡的一個演算法我就稱之為一個策略。雖然策略模式定義了演算法,但是它並不提供演算法的選擇,即什麼演算法對於什麼問題最合適這是策略模式所不關心的,所以對於策略的選擇還是要客戶端來做。客戶必須要清楚的知道每個演算法之間的區別和在什麼時候什麼地方使用什麼策略是最合適的,這樣就增加客戶端的負擔。
同時策略模式也非常完美的符合了“開閉原則”,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。但是一個策略對應一個類將會是系統產生很多的策略類。
模式結構
策略模式包含如下角色:
- Context: 環境類
- Strategy: 抽象策略類
- ConcreteStrategy: 具體策略類
策略模式的類圖呵狀態模式的類似,並且都是能夠動態改變物件的行為。但是狀態模式是通過狀態轉移來改變 Context 所組合的 State 物件,而策略模式是通過 Context 本身的決策來改變組合的 Strategy 物件。所謂的狀態轉移,是指 Context 在執行過程中由於一些條件發生改變而使得 State 物件發生改變,注意必須要是在執行過程中。
狀態模式主要是用來解決狀態轉移的問題,當狀態發生轉移了,那麼 Context 物件就會改變它的行為;而策略模式主要是用來封裝一組可以互相替代的演算法族,並且可以根據需要動態地去替換 Context 使用的演算法。
/**
* 策略介面
*/
public interface Strategy {
public int doOperation(int num1, int num2);
}
實現介面的策略實體類們:
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
/**
* 上下文環境類,維護一個對Strategy物件的引用
*/
public class Context {
private Strategy strategy;
public Context(){
}
public SetStrategy(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
public static void main(String[] args) {
Context context = new Context();
context.SetStrategy(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context.SetStrategy(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context.SetStrategy(new OperationMultiply()); System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
// 執行結果:
// 10 + 5 = 15
// 10 - 5 = 5
// 10 * 5 = 50
總結
意圖:定義一系列的演算法,把它們一個個封裝起來, 並且使它們可相互替換。
主要解決:在有多種演算法相似的情況下,使用 if...else 所帶來的複雜和難以維護。
何時使用:一個系統有許多許多類,而區分它們的只是他們直接的行為。
如何解決:將這些演算法封裝成一個一個的類,任意地替換。
關鍵程式碼:實現同一個介面。
應用例項:
- 諸葛亮的錦囊妙計,每一個錦囊就是一個策略。
- 旅行的出遊方式,選擇騎自行車、坐汽車,每一種旅行方式都是一個策略。
- JAVA AWT 中的 LayoutManager。
優點:
- 演算法可以自由切換。
- 避免使用多重條件判斷。
- 擴充套件性良好。
缺點:
- 策略類會增多。
- 所有策略類都需要對外暴露。
使用場景:
- 如果在一個系統裡面有許多類,它們之間的區別僅在於它們的行為,那麼使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。
- 一個系統需要動態地在幾種演算法中選擇一種。
- 如果一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。
注意事項:如果一個系統的策略多於四個,就需要考慮使用混合模式,解決策略類膨脹的問題。
訪問者模式
訪問者模式俗稱23大設計模式中最難的一個。除了結構複雜外,理解也比較難。在我們軟體開發中我們可能會對同一個物件有不同的處理,如果我們都做分別的處理,將會產生災難性的錯誤。對於這種問題,訪問者模式提供了比較好的解決方案。
訪問者模式即表示一個作用於某物件結構中的各元素的操作,它使我們可以在不改變各元素的類的前提下定義作用於這些元素的新操作。
例如,你在朋友家做客,你是訪問者,朋友接收你的訪問,你通過朋友的描述,然後對朋友的描述做出一個判斷,這就是訪問者模式。
訪問者模式的目的是封裝一些施加於某種資料結構元素之上的操作,一旦這些操作需要修改的話,接受這個操作的資料結構可以保持不變。為不同型別的元素提供多種訪問操作方式,且可以在不修改原有系統的情況下增加新的操作方式。同時我們還需要明確一點那就是訪問者模式是適用於那些資料結構比較穩定的,因為他是將資料的操作與資料結構進行分離了,如果某個系統的資料結構相對穩定,但是操作演算法易於變化的話,就比較適用適用訪問者模式,因為訪問者模式使得演算法操作的增加變得比較簡單了。
模式結構
訪問者模式包含如下角色:
- Vistor: 抽象訪問者
- ConcreteVisitor: 具體訪問者
- Element: 抽象元素
- ConcreteElement: 具體元素
- ObjectStructure: 物件結構
/**
* 抽象訪問者
*/
public abstract class Visitor {
public abstract void visitConcreteElementA(ConcreteElementA concreteElementA);
public abstract void visitConcreteElementB(ConcreteElementB concreteElementB);
}
/**
* 具體訪問者
*/
public class ConcreteVisitor1 extends Visitor {
@Override
public void visitConcreteElementA(ConcreteElementA concreteElementA) {
System.out.println(concreteElementA.getClass().getName() + " 被 " + this.getClass().getName() + " 訪問");
}
@Override
public void visitConcreteElementB(ConcreteElementB concreteElementB) {
System.out.println(concreteElementB.getClass().getName() + " 被 " + this.getClass().getName() + " 訪問");
}
}
/**
* 抽象元素
* 用於宣告接受哪一類訪問者訪問,程式上是通過accpet方法中的引數來定義的。
*/
public abstract class Element {
//抽象元素有兩類方法,一是本身的業務邏輯,也就是元素作為一個業務處理單元必須完成的職責;
//另外一個是允許哪一個訪問者來訪問。這裡只宣告的第二類即accept方法。
public abstract void accept(Visitor visitor);
}
/**
* 具體元素
*/
public class ConcreteElementA extends Element {
@Override
public void accept(Visitor visitor) {
visitor.visitConcreteElementA(this);
}
//其它方法
public void operationA() {
}
}
/**
* 結構物件
* 元素生產者,一般容納在多個不同類、不同介面的容器,
* 如List、Set、Map等,在專案中,一般很少抽象出這個角色。
*/
public class ObjectStructure {
private List<Element> elements = new LinkedList<>();
public void attach(Element element) {
elements.add(element);
}
public void detach(Element element) {
elements.remove(element);
}
public void accept(Visitor visitor) {
for (Element element : elements) {
element.accept(visitor);
}
}
}
/**
* Client客戶端
*/
public class Client {
public static void main(String[] args) {
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.attach(new ConcreteElementA());
objectStructure.attach(new ConcreteElementB());
ConcreteVisitor1 visitor1 = new ConcreteVisitor1();
ConcreteVisitor2 visitor2 = new ConcreteVisitor2();
objectStructure.accept(visitor1);
objectStructure.accept(visitor2);
}
// 執行結果:
// com.adamjwh.gof.visitor.ConcreteElementA 被 com.adamjwh.gof.visitor.Concretevisitor1 訪問
// com.adamjwh.gof.visitor.ConcreteElementB 被 com.adamjwh.gof.visitor.Concretevisitor1 訪問
// com.adamjwh.gof.visitor.ConcreteElementA 被 com.adamjwh.gof.visitor.Concretevisitor2 訪問
// com.adamjwh.gof.visitor.ConcreteElementB 被 com.adamjwh.gof.visitor.ConcreteVisitor2 訪問
}
總結
意圖:主要將資料結構與資料操作分離。
主要解決:穩定的資料結構和易變的操作耦合問題。
何時使用:需要對一個物件結構中的物件進行很多不同的並且不相關的操作,而需要避免讓這些操作"汙染"這些物件的類,使用訪問者模式將這些封裝到類中。
如何解決:在被訪問的類裡面加一個對外提供接待訪問者的介面。
關鍵程式碼:在資料基礎類裡面有一個方法接受訪問者,將自身引用傳入訪問者。
應用例項:您在朋友家做客,您是訪問者,朋友接受您的訪問,您通過朋友的描述,然後對朋友的描述做出一個判斷,這就是訪問者模式。
優點:
- 符合單一職責原則。
- 優秀的擴充套件性。
- 靈活性。
缺點:
- 具體元素對訪問者公佈細節,違反了迪米特原則。
- 具體元素變更比較困難。
- 違反了依賴倒置原則,依賴了具體類,沒有依賴抽象。
使用場景:
- 物件結構中物件對應的類很少改變,但經常需要在此物件結構上定義新的操作。
- 需要對一個物件結構中的物件進行很多不同的並且不相關的操作,而需要避免讓這些操作"汙染"這些物件的類,也不希望在增加新操作時修改這些類。
注意事項:訪問者可以對功能進行統一,可以做報表、UI、攔截器與過濾器。