前言
在最近寫項目的時候,涉及到一個需求,在途貨物模塊,貨物的狀態有四種,分別是待發,在途,到港,完成。這四個狀態的關係是,由待發狀態切換為在途狀態時,需要增加字段比如裝運日期字段,同時切換為在途狀態後就不能切換為待發狀態,其他的狀態也是這個思想。起初寫的時候按照以前的思路,需要切換什麼狀態,我就對應寫一個方法,但是這樣面臨一個問題,判斷的狀態越來越多且寫在一整個類中,太過於冗雜,查找起來較為困難,如果再增加多個狀態就是非常災難的。聽潘老師的建議後,選擇用狀態模式來進行改寫。
一、為什麼需要狀態模式?
🌰 如果你從網上買了一個東西,該訂單的狀態有以下狀態
- 待支付 (PendingPay)
- 已支付 (Paid)
用户點擊一個按鈕 “下一步處理”,訂單會按照狀態順序流轉。
在沒有使用狀態模式時:
public class Order {
private Byte status;
// 定義狀態常量
public static final Byte PENDING_PAY = 1;
public static final Byte PAID = 2;
public static final Byte SHIPPED = 3;
public static final Byte DELIVERED = 4;
public Order() {
this.status = PENDING_PAY; // 默認初始狀態
}
public void nextStep() {
if (PENDING_PAY.equals(status)) {
System.out.println("用户付款成功");
status = PAID;
} else if (PAID.equals(status)) {
System.out.println("商家發貨");
status = SHIPPED;
}
}
}
但是如果我再增加
- 已發貨 (Shipped)
- 已簽收 (Delivered)
那麼就需要再寫兩個else if
public class Order {
private Byte status;
// 定義狀態常量
public static final Byte PENDING_PAY = 1;
public static final Byte PAID = 2;
public static final Byte SHIPPED = 3;
public static final Byte DELIVERED = 4;
public Order() {
this.status = PENDING_PAY; // 默認初始狀態
}
public void nextStep() {
if (PENDING_PAY.equals(status)) {
System.out.println("用户付款成功");
status = PAID;
} else if (PAID.equals(status)) {
System.out.println("商家發貨");
status = SHIPPED;
} else if (SHIPPED.equals(status)) {
System.out.println("用户簽收");
status = DELIVERED;
} else if (DELIVERED.equals(status)) {
System.out.println("訂單已完成");
}
}
}
在這裏增加一個看不出來有什麼麻煩的地方,假設我再增加狀態 退貨中 已退款 退貨完成 等這些狀態,那麼增加的else if將是災難級別的。
因此,狀態模式可以很好的解決這樣的問題。
二、什麼是狀態模式?
核心思想:允許一個對象在其內部狀態改變時改變它的行為,使對象看起來好像修改了它的類。
狀態模式有三個核心的角色:
|
角色
|
職責
|
類比現實世界
|
|
Context(上下文)
|
維護當前狀態對象的引用,將客户端請求委託給當前狀態,提供狀態改變的接口
|
運動員:持有接力棒,跑到各個接力站
|
|
State(狀態接口)
|
定義狀態行為的接口,確保所有狀態類有一致的方法
|
工作規範:定義接力站工作人員的工作標準
|
|
ConcreteState(具體狀態)
|
實現當前狀態下的具體行為,知道下一狀態應該是什麼,通過上下文接口改變狀態
|
接力站工作人員:處理具體任務,告訴運動員下一站去哪
|
簡單的解釋:一個對象(稱之為“上下文”)擁有多種狀態。對於同一個請求,在不同的狀態下,它會做出不同的響應。狀態模式將每一個狀態封裝成一個獨立的類,這樣上下文對象就不需要維護一大堆 if-else 或 switch-case 來判斷當前狀態該做什麼,而是將行為委託給代表當前狀態的對象。
看起來還是有點難以理解,現在通過代碼改造上面的訂單狀態來解釋:
1. 對於同一個請求,在不同的狀態下,它會做出不同的響應:
- 當 status 為 待支付 時,nextStep() 執行付款邏輯
- 當 status 為 已支付 時,nextStep() 執行發貨邏輯
- 當 status 為 已發貨 時,nextStep() 執行簽收邏輯
- 當 status 為 已簽收 時,訂單已完成
2. 將每一個狀態分裝成一個獨立的類:
public interface OrderState {
void next(Order order);
}
// 狀態1:待支付狀態
public class PendingPayState implements OrderState {
public void next(Order order) {
System.out.println("用户付款成功");
order.setStatus(Order.PAID); // 狀態改變:待支付狀態 → 已支付狀態
}
}
// 狀態2:已支付狀態
public class PaidState implements OrderState {
public void next(Order order) {
System.out.println("商家發貨");
order.setStatus(Order.SHIPPED); // 狀態改變:已支付狀態 → 已發貨狀態
}
}
// 狀態3:已發貨狀態
public class ShippedState implements OrderState {
public void next(Order order) {
System.out.println("用户簽收");
order.setStatus(Order.DELIVERED); // 狀態改變:已發貨狀態 → 已簽收狀態
}
}
// 狀態4:已簽收狀態
public class DeliveredState implements OrderState {
public void next(Order order) {
System.out.println("訂單已完成");
}
}
3. 上下文將行為委託給代表當前狀態的對象:
用現實世界中的舉個例子:
原始方式(if-else):
你接到任務時,要查手冊:"我現在是什麼狀態?如果是狀態1就做A,狀態2就做B..."
狀態模式:
你接到任務時,直接問當前的狀態專員:"這個事情該怎麼處理?"
待支付專員説:"我來處理付款"
已支付專員説:"我來處理髮貨"
已發貨專員説:"我來處理簽收"
public class Order {
private OrderState currentState; // 當前狀態對象
public void nextStep() {
// 不再需要if-else,直接委託給當前狀態對象
currentState.next(this); // this = 當前正在執行的 Order 對象
}
public void setState(Object state) {
this.currentState = state;
}
}
currentState.next(this); 這塊是比較難以理解的
以下是測試的結果:
訂單狀態: 待支付
當前狀態: 待支付
Order對象(this): Order@279f2327
用户付款成功
下一步後狀態: 已支付
當前狀態: 已支付
Order對象(this): Order@279f2327
商家發貨
下一步後狀態: 已發貨
當前狀態: 已發貨
Order對象(this): Order@279f2327
用户簽收
下一步後狀態: 已簽收
當前狀態: 已簽收
Order對象(this): Order@279f2327
訂單已完成
下一步後狀態: 已簽收
可以看到這個打印出的this都是Order@279f2327,大概可以猜測一下,我要改變這個order的狀態,那麼我首先就要知道修改的是哪個order,這個this意在指明告訴當前狀態對象要操作Order@279f2327的訂單
實例化一個對象後,調用nextStep()
Order order = new Order();
order.nextStep();
當執行nextStep()中的以下代碼時:
currentState.next(this);
這個this(即新實例化的這個order對象本身)將會被傳遞
把 this 想象成你的身份證:
你去銀行辦事(調用 nextStep())
你把身份證遞給櫃員(傳遞 this)
櫃員查看身份證,知道要操作哪個賬户(狀態對象知道操作哪個Order)
櫃員辦理業務並更新你的賬户(狀態對象調用 order.setState())
4. 對象看起來好像修改了這個類:
主方法測試:
public static void main(String[] args) {
Order order = new Order();
order.displayStatus();
// 執行完整流程
order.nextStep(); // 第一次:執行付款邏輯
order.nextStep(); // 第二次:執行發貨邏輯
order.nextStep(); // 第三次:執行簽收邏輯
order.nextStep(); // 第四次:執行完成邏輯
}
同一個 order.nextStep() 調用,每次執行不同的邏輯!
就好像 Order 類在運行時"變身"了一樣:
- 第一次調用時,它像是一個"待支付訂單"
- 第二次調用時,它像是一個"已支付訂單"
- 第三次調用時,它像是一個"已發貨訂單"
- 第四次調用時,它像是一個"已完成訂單"
三、在途貨物模塊該用狀態模式
當前的需求圖如下:
待發狀態在途狀態到港狀態完成狀態增加 裝運日期 預計到港日期 船名航次 等字段增加 到港日期 字段增加 完成日期 提單號 提單數 成本價(元/噸) 字段待發狀態在途狀態到港狀態完成狀態
由上圖我們可以看到需要三次狀態轉換,按照原始的方法寫:則需要3個邏輯,代碼非常難找,同時由於狀態的切換不可逆且無法跳躍,需要大量的判斷當前狀態。
以 到港 切換 完成 狀態為例子:
public InTransitGoods switchToCompleted(Long id, InTransitGoodsDto.SwitchToCompletedRequest request) {
InTransitGoods inTransitGoods = this.inTransitGoodsRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
// 校驗當前貨物的狀態是否為[到港]
if (!this.checkCurrentStatus(inTransitGoods, InTransitGoodsStatus.arrive.getValue())) {
throw new IllegalArgumentException("當前貨物狀態不是[到港],無法切換至[完成]狀態");
}
inTransitGoods.setStatus(InTransitGoodsStatus.completed.getValue());
inTransitGoods.setCompletionDate(request.getCompletionDate());
inTransitGoods.setLadingCount(request.getLadingCount());
inTransitGoods.setLadingNumber(request.getLadingNumber());
inTransitGoods.setCostPrice(request.getCostPrice());
return this.inTransitGoodsRepository.save(inTransitGoods);
}
待發切換為在途,以及在途切換為完成也同理
也許三個不是很多,但是如果我有大量的狀態,比如再增加 被滯留 退貨中 退貨完成 已完成 等狀態,則可讀性將會變得非常差。
改用狀態模式:
/**
* 在途貨物狀態接口
*/
public interface InTransitGoodsStatus {
InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request);
InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request);
InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request);
}
這裏以完成狀態為例子
/**
* 完成狀態實現類
*/
public class CompletedStatus implements InTransitGoodsStatus {
@Override
public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
throw new IllegalStateException("完成貨物不能切換到在途");
}
@Override
public InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request) {
throw new IllegalStateException("完成貨物不能切換到到港");
}
@Override
public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
throw new IllegalStateException("貨物已經是完成狀態");
}
}
由於完成狀態不存在處理邏輯,所以在此給出在途狀態的示例:
public class InTransitState implements InTransitGoodsState {
@Override
public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
throw new IllegalStateException("在途貨物不能更新待發信息");
}
@Override
public InTransitGoods switchToArrived(InTransitGoods inTransitGoods, InTransitGoodsDto.SwitchToArrivedRequest request) {
inTransitGoods.setStatus(InTransitGoodsStatus.arrive.getValue());
inTransitGoods.setArrivalDate(request.getArrivalDate());
return inTransitGoods;
}
@Override
public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
throw new IllegalStateException("在途貨物不能直接切換到完成");
}
}
在這裏增加一個統一創建和管理所有狀態對象的類:
public class InTransitGoodsStatusFactory {
private static final Map<Byte, InTransitGoodsState> statusMap = Map.of(
InTransitGoodsStatus.pending.getValue(), new PendingStatus(),
InTransitGoodsStatus.inTravel.getValue(), new InTransitStatus(),
InTransitGoodsStatus.arrive.getValue(), new ArrivedStatus(),
InTransitGoodsStatus.completed.getValue(), new CompletedStatus()
);
public static InTransitGoodsState getStatus(InTransitGoods inTransitGoods) {
InTransitGoodsStatus inTransitGoodsStatus = statusMap.get(inTransitGoods.getStatus());
if (inTransitGoodsStatus == null) throw new IllegalArgumentException("未知狀態");
return inTransitGoodsStatus;
}
}
最終那一串的代碼將會變成如下:
public InTransitGoods switchToCompleted(Long id, InTransitGoodsDto.SwitchToCompletedRequest request) {
InTransitGoods inTransitGoods = this.inTransitGoodsRepository.findById(id).orElseThrow(EntityNotFoundException::new);
InTransitGoodsStatus status = InTransitGoodsStateFactory.getStatus(inTransitGoods);
inTransitGoods = status.switchToCompleted(inTransitGoods, request);
return this.inTransitGoodsRepository.save(inTransitGoods);
}
這樣,即使再增加多個狀態時,也不用擔心,增加幾個狀態我就重新創建幾個狀態類就可以了。
四、聽潘老師講解後的又一次啓發
我們在寫具體實現類的時候,以完成狀態為例子,我們難免會懷疑這樣寫很麻煩,我每個實現類都需要去實現所有的方法,如果不滿足則拋出異常,這樣導致看起來十分冗餘且不好分辨,證明還是沒有深刻理解狀態模式的巧妙之處。
/**
* 完成狀態實現類
*/
public class CompletedStatus implements InTransitGoodsStatus {
@Override
public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
throw new IllegalStateException("完成貨物不能切換到在途");
}
@Override
public InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request) {
throw new IllegalStateException("完成貨物不能切換到到港");
}
@Override
public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
throw new IllegalStateException("貨物已經是完成狀態");
}
}
而狀態管理真正的巧妙之處在於:我可以寫一個抽象類來實現這個接口:
先上圖:
«interface»
InTransitGoodsStatus
+switchToInTransit()
+switchToArrived()
+switchToCompleted()
«abstract»
AbstractInTransitGoodsStatus
+switchToInTransit()
+switchToArrived()
+switchToCompleted()
PendingStatus
+switchToInTransit()
InTransitStatus
+switchToArrived()
ArrivedStatus
+switchToCompleted()
public abstract class AbstractInTransitGoodsStatus implements InTransitGoodsState {
@Override
public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
throw new IllegalStateException("完成貨物不能切換到在途");
}
@Override
public InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request) {
throw new IllegalStateException("完成貨物不能切換到到港");
}
@Override
public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
throw new IllegalStateException("完成貨物已經是完成狀態");
}
}
然後在對應的狀態類中,只需要實現你所需要的邏輯轉換即可:
public class InTransitStatus extends AbstractInTransitGoodsStatus {
@Override
public InTransitGoods switchToArrived(InTransitGoods inTransitGoods, InTransitGoodsDto.SwitchToArrivedRequest request) {
inTransitGoods.setStatus(InTransitGoodsStatus.arrive.getValue());
inTransitGoods.setArrivalDate(request.getArrivalDate());
return inTransitGoods;
}
}
待發狀態在途狀態到港狀態完成狀態增加 裝運日期 預計到港日期 船名航次 等字段增加 到港日期 字段增加 完成日期 提單號 提單數 成本價(元/噸) 字段待發狀態在途狀態到港狀態完成狀態
所以我們可以得出結論:
這個狀態圖完全可以和我們的代碼一一對應,我在待發狀態要實現在途狀態的邏輯,那麼只寫轉換成在途狀態邏輯就好,如果增加了轉換為其他狀態,那麼只需要再該狀態對應的類下面再實現一個方法即可,如果沒有該方法,抽象類會拋出異常。
結語
通過這次的項目,簡單的瞭解了狀態模式的基本原理,在學習狀態模式時,瞭解到它與策略模式的類圖相同,我會繼續去學習策略模式,理解他們的異同。本文主要通過學習《Head First設計模式》有了大致的瞭解,如果存在理解錯誤的地方,歡迎指出!