前言

在最近寫項目的時候,涉及到一個需求,在途貨物模塊,貨物的狀態有四種,分別是待發,在途,到港,完成。這四個狀態的關係是,由待發狀態切換為在途狀態時,需要增加字段比如裝運日期字段,同時切換為在途狀態後就不能切換為待發狀態,其他的狀態也是這個思想。起初寫的時候按照以前的思路,需要切換什麼狀態,我就對應寫一個方法,但是這樣面臨一個問題,判斷的狀態越來越多且寫在一整個類中,太過於冗雜,查找起來較為困難,如果再增加多個狀態就是非常災難的。聽潘老師的建議後,選擇用狀態模式來進行改寫。

一、為什麼需要狀態模式?

🌰 如果你從網上買了一個東西,該訂單的狀態有以下狀態

  • 待支付 (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設計模式》有了大致的瞭解,如果存在理解錯誤的地方,歡迎指出!