五、狀態模式
? ? ? ? 1、概述
? ? ? ? ?狀態設計模式是一種行為型設計模式,它允許對象在其內部狀態發生時改變其行為,這種模式可以消除大量的條件語句,并將每個狀態的行為封裝到單獨的類中。
? ? ? ? 狀態模式的主要組成部分如下:
? ? ? ? 1)上下文(Context):上下文通常包含一個具體狀態的引用,用于維護當前狀態,上下文委托給當前對象處理狀態相關行為。
? ? ? ? 2)抽象狀態(State):定義一個接口,用于封裝與上下文的特定狀態相關的行為。
? ? ? ? 3)具體狀態(Concrete State):實現抽象狀態接口,為具體狀態定義行為。每個具體狀態類對應一個狀態。
? ? ? ? 簡單示例,假設要模擬一個簡易的電視遙控器,具有開啟、關閉和調整音量的功能。
? ? ? ? 假設不使用設計模式:
public class TV {private boolean isOn;private int volume;public TV() {isOn = false;volume = 0;}public void turnOn() {// 如果是開啟狀態if (isOn) {System.out.println("TV is already on.");// 否則打開電視} else {isOn = true;System.out.println("Turning on the TV.");}}public void turnOff() {if (isOn) {isOn = false;System.out.println("Turning off the TV.");} else {System.out.println("TV is already off.");}}public void adjustVolume(int volume) {if (isOn) {this.volume = volume;System.out.println("Adjusting volume to: " + volume);} else {System.out.println("Cannot adjust volume, TV is off.");}}
}
public class Main {public static void main(String[] args) {TV tv = new TV();tv.turnOn();tv.adjustVolume(10);tv.turnOff();}
}
? ? ? ? 該例子中我們狀態比較少,所以代碼看起來也不是很復雜,但是狀態如果變多了就會變得不好控制,比如增加換臺,快捷鍵,靜音等功能。
????????使用狀態設計模式之后:
? ? ? ? 首先定義抽象狀態接口TVState,將每一個修改狀態的動作抽象成一個接口:
public interface TVState {void turnOn();void turnOff();void adjustVolume(int volume);
}
? ? ? ? 接下來為每個具體的狀態創建類,實現TVState接口,例如:創建TVOnState 和 TVOffState 類:
// 在on狀態下,去執行以下各種操作
public class TVOnState implements TVState {@Overridepublic void turnOn() {System.out.println("TV is already on.");}@Overridepublic void turnOff() {System.out.println("Turning off the TV.");}@Overridepublic void adjustVolume(int volume) {System.out.println("Adjusting volume to: " + volume);}
}
// 在關機的狀態下執行以下的操作
public class TVOffState implements TVState {@Overridepublic void turnOn() {System.out.println("Turning on the TV.");}@Overridepublic void turnOff() {System.out.println("TV is already off.");}@Overridepublic void adjustVolume(int volume) {System.out.println("Cannot adjust volume, TV is off.");}
}
? ? ? ? 接下來定義上下文類TV:
public class TV {// 當前狀態private TVState state;public TV() {state = new TVOffState();}public void setState(TVState state) {this.state = state;}public void turnOn() {// 打開state.turnOn();// 設置為開機狀態setState(new TVOnState());}public void turnOff() {// 關閉state.turnOff();// 設置為關機狀態setState(new TVOffState());}public void adjustVolume(int volume) {state.adjustVolume(volume);}
}
? ? ? ? 最后通過以下方式使用這些類:
public class Main {public static void main(String[] args) {TV tv = new TV();tv.turnOn();tv.adjustVolume(10);tv.turnOff();}
}
????????這個例子展示了狀態模式的基本結構和用法。通過使用狀態模式,我們可以更好地組織和管理與特定狀態相關的代碼。當狀態較多時,這種模式的優勢就會凸顯出來,同時我們在代碼時,因為我們會對每個狀態進行獨立封裝,所以也會簡化代碼編寫。
? ? ? ? 2、有限狀態機
? ? ? ? 有限狀態機,英文翻譯時Flinite State Machine,縮寫為FSM,簡稱狀態機,比較官方的說法是:有限狀態機是描述對象在它的生命周期內所經歷的狀態序列,以及如何響應來自外界的各種事件。狀態機有3個組成部分:狀態(State)、事件(Event)、動作(Action)。其中,事件也成為轉移條件(Transition Condition)。事件觸發狀態的轉移及動作的執行,不過,動作不是必須的,也可能只轉移狀態,不執行任何動作。
? ? ? ? 2.1 分支法
? ? ? ? 如何實現狀態機,總結了三種方式。其中,最簡單直接的實現方式是,參照狀態轉移圖,將每一個狀態轉移。原模原樣的直譯成代碼。這樣編寫的代碼會包含大量的 if - else或 switch-case 分支判斷邏輯,甚至是嵌套的分支判斷邏輯。所以我們把這種方法暫且命名為分支法:
? ??????下面是一個使用if-else 語句實現的馬里奧形態變化的代碼示例:
public class Mario {private MarioState state;public Mario() {state = MarioState.SMALL;}public void handleEvent(Event event) {MarioState newState = state;// 處理吃蘑菇事件if (event == Event.MUSHROOM) {if (state == MarioState.SMALL) {newState = MarioState.BIG;}// 處理吃火花事件} else if (event == Event.FIRE_FLOWER) {if (state == MarioState.BIG) {newState = MarioState.FIRE;}// 處理遇到小怪事件} else if (event == Event.ENEMY_ATTACK) {if (state == MarioState.BIG) {newState = MarioState.SMALL;} else if (state == MarioState.FIRE) {newState = MarioState.BIG;} else if (state == MarioState.SMALL) {newState = MarioState.DEAD;}// 處理掉坑事件} else if (event == Event.FALL_INTO_PIT) {newState = MarioState.DEAD;}System.out.printf("從 %s 變為 %s%n", state, newState);state = newState;}
}
public class MarioDemo {public static void main(String[] args) {Mario mario = new Mario();mario.handleEvent(Event.MUSHROOM); // 變為大馬里奧mario.handleEvent(Event.FIRE_FLOWER); // 變為火焰馬里奧mario.handleEvent(Event.ENEMY_ATTACK); // 變為死亡馬里奧}
}
? ? ? ? 2.2 查表法
? ? ? ? ? ?該種方法略,不怎么用,可以自己查資料。
? ? ? ? 2.3 狀態模式
? ? ? ??在查表法的代碼實現中,事件觸發的動作只是簡單的狀態或者數值,所以,我們用一個 MarioState類型的二維數組 TRANSITION_TABLE 就能表示,二維數組中的值表示出發事件后的新狀態。但是,如果要執行的動作并非這么簡單,而是一系列復雜的邏輯操作(比如加減分數、處理位置信息等等),我們就沒法用如此簡單的二維數組來表示了。這也就是說,查表法的實現方式有一定局限性。
????????雖然分支邏輯的實現方式不存在這個問題,但它又存在前面講到的其他問題,比如分支判斷邏輯較多,導致代碼可讀性和可維護性不好等。實際上,針對分支邏輯法存在的問題,我們可以使用狀態模式來解決。
????????狀態模式通過將事件觸發的狀態轉移和動作執行,拆分到不同的狀態類中,來避免分支判斷邏輯。我們還是結合代碼來理解這句話。利用狀態模式,我們來補全 MarioStateMachine 類,補全后的代碼如下所示。
????????以下是一個使用 Java 實現的簡化版馬里奧形態變化的案例代碼,我為代碼添加了中文注釋以便理解:
// 定義事件枚舉類型
enum Event {// 吃蘑菇,吃火花,遇到小怪,調入深坑MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT
}
// 定義馬里奧狀態接口
interface MarioState {void handleEvent(Event event);
}
// 實現死亡馬里奧狀態
class DeadMario implements MarioState {private Mario mario;public DeadMario(Mario mario) {this.mario = mario;}@Overridepublic void handleEvent(Event event) {System.out.println("馬里奧已死亡,無法處理事件");}
}
// 實現小馬里奧狀態
class SmallMario implements MarioState {private Mario mario;public SmallMario(Mario mario) {this.mario = mario;}@Overridepublic void handleEvent(Event event) {switch (event) {case MUSHROOM:System.out.println("變為大馬里奧");mario.setState(new BigMario(mario));break;case FIRE_FLOWER:System.out.println("小馬里奧不能直接變為火焰馬里奧");break;case ENEMY_ATTACK:System.out.println("小瑪麗奧去世了");mario.setState(new DeadMario(mario));break;case FALL_INTO_PIT:System.out.println("小瑪麗奧去世了");mario.setState(new DeadMario(mario));break;}}
}
// 實現大馬里奧狀態
class BigMario implements MarioState {private Mario mario;public BigMario(Mario mario) {this.mario = mario;}@Overridepublic void handleEvent(Event event) {switch (event) {case MUSHROOM:System.out.println("保持大馬里奧");break;case FIRE_FLOWER:System.out.println("變為火焰馬里奧");mario.setState(new FireMario(mario));break;case ENEMY_ATTACK:System.out.println("變為小馬里奧");mario.setState(new SmallMario(mario));break;case FALL_INTO_PIT:System.out.println("馬里奧去世了");mario.setState(new DeadMario(mario));break;}}
}
// 實現火焰馬里奧狀態
class FireMario implements MarioState {private Mario mario;public FireMario(Mario mario) {this.mario = mario;}@Overridepublic void handleEvent(Event event) {switch (event) {case MUSHROOM:System.out.println("保持火焰馬里奧");break;case FIRE_FLOWER:System.out.println("保持火焰馬里奧");break;case ENEMY_ATTACK:System.out.println("變為大馬里奧");mario.setState(new BigMario(mario));break;case FALL_INTO_PIT:System.out.println("馬里奧去世了");mario.setState(new DeadMario(mario));break;}}
}
// 定義馬里奧類,作為狀態的上下文
class Mario {private MarioState state;public Mario() {state = new SmallMario(this);}public void setState(MarioState state) {this.state = state;}public void handleEvent(Event event) {state.handleEvent(event);}
}
// 測試類
public class MarioDemo {public static void main(String[] args) {Mario mario = new Mario();mario.handleEvent(Event.MUSHROOM); // 變為大馬里奧mario.handleEvent(Event.FIRE_FLOWER); // 變為火焰馬里奧mario.handleEvent(Event.ENEMY_ATTACK); // 變為大馬里奧}
}
????????在這個簡化示例中,我們定義了 MarioState 接口以及實現了 DeadMario 、SmallMario 、 BigMario 和 FireMario 類,分別表示馬里奧的四種形態。每個形態類實現了 handleEvent 方法,用于處理不同的游戲事件并根據有限狀態機規 則進行狀態轉換。
????????Mario 類作為狀態的上下文,用于管理和切換馬里奧的狀態。它有一個 setState方法,用于更新當前狀態。 handleEvent 方法將事件傳遞給當前狀態,以便根據事件執行相應的狀態轉換。
????????在 MarioDemo 測試類中,我們創建了一個 Mario 實例,并通過調用handleEvent 方法模擬游戲中的事件。通過運行這個測試類,你可以觀察到馬里奧根據有限狀態機的規則在不同形態之間切換。
????????這個簡化示例展示了如何使用有限狀態機來實現馬里奧角色的形態變化。在實際游戲開發中,你可能需要考慮更多的事件和狀態,以及與游戲引擎或框架集成的方式。不過,這個示例可以幫助你理解有限狀態機在游戲中的應用。
六、迭代器模式
? ? ? ? 迭代器模式,它用來遍歷集合對象,不過,很多編程語言都將迭代器作為一個基礎的類庫,直接提供出來了。在平時的開發中,特別是業務開發,我們直接使用即可,很少會自己去實現一個迭代器。不過弄懂原理能幫助我們更好的使用這些工具類。
? ? ? ? 不怎么用,省略,需要可以自行查詢。
????????
七、訪問者模式
? ? ? ? 1、概述和原理
? ? ? ? 訪問者設計模式(Visitor Pattern)是一種行為型設計模式,它允許你再不修改現有類結構的情況下,為類添加新的操作,這種模式可以實現良好的解耦和擴展性,尤其適用于現有類層次結構中添加新的功能的情況。
? ? ? ? 訪問者模式主要包含以下角色:
????????1. 訪問者(Visitor):定義一個訪問具體元素的接口,為每種具體元素類型聲明一個訪問操作。
????????2. 具體訪問者(ConcreteVisitor):實現訪問者接口,為每種具體元素提供具體的訪問操作實現。
????????3. 元素(Element):定義一個接口,聲明接受訪問者的方法。
????????4. 具體元素(ConcreteElement):實現元素接口,提供接受訪問者的具體實現。
????????5. 對象結構(ObjectStructure):包含一個元素集合,提供一個方法以遍歷這些元素并讓訪問者訪問它們。
????????以下是一個簡單的訪問者模式示例:
????????假設我們有一個表示計算機組件的類層次結構(如 CPU、內存和硬盤等),我們需要為這些組件實現一個功能,比如展示它們的詳細信息。使用訪問者模式,我們可以將【展示詳細信息】的功能與【組件類】分離,從而實現解耦和擴展性。
????????1、不同的元素(被訪問的對象)可以接收不同的訪問者。
????????2、不同的訪問者會對不同的被訪問者產生不同的行為。
????????3、如果想要擴展,則獨立重新實現訪問者接口,產生一個新的具體訪問者就可以了。
????????4、他實際解耦的是【被訪問者】和【對被訪問者的操作】。
????????簡單理解就是不同的訪問者,到了同一個被訪問對象的家里會干不同的事。這個【事】就是行為,通過訪問者模式,我們可以將行為和對象分離解耦,如下圖。
// 訪問者接口
interface ComputerPartVisitor {// 訪問 Computer 對象void visit(Computer computer);// 訪問 Mouse 對象void visit(Mouse mouse);// 訪問 Keyboard 對象void visit(Keyboard keyboard);
}
// 具體訪問者
class ComputerPartDisplayVisitor implements ComputerPartVisitor {// 訪問 Computer 對象@Overridepublic void visit(Computer computer) {System.out.println("Displaying Computer.");}// 訪問 Mouse 對象@Overridepublic void visit(Mouse mouse) {System.out.println("Displaying Mouse.");}// 訪問 Keyboard 對象@Overridepublic void visit(Keyboard keyboard) {System.out.println("Displaying Keyboard.");}
}
// 元素接口
interface ComputerPart {// 接受訪問者的訪問void accept(ComputerPartVisitor computerPartVisitor);
}
// 具體元素
class Computer implements ComputerPart {// 子元素數組ComputerPart[] parts;public Computer() {// 初始化子元素數組parts = new ComputerPart[]{new Mouse(), new Keyboard()};}// 接受訪問者的訪問@Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {// 遍歷所有子元素并接受訪問者的訪問for (int i = 0; i < parts.length; i++) {parts[i].accept(computerPartVisitor);}// 訪問 Computer 對象本身computerPartVisitor.visit(this);}
}
// 具體元素:鼠標
class Mouse implements ComputerPart {// 接受訪問者的訪問@Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {// 訪問 Mouse 對象computerPartVisitor.visit(this);}
}
// 具體元素:鍵盤
class Keyboard implements ComputerPart {// 接受訪問者的訪問@Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {// 訪問 Keyboard 對象computerPartVisitor.visit(this);}
}
// 客戶端代碼
public class VisitorPatternDemo {public static void main(String[] args) {// 創建一個 Computer 對象ComputerPart computer = new Computer();// 創建一個具體訪問者ComputerPartVisitor visitor = new ComputerPartDisplayVisitor();// 讓 Computer 對象接受訪問者的訪問computer.accept(visitor);}
}
????????在這個示例中,我們定義了一個表示計算機組件的類層次結構,包括 Computer 、Mouse 和 Keyboard 。這些類實現了 ComputerPart 接口,該接口聲明了一個接受訪問者的方法。我們還定義了一個 ComputerPartVisitor 接口,用于訪問這些計算機組件,并為每種組件類型聲明了一個訪問操作。
????????ComputerPartDisplayVisitor 類實現了 ComputerPartVisitor 接口,為每種計算機組件提供了展示詳細信息的功能。在客戶端代碼中,我們創建了一個Computer 對象和一個 ComputerPartDisplayVisitor 對象。當我們調用computer.accept() 方法時,計算機的所有組件都會被訪問者訪問,并顯示相應的詳細信息。
????????這個示例展示了如何使用訪問者模式將功能與類結構分離,實現解耦和擴展性。如果我們需要為計算機組件添加新功能,只需創建一個新的訪問者類,而無需修改現有的組件類。這使得在不影響現有代碼的情況下,為系統添加新功能變得容易。
// 添加一個更新計算機部件的訪問者實現
class ComputerPartUpdateVisitorImpl implements ComputerPartVisitor {// 訪問 Computer 對象并執行更新操作@Overridepublic void visit(Computer computer) {System.out.println("Updating Computer.");}// 訪問 Mouse 對象并執行更新操作@Overridepublic void visit(Mouse mouse) {System.out.println("Updating Mouse.");}// 訪問 Keyboard 對象并執行更新操作@Overridepublic void visit(Keyboard keyboard) {System.out.println("Updating Keyboard.");}
}
// 客戶端代碼,幾乎不用任何修改
public class VisitorPatternDemo {public static void main(String[] args) {// 創建一個 Computer 對象ComputerPart computer = new Computer();// 創建一個具體訪問者ComputerPartVisitor visitor = new ComputerPartUpdateVisitorImpl();// 讓 Computer 對象接受訪問者的訪問computer.accept(visitor);}
}
????????訪問者模式可以算是 23 種經典設計模式中最難理解的幾個之一。因為它難理解、難實現,應用它會導致代碼的可讀性、可維護性變差,所以,訪問者模式在實際的軟件開發中很少被用到,在沒有特別必要的情況下,建議你不要使用訪問者模式。
? ? ? ? 2、使用場景
? ? ? ? 2.1 抽象語法樹
????????訪問者模式在實際項目中的一個常見使用場景是處理抽象語法樹(AST)。例如,在編譯器或解釋器中,我們需要處理不同類型的語法結構,如聲明、表達式、循環等。 使用訪問者模式,我們可以將處理這些結構的功能與結構類分離,實現解耦和擴展性。
????????以下是一個簡單的示例,展示了如何使用訪問者模式處理抽象語法樹
// AST 節點基類
abstract class AstNode {// 接受訪問者的方法abstract void accept(AstVisitor visitor);
}
// 訪問者接口
interface AstVisitor {// 訪問表達式節點的方法void visit(ExpressionNode node);// 訪問數字節點的方法void visit(NumberNode node);// 訪問加法節點的方法void visit(AdditionNode node);
// 省略其他節點
}
// 數字節點,表示一個整數值
class NumberNode extends AstNode {int value;// 構造方法,接收一個整數作為值NumberNode(int value) {this.value = value;}// 實現基類的 accept 方法,接受訪問者void accept(AstVisitor visitor) {visitor.visit(this);}
}
// 加法節點,表示兩個子節點的相加
class AdditionNode extends AstNode {AstNode left;AstNode right;// 構造方法,接收兩個子節點AdditionNode(AstNode left, AstNode right) {this.left = left;this.right = right;}// 實現基類的 accept 方法,接受訪問者void accept(AstVisitor visitor) {visitor.visit(this);}
}
// 表達式節點,包含一個子節點
class ExpressionNode extends AstNode {AstNode node;// 構造方法,接收一個子節點ExpressionNode(AstNode node) {this.node = node;}// 實現基類的 accept 方法,接受訪問者void accept(AstVisitor visitor) {visitor.visit(this);}
}
class AstEvaluator implements AstVisitor {int result;AstEvaluator() {result = 0;}void visit(ExpressionNode node) {node.node.accept(this);}void visit(NumberNode node) {result = node.value;}void visit(AdditionNode node) {node.left.accept(this);int leftValue = result;node.right.accept(this);int rightValue = result;result = leftValue + rightValue;}
}
????????最后,我們可以使用這個訪問者類計算一個簡單的 AST
public class Main {public static void main(String[] args) {// 創建一個簡單的 AST:(2 + 3)AstNode ast = new ExpressionNode(new AdditionNode(new NumberNode(2),new NumberNode(3)));// 創建一個訪問者實例AstEvaluator evaluator = new AstEvaluator();// 使用訪問者計算 AST 的結果ast.accept(evaluator);// 輸出計算結果System.out.println("AST 的結果是: " + evaluator.result);}
}