如何在不修改原始碼、不重新編譯整個專案的前提下,為系統新增業務功能?本文通過一個「經紀商定價系統」的演進案例,深度演示如何結合介面導向設計與動態類載入,構建支援外掛式擴展的高靈活性架構。
某券商系統需支援多種客戶定價方案:
核心痛點: 每新增一種方案,若採用硬編碼 if-else 或 switch,都需修改核心交易類、重新編譯、全量回歸測試——違反開閉原則,維護成本極高。
// Trade 類直接耦合具體定價類
public double getCommissionPrice() {
if (getCustomer().getPlanId() == 1) {
return new PricingPlan20().calculateCommission(this);
} else {
return new PricingPlan1510().calculateCommission(this);
}
}
缺點: 違反依賴倒置原則,新增方案需修改 Trade 類。
public interface IPricingPlan {
double calculateCommission(Trade trade);
}
public class PricingPlan20 implements IPricingPlan { ... }
public class PricingPlan1510 implements IPricingPlan { ... }
// Trade 類依賴介面而非具體類
public double getCommissionPrice() {
IPricingPlan plan;
if (getCustomer().getPlanId() == 1) plan = new PricingPlan20();
else plan = new PricingPlan1510();
return plan.calculateCommission(this);
}
進步: 核心邏輯解耦,但實例化邏輯仍硬編碼在 Trade 中。
public double getCommissionPrice() {
try {
String className = (getCustomer().getPlanId() == 1)
? "string_interfaces.PricingPlan20"
: "string_interfaces.PricingPlan1510";
Class> clazz = Class.forName(className);
IPricingPlan plan = (IPricingPlan) clazz.newInstance();
return plan.calculateCommission(this);
} catch (Exception e) { ... }
}
關鍵點: 類名以字串形式存在,可從配置檔、資料庫讀取,但Trade 類仍需知曉所有可能的類名對應關係。
private static final String[] PRICING_PLANS = {
"string_interfaces.PricingPlan20",
"string_interfaces.PricingPlan1510",
"string_interfaces.PricingPlan8",
"string_interfaces.PricingPlan10"
};
public double getCommissionPrice() {
String className = PRICING_PLANS[getCustomer().getPlanId() - 1];
// ... 動態載入實例化
}
殘留問題: 新增方案仍需修改 Trade 類原始碼(增陣列元素)並重新編譯。
配置檔 pricing_plans.txt:
1,com.myapp.pricing.PricingPlan20
2,com.myapp.pricing.PricingPlan1510
3,com.myapp.pricing.PricingPlan8
4,com.myapp.pricing.PricingPlan10
PricingPlanFactory 實現:
public class PricingPlanFactory {
private static final Map planMap = loadFromConfig();
public static IPricingPlan createPlan(int planId) {
String className = planMap.get(planId);
if (className == null) throw new IllegalArgumentException("Unknown plan: " + planId);
try {
return (IPricingPlan) Class.forName(className).newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate pricing plan", e);
}
}
private static Map loadFromConfig() {
// 解析 txt / XML / 資料庫,建立 planId -> className 映射
}
}
Client 代碼極簡:
IPricingPlan plan = PricingPlanFactory.createPlan(customer.getPlanId());
double commission = plan.calculateCommission(trade);
Class.forName() 的類由擴展類載入器載入,預設不會搜尋應用類路徑Thread.currentThread().getContextClassLoader().loadClass(className) 或指定類載入器為防止介面升級導致現有外掛失效:
com.brokerage.version1.customer.IPricingPlanIPricingPlanV2 extends IPricingPlan本文展示的演進路徑體現了經典設計原則的實戰應用:
Class.forName() 實現運行時類發現與實例化最終達到的效果:新增定價方案 = 新增實現類 + 更新配置檔,零代碼修改、零重新編譯、零核心邏輯風險——這正是「動態擴展」的核心價值。