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 類(違反開閉原則)public interface PriceStrategy {
double getCharge(int daysRented);
int getFrequentRenterPoints(int daysRented);
}
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; }
}
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()); }
}
// 之前
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()),實現狀態機。
| 模式 | 核心意圖 | 狀態持有者 | 典型場景 |
|---|---|---|---|
| State | 對象行為隨內部狀態改變而改變,狀態可轉換 | Context (Movie) | 訂單狀態、影片定價類型、TCP 連接狀態 |
| Strategy | 封裝可互換算法族,客戶端顯式選擇/注入 | Client | 排序算法、支付渠道、壓縮策略 |
| Command | 將請求封裝為對象,支持撤銷/排隊/日志 | Invoker | GUI 動作、異步任務、事務操作 |
本例中,影片類型屬於 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 類測試極簡,**單元測試覆蓋率從「組合爆炸」降為「線性增長」**。
若引入會員等級影響定價,兩種擴展路徑:
new VipDiscountPrice(new NewReleasePrice(), vipLevel),保持 PriceStrategy 接口不變。getCharge(int days, Customer customer),現有實現忽略 customer,新實現使用。需評估接口變更影響面。推薦路徑 1,**零侵入擴展**,符合開閉原則。
狀態模式將條件分支轉化為多態分派,將狀態特有行為封裝進獨立類。核心收益:
movie.getPriceStrategy().getClass().getSimpleName() 即知當前定價模式。適用判斷:當某對象行為取決於其內部狀態,且狀態數量有限、轉換邏輯明確時,果斷引入 State 模式。這是《重構》第一章「以多態取代條件表達式」的教科書級實踐。