整理Effective Java書中Item 38: Emulate extensible enums with interfaces心得筆記

主旨

Java 的 enum 雖然功能強大,但有一個主要限制:enum 是封閉的類型,不能被擴展。這意味著一旦定義了 enum,就無法在不修改原始程式碼的情況下添加新的值。這篇文章介紹如何使用介面來解決這個問題。

問題:enum 的封閉性

public enum Operation {
    PLUS {
        double apply(double x, double y) { return x + y; }
    },
    MINUS {
        double apply(double x, double y) { return x - y; }
    },
    TIMES {
        double apply(double x, double y) { return x * y; }
    },
    DIVIDE {
        double apply(double x, double y) { return y == 0 ? Double.POSITIVE_INFINITY : x / y; }
    };

    abstract double apply(double x, double y);
}

這種設計的問題是:如果要添加新的運算(比如取餘數),必須修改原始的 Operation。這違反了開放封閉原則(OCP),讓程式碼難以維護。

解決方案1:使用介面

解決方案是將 enum 的行為抽取到介面,然後讓 enum 實作這個介面:

// 運算介面
public interface Operation {
    double apply(double x, double y);
}

// 基本運算的 enum 實作
public enum BasicOperation implements Operation {
    PLUS {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE {
        public double apply(double x, double y) { return y == 0 ? Double.POSITIVE_INFINITY : x / y; }
    };
}

// 可以在其他地方定義新的運算
public enum ExtendedOperation implements Operation {
    MOD {
        public double apply(double x, double y) { return x % y; }
    };
}

// 使用方式
public class Calculator {
    private final double x;
    private final double y;
    private final Operation operation;

    public Calculator(double x, double y, Operation operation) {
        this.x = x;
        this.y = y;
        this.operation = operation;
    }

    public double calculate() {
        return operation.apply(x, y);
    }
}

// 在運行時動態載入所有運算
public class OperationFactory {
    public static List<Operation> getAllOperations() {
        List<Operation> operations = new ArrayList<>();
        operations.addAll(Arrays.asList(BasicOperation.values()));
        operations.addAll(Arrays.asList(ExtendedOperation.values()));
        return operations;
    }
}

解決方案2:使用介面(運費策略)

// 運費策略介面
public interface ShippingStrategy {
    double calculateShippingCost(double basePrice, int weight);
    String getStrategyName();
}

// 基本運費策略的 enum 實作
public enum BasicShippingStrategy implements ShippingStrategy {
    STANDARD {
        // 基本運費計算:基本費 + 重量費
        public double calculateShippingCost(double basePrice, int weight) {
            double baseFee = 30; // 基本運費
            double weightFee = weight * 0.5; // 每公斤 0.5 元
            return baseFee + weightFee;
        }
        public String getStrategyName() { return "標準運費"; }
    },
    EXPRESS {
        // 隔日達運費計算:基本費 + 隔日達費 + 重量費
        public double calculateShippingCost(double basePrice, int weight) {
            double baseFee = 30; // 基本運費
            double expressFee = 50; // 隔日達費
            double weightFee = weight * 0.8; // 每公斤 0.8 元
            return baseFee + expressFee + weightFee;
        }
        public String getStrategyName() { return "隔日達"; }
    },
    FREE {
        // 滿額免運邏輯:滿 1000 元免運,否則使用標準運費
        public double calculateShippingCost(double basePrice, int weight) {
            if (basePrice >= 1000) return 0;
            return STANDARD.calculateShippingCost(basePrice, weight);
        }
        public String getStrategyName() { return "滿額免運"; }
    };
}

// 可以在其他地方定義新的運費策略
public enum StorePickupStrategy implements ShippingStrategy {
    PICKUP {
        // 超商取貨運費計算:
        // - 商品重量超過 1000 公克,收取 20 元取貨費
        // - 商品重量未超過 1000 公克,免費取貨
        public double calculateShippingCost(double basePrice, int weight) {
            if (weight > 1000) return 20;
            return 0;
        }
        public String getStrategyName() { return "超商取貨"; }
    },
    SAME_DAY {
        // 當日取貨運費計算:
        // - 基本費 50 元
        // - 商品重量超過 1000 公克,每公斤加收 1 元
        public double calculateShippingCost(double basePrice, int weight) {
            double baseFee = 50; // 基本費
            if (weight > 1000) {
                double extraFee = (weight - 1000) * 1.0; // 超過 1000 公克後每公斤 1 元
                return baseFee + extraFee;
            }
            return baseFee;
        }
        public String getStrategyName() { return "當日取貨"; }
    };
}

// 使用方式
public class Order {
    private final double basePrice;
    private final int weight;
    private final ShippingStrategy shippingStrategy;
    private final String customerRegion; // 客戶所在區域

    public Order(double basePrice, int weight, String customerRegion, ShippingStrategy strategy) {
        this.basePrice = basePrice;
        this.weight = weight;
        this.customerRegion = customerRegion;
        this.shippingStrategy = strategy;
    }

    public double calculateTotalCost() {
        double shippingCost = shippingStrategy.calculateShippingCost(basePrice, weight);
        return basePrice + shippingCost;
    }

    public String getShippingInfo() {
        return String.format("運費策略: %s, 運費: %.2f", 
            shippingStrategy.getStrategyName(), 
            shippingStrategy.calculateShippingCost(basePrice, weight));
    }

    // 根據區域自動選擇最合適的運費策略
    public static ShippingStrategy chooseBestStrategy(String region, int weight) {
        // 可以根據區域和商品重量自動選擇最合適的運費策略
        if (region.equals("Taipei") && weight < 1000) {
            return StorePickupStrategy.PICKUP; // 台北地區且商品重量輕,建議超商取貨
        }
        return BasicShippingStrategy.STANDARD; // 預設使用標準運費
    }
}

// 在運行時動態載入所有運費策略
public class ShippingStrategyFactory {
    public static List<ShippingStrategy> getAllStrategies() {
        List<ShippingStrategy> strategies = new ArrayList<>();
        strategies.addAll(Arrays.asList(BasicShippingStrategy.values()));
        strategies.addAll(Arrays.asList(StorePickupStrategy.values()));
        return strategies;
    }

    // 根據條件過濾可用的運費策略
    public static List<ShippingStrategy> getAvailableStrategies(String region, int weight) {
        List<ShippingStrategy> allStrategies = getAllStrategies();
        return allStrategies.stream()
            .filter(strategy -> {
                // 篩選條件:
                // 1. 超商取貨策略只適用於特定區域
                // 2. 隔日達策略不適用於重量超過 5000 公克的商品
                if (strategy instanceof StorePickupStrategy) {
                    return region.equals("Taipei") || region.equals("NewTaipei");
                }
                if (strategy == BasicShippingStrategy.EXPRESS && weight > 5000) {
                    return false;
                }
                return true;
            })
            .collect(Collectors.toList());
    }
}

小結

使用介面來模擬可擴展的 enum 有幾個好處:

  1. 解決封閉性問題:可以通過新的 enum 實作介面來擴展功能
  2. 維護更容易:新的功能可以完全獨立於原始程式碼
  3. 型別安全:仍然保持了 enum 的型別安全性
  4. 靈活多樣:可以根據需要在不同位置定義新的 enum 實作

這種設計模式特別適合需要在不修改原始程式碼的情況下擴展功能的場景。