首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

重構實戰:用狀態模式消除 if-else 條件泥潭

     編輯:Java 程式設計入門

問題現場:影片租賃定價邏輯的 if-else 地獄

public class Movie {
    public static final int REGULAR = 1, NEW_RELEASE = 2, CHILDREN = 3;

    public double getCharge(int movieCode, int days) throws MovieException {
        double result = 0;
        if (movieCode == REGULAR) {
            result = 2;
            if (days > 2) result += (days - 2) * 1.5;
            return result;
        } else if (movieCode == NEW_RELEASE) {
            result = days * 3;
            return result;
        } else if (movieCode == CHILDREN) {
            result = 1.5;
            if (days > 3) result += (days - 3) * 1.5;
            return result;
        }
        throw new MovieException("Unknown movie code");
    }

    public int getFrequentRenterPoints(int movieCode, int days) {
        if (movieCode == NEW_RELEASE && days > 1) return 2;
        return 1;
    }
}

痛點:

  • 新增影片類型需修改 Movie 類(違反開閉原則)
  • 定價與積分邏輯耦合在同一類,職責不清
  • 測試需構造大量組合參數,難以隔離驗證單一規則
  • 未來若引入「會員等級折扣」、「節假日漲價」等維度,條件爆炸

狀態模式重構步驟

1. 提取狀態介面

public interface PriceStrategy {
    double getCharge(int daysRented);
    int getFrequentRenterPoints(int daysRented);
}

2. 實現具體狀態類

public class RegularPrice implements PriceStrategy {
    public double getCharge(int days) {
        double result = 2;
        if (days > 2) result += (days - 2) * 1.5;
        return result;
    }
    public int getFrequentRenterPoints(int days) { return 1; }
}

public class NewReleasePrice implements PriceStrategy {
    public double getCharge(int days) { return days * 3; }
    public int getFrequentRenterPoints(int days) { return days > 1 ? 2 : 1; }
}

public class ChildrenPrice implements PriceStrategy {
    public double getCharge(int days) {
        double result = 1.5;
        if (days > 3) result += (days - 3) * 1.5;
        return result;
    }
    public int getFrequentRenterPoints(int days) { return 1; }
}

3. Movie 類委派給狀態對象

public class Movie {
    private PriceStrategy priceStrategy;

    public Movie(PriceStrategy priceStrategy) {
        this.priceStrategy = priceStrategy;
    }

    public double getCharge(int daysRented) {
        return priceStrategy.getCharge(daysRented);
    }

    public int getFrequentRenterPoints(int daysRented) {
        return priceStrategy.getFrequentRenterPoints(daysRented);
    }

    // 靜態工廠簡化創建
    public static Movie newRegular() { return new Movie(new RegularPrice()); }
    public static Movie newNewRelease() { return new Movie(new NewReleasePrice()); }
    public static Movie newChildren() { return new Movie(new ChildrenPrice()); }
}

4. 客戶端代碼簡化

// 之前
Movie movie = new Movie();
double charge = movie.getCharge(Movie.NEW_RELEASE, 5);

// 之後
Movie movie = Movie.newNewRelease();
double charge = movie.getCharge(5); // 類型信息封裝在對象內部

進階:狀態轉換與上下文感知

若影片類型會隨時間變化(如新片 30 天后轉為普通片),引入狀態轉換邏輯:

public class Movie {
    private PriceStrategy priceStrategy;
    private LocalDate releaseDate;

    public void updatePriceStrategy() {
        if (priceStrategy instanceof NewReleasePrice &&
            ChronoUnit.DAYS.between(releaseDate, LocalDate.now()) > 30) {
            this.priceStrategy = new RegularPrice(); // 狀態遷移
        }
    }
    // 委派方法前調用 updatePriceStrategy()
}

或將轉換邏輯封裝在 PriceStrategy 內部(如 nextStrategy()),實現狀態機。

對比:狀態模式 vs 策略模式 vs 命令模式

模式核心意圖狀態持有者典型場景
State對象行為隨內部狀態改變而改變,狀態可轉換Context (Movie)訂單狀態、影片定價類型、TCP 連接狀態
Strategy封裝可互換算法族,客戶端顯式選擇/注入Client排序算法、支付渠道、壓縮策略
Command將請求封裝為對象,支持撤銷/排隊/日志InvokerGUI 動作、異步任務、事務操作

本例中,影片類型屬於 Movie 內在屬性,且可能隨時間遷移,屬於 State 模式經典適用場景。

測試驗證:狀態隔離帶來的可測試性提升

@Test
void newReleasePrice_chargeAndPoints() {
    PriceStrategy strategy = new NewReleasePrice();
    assertEquals(15, strategy.getCharge(5), 0.001);
    assertEquals(2, strategy.getFrequentRenterPoints(5));
    assertEquals(1, strategy.getFrequentRenterPoints(1));
}

// Movie 測試僅驗證委派
@Test
void movie_delegatesToStrategy() {
    PriceStrategy mock = mock(PriceStrategy.class);
    when(mock.getCharge(3)).thenReturn(9.0);
    Movie movie = new Movie(mock);
    assertEquals(9.0, movie.getCharge(3), 0.001);
}

每個定價規則獨立測試,Movie 類測試極簡,**單元測試覆蓋率從「組合爆炸」降為「線性增長」**。

擴展性演示:添加「VIP 會員折扣」維度

若引入會員等級影響定價,兩種擴展路徑:

  1. 裝飾器模式:new VipDiscountPrice(new NewReleasePrice(), vipLevel),保持 PriceStrategy 接口不變。
  2. 參數化策略:接口改為 getCharge(int days, Customer customer),現有實現忽略 customer,新實現使用。需評估接口變更影響面。

推薦路徑 1,**零侵入擴展**,符合開閉原則。

總結

狀態模式將條件分支轉化為多態分派,將狀態特有行為封裝進獨立類。核心收益:

  1. 消除 if-else 鏈,新增類型只需新增類,不動既有代碼。
  2. 職責單一:定價邏輯、積分邏輯、狀態轉換邏輯各司其職。
  3. 可測試性質變:從參數組合測試轉為單一策略驗證 + 委派驗證。
  4. 運行時狀態可視:movie.getPriceStrategy().getClass().getSimpleName() 即知當前定價模式。

適用判斷:當某對象行為取決於其內部狀態,且狀態數量有限、轉換邏輯明確時,果斷引入 State 模式。這是《重構》第一章「以多態取代條件表達式」的教科書級實踐。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved