電梯具有的動作:
- 開門:乘客進入鲁捏、出去芯砸。
- 關(guān)門:電梯準備開始運行。
- 運行:上下運行给梅。
- 停止:停止運行假丧。
1、先讓電梯運行起來
//定義電梯接口
public interface ILift {
void open();
void close();
void run();
void stop();
}
//電梯實現(xiàn)類
public class LiftImp implements ILift{
@Override
public void open() {
System.out.println("電梯開門了……");
}
@Override
public void close() {
System.out.println("電梯關(guān)門了……");
}
@Override
public void run() {
System.out.println("電梯運行起來了……");
}
@Override
public void stop() {
System.out.println("電梯停止了……");
}
}
思考:這個程序有什么問題动羽?
電梯門可以打開包帚,但不是隨時都可以開,是有前提條件的运吓。你不可能電梯在運行的時候突然開門吧渴邦?疯趟!電梯也不會出現(xiàn)停止了但是不開門的情況吧?谋梭!那要是有也是事故信峻。再仔細想想,電梯的這4個動作的執(zhí)行都有前置條件瓮床,具體點說就是在特定狀態(tài)下才能做特定事盹舞,那我們來分析一下電梯有哪些特定狀態(tài)。
- 敞門狀態(tài):按了電梯上下按鈕隘庄,電梯門開踢步,這中間大概有10秒的時間,那就是敞門狀態(tài)丑掺。在這個狀態(tài)下電梯只能做的動作是關(guān)門動作获印。
- 閉門狀態(tài):電梯門關(guān)閉了,在這個狀態(tài)下吼鱼,可以進行的動作是:開門(我不想坐電梯了)蓬豁、停止(忘記按路層號了)、運行菇肃。
- 運行狀態(tài):電梯正在跑地粪,上下竄,在這個狀態(tài)下琐谤,電梯只能做的是停止蟆技。
- 停止狀態(tài):電梯停止不動,在這個狀態(tài)下斗忌,電梯有兩個可選動作:繼續(xù)運行和開門動作质礼。
我們用一張表來表示電梯狀態(tài)和動作之間的關(guān)系:
2、修復(fù)一下電梯的異常
//接口中增加了狀態(tài)
public interface ILift2 {
public final static int OPENING_STATE = 1; //敞門狀態(tài)
public final static int CLOSING_STATE = 2; //閉門狀態(tài)
public final static int RUNNING_STATE = 3; //運行狀態(tài)
public final static int STOPPING_STATE = 4; //停止狀態(tài)
void open();
void close();
void run();
void stop();
}
public class LiftImp2 implements ILift2 {
private int state;
public void setState(int state) {
this.state = state;
}
@Override
public void open() {
//電梯在什么狀態(tài)才能開啟
switch (this.state) {
case OPENING_STATE: //閉門狀態(tài)织阳,什么都不做
//do nothing;
break;
case CLOSING_STATE: //閉門狀態(tài)眶蕉,則可以開啟
System.out.println("電梯開門了……");
this.setState(OPENING_STATE);
break;
case RUNNING_STATE: //運行狀態(tài),則不能開門唧躲,什么都不做
//do nothing;
break;
case STOPPING_STATE: //停止狀態(tài)造挽,當然要開門了
System.out.println("電梯開門了……");
this.setState(OPENING_STATE);
break;
}
}
@Override
public void close() {
//電梯在什么狀態(tài)下才能關(guān)閉
switch (this.state) {
case OPENING_STATE: //可以關(guān)門,同時修改電梯狀態(tài)
System.out.println("電梯關(guān)門了……");
this.setState(CLOSING_STATE);
break;
case CLOSING_STATE: //電梯是關(guān)門狀態(tài)弄痹,則什么都不做
//do nothing;
break;
case RUNNING_STATE: //正在運行饭入,門本來就是關(guān)閉的,也什么都不做
//do nothing;
break;
case STOPPING_STATE: //停止狀態(tài)肛真,門也是關(guān)閉的谐丢,什么也不做
//do nothing;
break;
}
}
@Override
public void run() {
switch (this.state) {
case OPENING_STATE: //敞門狀態(tài),什么都不做
//do nothing;
break;
case CLOSING_STATE: //閉門狀態(tài),則可以運行
System.out.println("電梯運行起來了……");
this.setState(RUNNING_STATE);
break;
case RUNNING_STATE: //運行狀態(tài)乾忱,則什么都不做
//do nothing;
break;
case STOPPING_STATE: //停止狀態(tài)讥珍,可以運行
System.out.println("電梯運行起來了……");
this.setState(RUNNING_STATE);
}
}
@Override
public void stop() {
switch (this.state) {
case OPENING_STATE: //敞門狀態(tài),要先停下來的饭耳,什么都不做
//do nothing;
break;
case CLOSING_STATE: //閉門狀態(tài)串述,則當然可以停止了
System.out.println("電梯停止了……");
this.setState(CLOSING_STATE);
break;
case RUNNING_STATE: //運行狀態(tài),有運行當然那也就有停止了
System.out.println("電梯停止了……");
this.setState(CLOSING_STATE);
break;
case STOPPING_STATE: //停止狀態(tài)寞肖,什么都不做
//do nothing;
break;
}
}
}
思考:這個程序有什么問題纲酗?
-
電梯實現(xiàn)類Lift有點長
長的原因是我們在程序中使用了大量的switch...case這樣的判斷(if...else也是一樣),程序中只要有這樣的判斷就避免不了加長程序新蟆,而且在業(yè)務(wù)復(fù)雜的情況下觅赊,程序會更長,這就不是一個很好的習慣了琼稻,較長的方法和類無法帶來良好的維護性吮螺,畢竟,程序首先是給人閱讀的帕翻,然后才是機器執(zhí)行鸠补。
-
擴展性非常差勁
大家來想想,電梯還有兩個狀態(tài)沒有加嘀掸,是什么紫岩?通電狀態(tài)和斷電狀態(tài),你要是在程序增加這兩個方法睬塌,你看看Open()泉蝌、Close()、Run()揩晴、Stop()這4個方法都要增加判斷條件勋陪,也就是說switch判斷體中還要增加case項,這與開閉原則相違背硫兰。
-
非常規(guī)狀態(tài)無法實現(xiàn)
我們來思考我們的業(yè)務(wù)诅愚,電梯在門敞開狀態(tài)下就不能上下運行了嗎?電梯有沒有發(fā)生過只有運行沒有停止狀態(tài)呢(從40層直接墜到1層嘛)劫映?電梯故障嘛呻粹,還有電梯在檢修的時候,可以在stop狀態(tài)下不開門苏研,這也是正常的業(yè)務(wù)需求呀,你想想看腮郊,如果加上這些判斷條件摹蘑,上面的程序有多少需要修改?雖然這些都是電梯的業(yè)務(wù)邏輯轧飞,但是一個類有且僅有一個原因引起類的變化衅鹿,單一職責原則撒踪,看看我們的類,業(yè)務(wù)任務(wù)上一個小小的增加或改動都使得我們這個電梯類產(chǎn)生了修改大渤,這在項目開發(fā)上是有很大風險的制妄。
如何解決這些問題?剛剛我們都是從電梯的執(zhí)行方法來分析的泵三,換一個角度:狀態(tài)耕捞!可以將電梯的運行抽解成兩個任務(wù)模型:
- 當前狀態(tài)如何來的?比如停止狀態(tài)烫幕,肯定是執(zhí)行了stop方法來的俺抽。
- 在當前狀態(tài)下能執(zhí)行哪些動作?比如停止狀態(tài)下较曼,能執(zhí)行開門磷斧、運行。
3捷犹、讓電梯完美的運行起來
在類圖中弛饭,定義了一個LiftState抽象類,聲明了一個受保護的類型Context變量萍歉,這個是串聯(lián)各個狀態(tài)的封裝類侣颂。封裝的目的很明顯,就是電梯對象內(nèi)部狀態(tài)的變化不被調(diào)用類知曉翠桦,也就是迪米特法則了(我的類內(nèi)部情節(jié)你知道得越少越好)横蜒,并且還定義了4個具體的實現(xiàn)類,承擔的是狀態(tài)的產(chǎn)生以及狀態(tài)間的轉(zhuǎn)換過渡销凑,每個實現(xiàn)類都有open丛晌、close、run斗幼、stop四個方法澎蛛。
對于context我們可以這樣理解:Context是一個環(huán)境角色,它的作用是串聯(lián)各個狀態(tài)的過渡蜕窿,在LiftSate抽象類中我們定義并把這個環(huán)境角色聚合進來谋逻,并傳遞到子類,也就是4個具體的實現(xiàn)類中自己根據(jù)環(huán)境來決定如何進行狀態(tài)的過渡桐经。
對于LiftState實現(xiàn)類中的方法毁兆,以O(shè)penningState為例進行解釋:Openning狀態(tài)是由open()方法產(chǎn)生的,因此阴挣,在這個方法中有一個具體的業(yè)務(wù)邏輯气堕,我們是用print來代替了。在Openning狀態(tài)下,電梯能過渡到其他什么狀態(tài)呢茎芭?按照現(xiàn)在的定義的是只能過渡到Closing狀態(tài)揖膜,因此我們在Close()中定義了狀態(tài)變更,同時把Close這個動作也委托了給CloseState類下的Close方法執(zhí)行梅桩,
public abstract class LiftState {
//定義一個環(huán)境角色壹粟,也就是封裝狀態(tài)的變化引起的功能變化
protected Context context;
public void setContext(Context _context){
this.context = _context;
}
//首先電梯門開啟動作
public abstract void open();
//電梯門有開啟,那當然也就有關(guān)閉了
public abstract void close();
//電梯要能上能下宿百,運行起來
public abstract void run();
//電梯還要能停下來
public abstract void stop();
}
public class Context {
//定義出所有的電梯狀態(tài)
public final static OpenningState openningState = new OpenningState();
public final static ClosingState closeingState = new ClosingState();
public final static RunningState runningState = new RunningState();
public final static StoppingState stoppingState = new StoppingState();
//定義一個當前電梯狀態(tài)
private LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
//把當前的環(huán)境通知到各個實現(xiàn)類中
this.liftState.setContext(this);
}
public void open(){
this.liftState.open();
}
public void close(){
this.liftState.close();
}
public void run(){
this.liftState.run();
}
public void stop(){
this.liftState.stop();
}
}
public class OpenningState extends LiftState {
//開啟當然可以關(guān)閉了趁仙,我就想測試一下電梯門開關(guān)功能
@Override
public void close() {
//狀態(tài)修改
super.context.setLiftState(Context.closeingState);
//動作委托為CloseState來執(zhí)行
super.context.getLiftState().close();
}
//打開電梯門
@Override
public void open() {
System.out.println("電梯門開啟...");
}
//門開著時電梯就運行跑,這電梯犀呼,嚇死你幸撕!
@Override
public void run() {
//do nothing;
}
//開門還不停止?
public void stop() {
//do nothing;
}
}
public class ClosingState extends LiftState{
//電梯門關(guān)閉外臂,這是關(guān)閉狀態(tài)要實現(xiàn)的動作
@Override
public void close() {
System.out.println("電梯門關(guān)閉...");
}
//電梯門關(guān)了再打開
@Override
public void open() {
super.context.setLiftState(Context.openningState); //置為敞門狀態(tài)
super.context.getLiftState().open();
}
//電梯門關(guān)了就運行坐儿,這是再正常不過了
@Override
public void run() {
super.context.setLiftState(Context.runningState); //設(shè)置為運行狀態(tài)
super.context.getLiftState().run();
}
//電梯門關(guān)著,我就不按樓層
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState); //設(shè)置為停止狀態(tài)
super.context.getLiftState().stop();
}
}
public class RunningState extends LiftState {
//電梯門關(guān)閉宋光?這是肯定的
@Override
public void close() {
//do nothing
}
//運行的時候開電梯門貌矿?你瘋了!電梯不會給你開的
@Override
public void open() {
//do nothing
}
//這是在運行狀態(tài)下要實現(xiàn)的方法
@Override
public void run() {
System.out.println("電梯上下運行...");
}
//這絕對是合理的罪佳,只運行不停止還有誰敢坐這個電梯逛漫?!估計只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);//環(huán)境設(shè)置為停止狀態(tài)
super.context.getLiftState().stop();
}
}
public class StoppingState extends LiftState {
//停止狀態(tài)關(guān)門赘艳?電梯門本來就是關(guān)著的酌毡!
@Override
public void close() {
//do nothing;
}
//停止狀態(tài),開門蕾管,那是要的枷踏!
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.getLiftState().open();
}
//停止狀態(tài)再運行起來,正常得很
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.getLiftState().run();
}
//停止狀態(tài)是怎么發(fā)生的呢掰曾?當然是停止方法執(zhí)行了
@Override
public void stop() {
System.out.println("電梯停止了...");
}
}
總結(jié)一下:
代碼太長的問題:通過各個子類來實現(xiàn)旭蠕,每個子類的代碼都很短,而且也取消了switch...case條件的判斷旷坦。
不符合開閉原則:如果在我們這個例子中要增加兩個狀態(tài)應(yīng)該怎么做呢掏熬?增加兩個子類,一個是通電狀態(tài)秒梅,另一個是斷電狀態(tài)旗芬,同時修改其他實現(xiàn)類的相應(yīng)方法,因為狀態(tài)要過渡捆蜀,那當然要修改原有的類岗屏,只是在原有類中的方法上增加辆琅,而不去做修改。
不符合迪米特法則:我們現(xiàn)在的各個狀態(tài)是單獨的類这刷,只有與這個狀態(tài)有關(guān)的因素修改了,這個類才修改娩井,符合迪米特法則暇屋。
非常完美!這就是狀態(tài)模式。
4洞辣、總結(jié)
1咐刨、狀態(tài)模式的定義:
當一個對象內(nèi)在狀態(tài)改變時允許其改變行為,這個對象看起來像改變了其類扬霜。(Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.)
狀態(tài)模式的核心是封裝定鸟,狀態(tài)的變更引起了行為的變更,從外部看起來就好像這個對象對應(yīng)的類發(fā)生了改變一樣著瓶。
2联予、狀態(tài)模式中的3個角色:
- State——抽象狀態(tài)角色:接口或抽象類,負責對象狀態(tài)定義材原,并且封裝環(huán)境角色以實現(xiàn)狀態(tài)切換沸久。
- ConcreteState——具體狀態(tài)角色:每一個具體狀態(tài)必須完成兩個職責:本狀態(tài)的行為管理以及趨向狀態(tài)處理,通俗地說余蟹,就是本狀態(tài)下要做的事情卷胯,以及本狀態(tài)如何過渡到其他狀態(tài)。
- Context——環(huán)境角色:定義客戶端需要的接口威酒,并且負責具體狀態(tài)的切換窑睁。
3、狀態(tài)模式的優(yōu)點
- 結(jié)構(gòu)清晰:避免了過多的switch...case或者if...else語句的使用葵孤,避免了程序的復(fù)雜性,提高系統(tǒng)的可維護性担钮。
- 遵循設(shè)計原則:很好地體現(xiàn)了開閉原則和單一職責原則,每個狀態(tài)都是一個子類佛呻,你要增加狀態(tài)就要增加子類裳朋,你要修改狀態(tài),你只修改一個子類就可以了吓著。
- 封裝性非常好:這也是狀態(tài)模式的基本要求鲤嫡,狀態(tài)變換放置到類的內(nèi)部來實現(xiàn),外部的調(diào)用不用知道類內(nèi)部如何實現(xiàn)狀態(tài)和行為的變換绑莺。
4暖眼、狀態(tài)模式的缺點
子類會太多,也就是類膨脹纺裁。如果一個事物有很多個狀態(tài)也不稀奇诫肠,如果完全使用狀態(tài)模式就會有太多的子類司澎,不好管理,這個需要大家在項目中自己衡量栋豫。
5挤安、狀態(tài)模式的使用場景
行為隨狀態(tài)改變而改變的場景
這也是狀態(tài)模式的根本出發(fā)點,例如權(quán)限設(shè)計丧鸯,人員的狀態(tài)不同即使執(zhí)行相同的行為結(jié)果也會不同蛤铜,在這種情況下需要考慮使用狀態(tài)模式。條件丛肢、分支判斷語句的替代者
在程序中大量使用switch語句或者if判斷語句會導致程序結(jié)構(gòu)不清晰围肥,邏輯混亂,使用狀態(tài)模式可以很好地避免這一問題蜂怎,它通過擴展子類實現(xiàn)了條件的判斷處理穆刻。
參考書籍《設(shè)計模式之禪》:https://www.kancloud.cn/sstd521/design/193608