1. 前文匯總
2. LOL 中的狀態
感覺我天天在用 LOL 舉例子,沒辦法,都已經 S11 了,而我依然在玩這個游戲。
LOL 中的英雄有很多狀態,有正常狀態,有吃了偉哥一樣的加速狀態,有被對方套了虛弱的虛弱狀態,還有被對方控制的眩暈狀態。
下面來看下,在 LOL 中,初始的英雄狀態:
public class Hero {
//正常狀態
public static final int COMMON = 1;
//加速狀態
public static final int SPEED_UP = 2;
//減速狀態
public static final int SPEED_DOWN = 3;
//眩暈狀態
public static final int SWIM = 4;
//默認是正常狀態
private int state = COMMON;
//跑動線程
private Thread runThread;
//設置狀態
public void setState(int state) {
this.state = state;
}
//停止跑動
public void stopRun() {
if (isRunning()) runThread.interrupt();
System.out.println("--------------停止跑動---------------");
}
//開始跑動
public void startRun() {
if (isRunning()) {
return;
}
final Hero hero = this;
runThread = new Thread(new Runnable() {
public void run() {
while (!runThread.isInterrupted()) {
try {
hero.run();
} catch (InterruptedException e) {
break;
}
}
}
});
System.out.println("--------------開始跑動---------------");
runThread.start();
}
private boolean isRunning(){
return runThread != null && !runThread.isInterrupted();
}
//英雄類開始奔跑
private void run() throws InterruptedException{
if (state == SPEED_UP) {
System.out.println("--------------加速跑動---------------");
Thread.sleep(2000);//假設加速持續2秒
state = COMMON;
System.out.println("------加速狀態結束,變為正常狀態------");
}else if (state == SPEED_DOWN) {
System.out.println("--------------減速跑動---------------");
Thread.sleep(2000);//假設減速持續2秒
state = COMMON;
System.out.println("------減速狀態結束,變為正常狀態------");
}else if (state == SWIM) {
System.out.println("--------------不能跑動---------------");
Thread.sleep(1000);//假設眩暈持續2秒
state = COMMON;
System.out.println("------眩暈狀態結束,變為正常狀態------");
}else {
//正常跑動則不打印內容
}
}
}
場景類:
public class Client {
public static void main(String[] args) throws InterruptedException {
Hero hero = new Hero();
hero.startRun();
hero.setState(Hero.SPEED_UP);
Thread.sleep(2000);
hero.setState(Hero.SPEED_DOWN);
Thread.sleep(2000);
hero.setState(Hero.SWIM);
Thread.sleep(2000);
hero.stopRun();
}
}
可以看到,我們的英雄在跑動過程中隨着狀態的改變,我們的英雄會以不同的狀態進行跑動。
但是問題也隨之而來,我們的英雄類當中有明顯的 if else 結構,這並不是我們希望看到的,接下來,我們看下狀態模式。
3. 狀態模式
3.1 定義
狀態模式的定義如下:
Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.(當一個對象內在狀態改變時允許其改變行為, 這個對象看起來像改變了其類。)
3.2 通用類圖
- State 抽象狀態角色:接口或抽象類, 負責對象狀態定義, 並且封裝環境角色以實現狀態切換。
- ConcreteState 具體狀態角色:每一個具體狀態必須完成兩個職責: 本狀態的行為管理以及趨向狀態處理, 通俗地說,就是本狀態下要做的事情, 以及本狀態如何過渡到其他狀態。
- Context 環境角色:定義客戶端需要的接口, 並且負責具體狀態的切換。
狀態模式從類圖上看比較簡單,實際上還是比較復雜的,它提供了一種對物質運動的另一個觀察視角, 通過狀態變更促使行為的變化。
類似水的狀態變更一樣, 一碗水的初始狀態是液態, 通過加熱轉變為、氣態, 狀態的改變同時也引起體積的擴大, 然后就產生了一個新的行為: 鳴笛或頂起壺蓋,瓦特就是這么發明蒸汽機的。
3.3 通用代碼:
抽象環境角色:
public abstract class State {
// 定義一個環境角色,提供子類訪問
protected Context context;
// 設置環境資源
public void setContext(Context context) {
this.context = context;
}
// 行為1
abstract void handle1();
// 行為2
abstract void handle2();
}
具體環境角色:
public class ConcreteState1 extends State {
@Override
void handle1() {
//本狀態下必須處理的邏輯
}
@Override
void handle2() {
//設置當前狀態為stat2
super.context.setCurrentState(Context.STATE2);
//過渡到state2狀態, 由Context實現
super.context.handle2();
}
}
public class ConcreteState2 extends State {
@Override
void handle1() {
//設置當前狀態為stat2
super.context.setCurrentState(Context.STATE1);
//過渡到state2狀態, 由Context實現
super.context.handle1();
}
@Override
void handle2() {
// 本狀態下必須處理的邏輯
}
}
具體環境角色:
public class Context {
final static State STATE1 = new ConcreteState1();
final static State STATE2 = new ConcreteState2();
private State concreteState;
public State getCurrentState() {
return concreteState;
}
//設置當前狀態
public void setCurrentState(State currentState) {
this.concreteState = currentState;
//切換狀態
this.concreteState.setContext(this);
}
public void handle1(){
this.concreteState.handle1();
}
public void handle2(){
this.concreteState.handle2();
}
}
環境角色有兩個不成文的約束:
- 把狀態對象聲明為靜態常量, 有幾個狀態對象就聲明幾個靜態常量。
- 環境角色具有狀態抽象角色定義的所有行為, 具體執行使用委托方式。
public class Client {
public static void main(String[] args) {
//定義環境角色
Context context = new Context();
//初始化狀態
context.setCurrentState(new ConcreteState1());
//行為執行
context.handle1();
context.handle2();
}
}
這里我們已經隱藏了狀態的變化過程, 它的切換引起了行為的變化。 對外來說, 我們只看到行為的發生改變, 而不用知道是狀態變化引起的。
3.4 優點
- 避免了過多的 if else 語句的使用,避免了程序的復雜性,提高系統的可維護性。
- 使用多態代替了條件判斷,這樣我們代碼的擴展性更強,比如要增加一些狀態,會非常的容易。
- 狀態是可以被共享的,狀態都是由 static final 進行修飾的。
3.5 缺點
有優點的同事也會產生缺點,有時候,優點和缺點的產生其實是同一個事實:
狀態模式最主要的一個缺點是:子類會太多,也就是類膨脹。因為一個事物有很多個狀態也不稀奇,如果完全使用狀態模式就會有太多的子類,不好管理。
4. 案例完善
前面那個 LOL 的例子,如果使用狀態模式重寫一下,會是這樣的:
首先創建一個跑動的接口:
public interface RunState {
void run(Hero hero);
}
接下來是4個實現類,分別實現不同狀態的跑動結果:
public class CommonState implements RunState {
@Override
public void run(Hero hero) {
// 正常跑動則不打印內容,否則會刷屏
}
}
public class SpeedUpState implements RunState{
@Override
public void run(Hero hero) {
System.out.println("--------------加速跑動---------------");
try {
Thread.sleep(2000);//假設加速持續2秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------加速狀態結束,變為正常狀態------");
}
}
public class SpeedDownState implements RunState{
@Override
public void run(Hero hero) {
System.out.println("--------------減速跑動---------------");
try {
Thread.sleep(2000);//假設減速持續2秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------減速狀態結束,變為正常狀態------");
}
}
public class SwimState implements RunState {
@Override
public void run(Hero hero) {
System.out.println("--------------不能跑動---------------");
try {
Thread.sleep(1000);//假設眩暈持續1秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------眩暈狀態結束,變為正常狀態------");
}
}
最后是一個 Hero(Context) 類:
public class Hero {
public static final RunState COMMON = new CommonState();//正常狀態
public static final RunState SPEED_UP = new SpeedUpState();//加速狀態
public static final RunState SPEED_DOWN = new SpeedDownState();//減速狀態
public static final RunState SWIM = new SwimState();//眩暈狀態
private RunState state = COMMON;//默認是正常狀態
private Thread runThread;//跑動線程
//設置狀態
public void setState(RunState state) {
this.state = state;
}
//停止跑動
public void stopRun() {
if (isRunning()) runThread.interrupt();
System.out.println("--------------停止跑動---------------");
}
//開始跑動
public void startRun() {
if (isRunning()) {
return;
}
final Hero hero = this;
runThread = new Thread(new Runnable() {
public void run() {
while (!runThread.isInterrupted()) {
state.run(hero);
}
}
});
System.out.println("--------------開始跑動---------------");
runThread.start();
}
private boolean isRunning(){
return runThread != null && !runThread.isInterrupted();
}
}
可以看到,這段代碼和開頭那段代碼雖然完成了一樣的功能,但是整個代碼的復雜度缺以肉眼可見的級別提高了,一般而言,我們犧牲復雜性去換取的高可維護性和擴展性是相當值得的,除非增加了復雜性以后,對於后者的提升會乎其微。