State Pattern 是 OOP 中著名的 Design Pattern,當 method 的行為會隨著 field 而改變時特別有效。在本文中,我們將以 Angular 與 TypeScript 實現。
Version
macOS High Sierra 10.13.3 Node.js 8.9.4 Angular CLI 1.7.2 TypeScript 2.5.3 Angular 5.2.7
User Story
模擬 iPhone 的 Home
鍵動作
Home
雖然只有一個按鍵,但在不同的情境下有不同的功能
當在 Locked
下,可以 Unlocked
當在 Unlocked
下,可以進入 Home
當在 App
開啟下,可以回到 Home
當在 Desktop
下,可以切換 Home
Task
因為 Home
有在不同情境下有不同的功能,勢必有很多 if else
判斷情境而切換功能
先用 if else
寫法完成,最後再重構成 State Pattern
Definition
State Pattern
當 method 的行為會隨著 field 而改變時,將 if
改用 state
物件表示
將 外部 nested if
改由 內部決定
的 物件串列
表示,藉以消除 if
將 if
要處理的邏輯包在每個 state 內,但不包含 if
。
Client : Context
的 user,實務上可能是 component 或 controller
Context : 根據 field 的不同,request()
會有不同功能,實務上可能是 service
State : 定義 ConcreteState
的 interface,只有 handle()
,負責封裝 if 要處理的邏輯
ConcreteState : 將 if 要處理的邏輯
封裝成物件
一個 if
要處理的邏輯放在一個 ConcreteState
物件內,並根據 state diagram 指定下一個 state。
由於 method 的行為會根據 field (state) 改變,因此稱為 State
Pattern。
適用時機
深層 nested if
在 design-time 就可決定 if
組合 (以 state diagram 描述)
當 method 的行為會隨著 field 而改變時
優點
每個 if 要處理的邏輯
使用一個 class,符合 單一職責原則
將來若有新的 if 要處理的邏輯
,不用修改 service,而是新增 ConcreState
,符合 開放封閉原則
Client 與 if 判斷
解耦合,兩者都僅相依於 interface,符合 依賴反轉原則
可使用 state diagram 清楚描述狀態轉變
缺點
若邏輯複雜,state class 可能會很多檔案,但還是比單一檔案 nested if
好維護
Architecture
只有在 Unlock
、App
與 Desktop
才能使用 Home
鍵
只有在 Home
與 Desktop
才能切到 App
只有 Home
才能切到 Desktop
AppComponent
相當於 Client
PhoneContext
相當於 Context
,AppComponent
負責注入 PhoneContext
,無論怎麼重構,PhoneContext
都是穩定的,不會導致 AppComponent
修改
PhoneStateInterface
相當於 State
interface,訂出所有 state 標準
LockedState
及其他 state 都是 ConcreteState
,為實際 if
所處理的邏輯
Implmentation
app.component.html
1 2 3 4 5 6 7 <button (click )="onHomeClick()" > Home</button > <p > </p > <button (click )="onOpenAppClick()" > Open App</button > <p > </p > <button (click )="onSwitchDesktopClick()" > Switch Desktop</button > <p > </p > {{ message }}
在 HTML 提供 Home
、Open App
與 Switch Desktop
3 個 button,任何訊息將顯示在 message
。
If Else app.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import { Component } from '@angular/core' ;@Component({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent { private state = 'Locked' ; message = 'The phone is locked' ; onHomeClick() { if (this .state === 'Locked' ) { this .state = 'Unlocked' ; this .message = 'The phone is unlocked' ; } else if (this .state === 'Unlocked' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; } else if (this .state === 'App' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; } else if (this .state === 'Desktop' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; } else { this .state = 'Home' ; this .message = 'The phone is at home' ; } } onOpenAppClick() { if (this .state === 'Home' || this .state === 'Desktop' ) { this .state = 'App' ; this .message = 'The phone is opening app' ; } else { this .state = 'Null' ; this .message = 'The operation is not allowed' ; } } onSwitchDesktopClick() { if (this .state === 'Home' ) { this .state = 'Desktop' ; this .message = 'The phone is switching desktop' ; } else { this .state = 'Null' ; this .message = 'The operation is not allowed' ; } } }
12 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 onHomeClick() { if (this .state === 'Locked' ) { this .state = 'Unlocked' ; this .message = 'The phone is unlocked' ; } else if (this .state === 'Unlocked' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; } else if (this .state === 'App' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; } else if (this .state === 'Desktop' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; } else { this .state = 'Home' ; this .message = 'The phone is at home' ; } }
模擬 Home
鍵,由於 Home
在不同情境有不同功能,因此分成 Locked
、Unlocked
、Home
、App
與 Desktop
5 個 state,使用 if
判斷當目前什麼 state 下,要做什麼事情,以及即將切到什麼 state。
31 行
1 2 3 4 5 6 7 8 9 onOpenAppClick() { if (this .state === 'Home' || this .state === 'Desktop' ) { this .state = 'App' ; this .message = 'The phone is opening app' ; } else { this .state = 'Null' ; this .message = 'The operation is not allowed' ; } }
模擬 開啟 App
,根據 state diagram,由於不可能在 Locked
、Unlocked
與 App
state 下開啟 app,將顯示 The operation is not allowed
,並切到特別建立的 Null
state。
也就是只有 Home
與 Desktop
state 下才能開啟 app,因此特別使用 if else
做判斷。
41 行
1 2 3 4 5 6 7 8 9 onSwitchDesktopClick() { if (this .state === 'Home' ) { this .state = 'Desktop' ; this .message = 'The phone is switching desktop' ; } else { this .state = 'Null' ; this .message = 'The operation is not allowed' ; } }
模擬 切換桌面
,根據 state diagram,只能在 Home
state 下切換桌面,因此需要 if else
做判斷。
以功能面來說,目前已經完全需求,只是程式碼含有眾多的 code smell,尚無法達成 production code 的標準,需要繼續重構。
Unit Test 在重構之前,必須要有測試保護,才能確保沒把原本的商業邏輯重構壞,因此我們先準備好 AppComponent
的 Unit Test,確保每個 if else
的 path 都有測到。
app.component.spec.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import { ComponentFixture, TestBed } from '@angular/core/testing' ;import { AppComponent } from './app.component' ;import { DebugElement } from '@angular/core' ;describe('AppComponent' , () => { let fixture: ComponentFixture<AppComponent>; let appComponent: AppComponent; let debugElement: DebugElement; let htmlElement: HTMLElement; let target: AppComponent; beforeEach(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ] }); fixture = TestBed.createComponent(AppComponent); appComponent = fixture.componentInstance; debugElement = fixture.debugElement; htmlElement = debugElement.nativeElement; target = new AppComponent(); fixture.detectChanges(); }); it('should create the app' , () => { expect(appComponent).toBeTruthy(); }); it(`一開始應顯示 'The phone is locked' `, () => { expect(appComponent.message).toBe('The phone is locked' ); }); it(`當第 1 次按下 Home 應顯示 'The phone is unlocked' `, () => { appComponent.onHomeClick(); expect(appComponent.message).toBe('The phone is unlocked' ); }); it(`當第 2 次按下 Home 應顯示 'The phone is at home' `, () => { appComponent.onHomeClick(); appComponent.onHomeClick(); expect(appComponent.message).toBe('The phone is at home' ); }); it(`當第 3 次按下 Home 應顯示 'The phone is at home' `, () => { appComponent.onHomeClick(); appComponent.onHomeClick(); appComponent.onHomeClick(); expect(appComponent.message).toBe('The phone is at home' ); }); it(`當第 2 次按下 Home 與第一次按下 Open App 應顯示 'The phone is at home' `, () => { appComponent.onHomeClick(); appComponent.onHomeClick(); appComponent.onOpenAppClick(); expect(appComponent.message).toBe('The phone is opening app' ); }); it(`當按下 Open App 再按下 Home 應顯示 'The phone is at home' `, () => { appComponent.onHomeClick(); appComponent.onHomeClick(); appComponent.onOpenAppClick(); appComponent.onHomeClick(); expect(appComponent.message).toBe('The phone is at home' ); }); it(`當第 2 次按下 Home 與第一次按下 Switch Desktop 應顯示 'The phone is switching desktop' `, () => { appComponent.onHomeClick(); appComponent.onHomeClick(); appComponent.onSwitchDesktopClick(); expect(appComponent.message).toBe('The phone is switching desktop' ); }); it(`當按下 Switch Desktop 再按下 Home 應顯示 'The phone is switching desktop' `, () => { appComponent.onHomeClick(); appComponent.onHomeClick(); appComponent.onSwitchDesktopClick(); appComponent.onHomeClick(); expect(appComponent.message).toBe('The phone is at home' ); }); it(`當第 1 次按下 Home 再按下 Open App 應顯示 'The operation is not allowed' `, () => { appComponent.onHomeClick(); appComponent.onOpenAppClick(); expect(appComponent.message).toBe('The operation is not allowed' ); }); it(`當第 1 次按下 Home 再按下 Switch Desktop 應顯示 'The operation is not allowed' `, () => { appComponent.onHomeClick(); appComponent.onSwitchDesktopClick(); expect(appComponent.message).toBe('The operation is not allowed' ); }); });
由於本文重點不是在講 Unit Test,因此就不浪費篇幅解釋以上程式碼。
執行 Wallaby.js 單元測試,確保 AppComponent
所有的 if else
的 path 都有測到,也就是 coverage 是 100%
,接下來的重構將以此 Unit Test 為標準,無論怎麼重構,都必須確保 11 個測試案例 綠燈
。
實務上若 TDD 有困難,其實可以先用 if else
寫一段 比較髒
的 production code,最少功能都符合需求,因為 比較髒
,所以一定得重構,在重構之前補上 Unit Test,重點是 coverage 是 100%
,然後不斷的重構維持 Unit Test 都是 綠燈
,無論是先寫測試或是後寫測試,但要寫 Unit Test 與重構的目標都是一致的,只是先寫還是後寫而已。
若發現 比較髒
的 production code 已經無法寫出 Unit Test,就必須回頭修改 production code 寫法,那表示你的 production code 已經違反 SOLID 原則,導致 Unit Test 寫不出來。
Refactoring 目前這段 code 至少含有以下 3 項 code smell :
使用 else
造成 nested if
state
與 message
目前都以 string
hardcode
onHomeClick()
眾多 if else
嚴重違反 單一職責原則
與 開放封閉原則
我們將針對這 3 點加以重構。
Guard Clause 重構目標 : 使用 Guard Clause 取代 else
。
app.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import { Component } from '@angular/core' ;@Component({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent { private state = 'Locked' ; message = 'The phone is locked' ; onHomeClick() { if (this .state === 'Locked' ) { this .state = 'Unlocked' ; this .message = 'The phone is unlocked' ; return ; } if (this .state === 'Unlocked' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; return ; } if (this .state === 'App' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; return ; } if (this .state === 'Desktop' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; return ; } this .state = 'Home' ; this .message = 'The phone is at home' ; } onOpenAppClick() { if (this .state === 'Home' || this .state === 'Desktop' ) { this .state = 'App' ; this .message = 'The phone is opening app' ; return ; } this .state = 'Null' ; this .message = 'The operation is not allowed' ; } onSwitchDesktopClick() { if (this .state === 'Home' ) { this .state = 'Desktop' ; this .message = 'The phone is switching desktop' ; return ; } this .state = 'Null' ; this .message = 'The operation is not allowed' ; } }
25 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (this .state === 'App' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; return ; } if (this .state === 'Desktop' ) { this .state = 'Home' ; this .message = 'The phone is at home' ; return ; } this .state = 'Home' ;this .message = 'The phone is at home' ;
以 return
取代 else
,換來 if
全部壓平在第一層,避免寫出 nested if
。
最後一行即為預設值。
其他皆以這種方式重構。
Enum 重構目標 : 使用 enum
取代 string
hardcode。
message.enum.ts
1 2 3 4 5 6 7 8 export enum MessageEnum { Locked = 'The phone is locked' , Unlocked = 'The phone is unlocked' , Home = 'The phone is at home' , App = 'The phone is opening app' , Desktop = 'The phone is switching desktop' , Null = 'The operation is not allowed' }
之前 state
與 message
都使用 string
hardcode,使用上雖然直覺簡單,但有兩個致命缺點 :
message
到處散佈,將來若 message
修改,要改的地方很多
state
使用 string
,不僅容易 typo,也無法受到 compiler 保護
比較好的方式是 state
由 string
改由 enum
,如此不會 typo,也受到 compiler 保護。
一般語言由於 enum
傳統只能代表 int
,因此還掉搭配其他 collection,如 ES6 的 Map
或 C# 的 Dictionary
,但 TypeScript 2.4 的 enum
可代表 string
,因此可省掉 Map
或 Dictionary
,直接使用 enum
搞定。
app.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import { Component } from '@angular/core' ;import { MessageEnum } from './message.enum' ;@Component({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent { message = MessageEnum.Locked; onHomeClick() { if (this .message === MessageEnum.Locked) { this .message = MessageEnum.Unlocked; return ; } if (this .message === MessageEnum.Unlocked) { this .message = MessageEnum.Home; return ; } if (this .message === MessageEnum.App) { this .message = MessageEnum.Home; return ; } if (this .message === MessageEnum.Desktop) { this .message = MessageEnum.Home; return ; } this .message = MessageEnum.Home; } onOpenAppClick() { if (this .message === MessageEnum.Home || this .message === MessageEnum.Desktop) { this .message = MessageEnum.App; return ; } this .message = MessageEnum.Null; } onSwitchDesktopClick() { if (this .message === MessageEnum.Home) { this .message = MessageEnum.Desktop; return ; } this .message = MessageEnum.Null; } }
第 10 行
1 message = MessageEnum.Locked;
將 state
拿掉,只留下 message
。
13 行
1 2 3 4 if (this .message === MessageEnum.Locked) { this .message = MessageEnum.Unlocked; return ; }
原本判斷 state
,直接判斷 message
即可。
其他以此類推。
State Pattern 重構目標 : 使用 State Pattern 實現 單一職責原則
與 開放封閉原則
。
AppComponent
app.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { Component } from '@angular/core' ;import { MessageEnum } from './message.enum' ;import { PhoneContext } from './phone.context' ;import { AppState } from './AppState' ;import { DesktopState } from './DesktopState' ;@Component({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent { message = MessageEnum.Locked; constructor (private phoneContext: PhoneContext) { } onHomeClick() { this .message = this .phoneContext.request(); } onOpenAppClick() { this .message = this .phoneContext.setState(new AppState()); } onSwitchDesktopClick() { this .message = this .phoneContext.setState(new DesktopState()); } }
15 行
1 2 constructor (private phoneContext: PhoneContext) {}
使用 DI 依賴注入 PhoneContext
。
18 行
1 2 3 onHomeClick() { this .message = this .phoneContext.request(); }
onHomeClick()
只剩下一行 Context.request()
。
我們可以發現 onHomeClick()
的 if
都不見了,也就是說,State Pattern 用多個檔案的 class 去換一個檔案的 if
。
由於多個檔案,一個 class 代表一個 if
,符合 單一職責原則
將來若有新的 state,只要根據 interface 新增 class,不用修改原來的 code,符合 開放封閉原則
22 行
1 2 3 onOpenAppClick() { this .message = this .phoneContext.setState(new AppState()); }
原本需要在 client 判斷是否可以切換 state,現在只管新增 state 即可,不用再用 if
判斷。
PhoneContext
phone.context.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { Inject, Injectable } from '@angular/core' ;import { MessageEnum } from './message.enum' ;import { PhoneStateInterface } from './phone.state.interface' ;import { PhoneStateInterfaceToken } from './interface.token' ;@Injectable() export class PhoneContext { constructor (@Inject(PhoneStateInterfaceToken) private state: PhoneStateInterface) { } request(): MessageEnum { this .state = this .state.handle(); return this .state.getMessage(); } setState(state: PhoneStateInterface): MessageEnum { if (!state.chkContext(this .state)) { return MessageEnum.Null; } else { this .state = state; return state.getMessage(); } } }
第 9 行
1 2 constructor (@Inject(PhoneStateInterfaceToken) private state: PhoneStateInterface) {}
即將使用 State Pattern,使用 DI 依賴注入 state
,注意其型別為 PhoneStateInterface
。
12 行
1 2 3 4 request(): MessageEnum { this .state = this .state.handle(); return this .state.getMessage(); }
request()
為原本 State Pattern 所設計的 method。
執行目前 state
的 handle()
,此為目前 state 主要的商業邏輯所在。
handle()
將回傳下一個 state
。
state.getMessage()
將回傳目前 state
要顯示的訊息。
17 行
1 2 3 4 5 6 7 8 setState(state: PhoneStateInterface): MessageEnum { if (!state.chkContext(this .state)) { return MessageEnum.Null; } else { this .state = state; return state.getMessage(); } }
setState()
並非 State Pattern 所設計的 method,目前因為 Open App
與 Switch Desktop
需要動態切換 state,因而衍生出動態設定 state 的需求。
由於不是每個 state 都能任意切換到其他 state,因此特別在每個 state 設計 chkContext()
,判斷是否允許切換 state。
Context.setState()
與 State.chkContext()
並非原始 State Pattern 所設計,為了能動態切換 state,實務上經常會有此需求
PhoneStateInterface
phone.state.interface.ts
1 2 3 4 5 6 7 import { MessageEnum } from './message.enum' ;export interface PhoneStateInterface { handle(): PhoneStateInterface; getMessage(): MessageEnum; chkContext(state: PhoneStateInterface): boolean ; }
定義 State Pattern 的 interface :
handle()
: 為 State Pattern 所設計的 method,專門放每個 if
的邏輯
getMessage()
: 回傳目前 state 的 message,非 State Pattern 所設計,為根據目前需求所設計的 method
chkContext()
: 檢查目前 state 是否可以允許切換到下一個 state,非 State Pattern 所設計,為根據目前需求所設計的 method
LockedState
接下來要將每個 state 的 if
搬進 class,實現 單一職責原則
與 開放封閉原則
。
locked.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;import { UnlockedState } from './UnlockedState' ;export class LockedState implements PhoneStateInterface { handle(): PhoneStateInterface { return new UnlockedState(); } getMessage(): MessageEnum { return MessageEnum.Locked; } chkContext(state: PhoneStateInterface): boolean { return true ; } }
第 6 行
1 2 3 handle(): PhoneStateInterface { return new UnlockedState(); }
將原本 Locked
state 的 code 搬到 handle()
,最後回傳下一個 state,其型別為 PhoneStateInterface
。
10 行
1 2 3 getMessage(): MessageEnum { return MessageEnum.Locked; }
回傳目前 state 的 message,注意其型別為 MessageEnum
,而不是 string
。
14 行
1 2 3 chkContext(state: PhoneStateInterface): boolean { return true ; }
檢查目前 state 是否進進入 Locked
state,因為毫無限制,傳回 true
即可。
UnlockedState
unlocked.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;import { HomeState } from './HomeState' ;export class UnlockedState implements PhoneStateInterface { getMessage(): MessageEnum { return MessageEnum.Unlocked; } chkContext(state: PhoneStateInterface): boolean { return state.getMessage() === MessageEnum.Locked; } handle(): PhoneStateInterface { return new HomeState(); } }
10 行
1 2 3 chkContext(state: PhoneStateInterface): boolean { return state.getMessage() === MessageEnum.Locked; }
根據 state diagram,只有當 Locked
state 才能進入 Unlocked
state。
HomeState
home.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;export class HomeState implements PhoneStateInterface { getMessage(): MessageEnum { return MessageEnum.Home; } chkContext(state: PhoneStateInterface): boolean { return true ; } handle(): PhoneStateInterface { return new HomeState(); } }
與 LockedState
類似,就不再贅述。
AppState
app.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;import { HomeState } from './HomeState' ;export class AppState implements PhoneStateInterface { getMessage(): MessageEnum { return MessageEnum.App; } chkContext(state: PhoneStateInterface): boolean { if (state.getMessage() === MessageEnum.Home) { return true ; } if (state.getMessage() === MessageEnum.Desktop) { return true ; } return false ; } handle(): PhoneStateInterface { return new HomeState(); } }
10 行
1 2 3 4 5 6 7 8 9 10 11 chkContext(state: PhoneStateInterface): boolean { if (state.getMessage() === MessageEnum.Home) { return true ; } if (state.getMessage() === MessageEnum.Desktop) { return true ; } return false ; }
根據 state diagram,只有 Home
與 Desktop
state 才能進入 App
state。
DesktopState
desktop.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;import { HomeState } from './home.state' ;export class DesktopState implements PhoneStateInterface { getMessage(): MessageEnum { return MessageEnum.Desktop; } chkContext(state: PhoneStateInterface): boolean { if (state.getMessage() === MessageEnum.Home) { return true ; } return false ; } handle(): PhoneStateInterface { return new HomeState(); } }
根據 state diagram,只有 Home
state 才能進入 Desktop
state。
Refactoring PhoneContext
phone.context.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { Inject, Injectable } from '@angular/core' ;import { MessageEnum } from './message.enum' ;import { PhoneStateInterface } from './phone.state.interface' ;import { PhoneStateInterfaceToken } from './interface.token' ;@Injectable() export class PhoneContext { constructor (@Inject(PhoneStateInterfaceToken) private state: PhoneStateInterface) { } request(): MessageEnum { this .state = this .state.handle(); return this .state.getMessage(); } setState(state: PhoneStateInterface): MessageEnum { const setCurrentState = (currentState) => { this .state = currentState; return currentState.getMessage(); }; return !state.chkContext(this .state) ? MessageEnum.Null : setCurrentState(state); } }
17 行
1 2 3 4 5 6 7 8 9 setState(state: PhoneStateInterface): MessageEnum { const setCurrentState = (currentState) => { this .state = currentState; return currentState.getMessage(); }; return !state.chkContext(this .state) ? MessageEnum.Null : setCurrentState(state); }
將原本的 if else
重構成 ?:
。
尤其原本 else
的部分抽成 function,讓可讀性更高。
AppState
app.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;import { HomeState } from './home.state' ;export class AppState implements PhoneStateInterface { getMessage(): MessageEnum { return MessageEnum.App; } chkContext(state: PhoneStateInterface): boolean { return state.getMessage() === MessageEnum.Home ? true : state.getMessage() === MessageEnum.Desktop; } handle(): PhoneStateInterface { return new HomeState(); } }
10 行
1 2 3 4 chkContext(state: PhoneStateInterface): boolean { return state.getMessage() === MessageEnum.Home ? true : state.getMessage() === MessageEnum.Desktop; }
將 if else
重構成 ?:
。
DesktopState
destop.state.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { PhoneStateInterface } from './phone.state.interface' ;import { MessageEnum } from './message.enum' ;import { HomeState } from './home.state' ;export class DesktopState implements PhoneStateInterface { getMessage(): MessageEnum { return MessageEnum.Desktop; } chkContext(state: PhoneStateInterface): boolean { return state.getMessage() === MessageEnum.Home; } handle(): PhoneStateInterface { return new HomeState(); } }
10 行
1 2 3 chkContext(state: PhoneStateInterface): boolean { return state.getMessage() === MessageEnum.Home; }
將 if else
重構成 ?:
。
經過一系列的重構成 State Pattern,一樣必須確認 Wallaby.js 單元測試的 11 個測試案例都是 綠燈
。
Summary
Chain of Responsibility Pattern vs. State Pattern
以 class diagram 角度,Chain of Responsibility 與 State Pattern 完全一樣。
學 Design Pattern 不能以 class diagram 的角度去思考,而要以他要解決什麼問題來思考
Similarity
解決 nested if
難以維護
都是將 if
改用 object 表示
Difference
CoR 特別適用於一連串的 判斷檢查
;State 特別適用於 method 功能隨 field 改變
CoR 內仍然會有 if
;State 內無 if
CoR 由 外部
決定下一個 handler;State 由 內部
決定下一個 state
State 會搭配 state diagam 描述 state 的切換
Conclusion
並不是所有的 if
都該使用 State Pattern,當 method 功能會隨 field 改變時特別適用,如複雜的 GUI,button 可能根據不同情境有不同功能
Chain of Responsibility 與 State Pattern,從 class diagram 角度,兩者完全一樣,但適用時機與語意是不同的,尤其 Chain of Responsibility 是由外部決定 if
順序,但 State Pattern 是由內部決定 if
順序
Sample Code
完整的範例可以在我的 GitHub 上找到