單一職責原則,英文稱作 Single Responsibility Principle,簡稱 SRP,是軟體設計中的一個重要原則。該原則強調每個類別(或每個方法)應該只負責一項職責,或服務於一個角色,並且這項職責必須完全封裝於類別或方法當中。透過「將不同的職責分離到不同的位置」這一行為,雖然實現該原則時,容易產生較高的類別數目,但這麼做不僅可以提高類別的內聚性,還可以改善系統的可維護性、可讀性、和可測試性,使程式更加易讀、易測試、和易於管理。
舉例來說:假設我們正在撰寫一個銀行行業的業務邏輯,邏輯當中帶有一個提款(withdraw)的方法,該方法負責處理用戶的提款請求。如果該銀行目前只針對客戶簡單區分為普通客戶與 VIP 客戶,其中普通客戶的單次提醒上限為 1000,VIP 沒有提領上限,且可以稍微預支提領金額(為當前戶頭金額再多 5%),程式可能會寫得像下面這個樣子:
class BankAccount {
public double withdraw(double amount) {
// 普通客戶
if (!User.isVIP) {
if (User.balance < amount) {
throw new IllegalArgumentException("Insufficient balance");
}
if(amount > BankAccount.REGULAR_MAX_WITHDRAWAL) {
throw new IllegalArgumentException("Exceeds maximum withdrawal amount");
}
}
// VIP客戶
else {
double vip_max_withdrawal = User.balance * BankAccount.OVERFLOT_PERCENTAGE;
if (amount > vip_max_withdrawal) {
throw new IllegalArgumentException("Exceeds maximum withdrawal amount");
}
}
User.balance -= amount;
return User.balance;
}
}
但如果我們複雜化銀行的業務邏輯:現在銀行將客戶分為普通客戶、白金客戶、鑽石客戶和 VIP 客戶 ...等 4 個等級,且每種客戶類型都有不同的提款規則和權限,包含但不限於以下條件:普通客戶的單次提款上限為 1,000、每日提款總額不得超過 5,000、不允許透支、且超過 500 的提款需要接收手機驗證碼;白金客戶的單次提款上限為 5,000、每日提款總額不得超過 50,000、允許小額透支(最多為賬戶餘額的 2%),但透支需於月底要支付總透支的 1% 作為手續費,也同樣需要接收手機驗證碼(2,000 元);
鑽石客戶的單次提款上限為 20,000、每日提款總額不得超過 500,000、允許最多賬戶餘額的 5% 作為透支金額,同樣需要於月底支付總透支的 0.5% 作為手續費,超過 10,000 的大額提款一定會有簡訊通知,無法關閉;而 VIP 客戶無單次提款上限、無每日提款總額不設限,但超過 500,000 需特別申請,需要透過客戶專屬的經理確認。允許大額透支,依照 VIP 的不同等級可再細分為賬戶餘額的 10% ~ 20% 作為最高上限金額。
且對於特定的卡號(不一定是哪個等級),有著額外的業務邏輯需要處理(被凍結的、行員內部的、需要被監控的、或者是大戶贈與子女的特別卡 ...等)。不難想像當這些業務邏輯全部都放在 withdraw()
裡面之後,該方法將會變得非常的生人勿近 ...。因此,對於這樣的情況(一個 withdraw()
需要為不同等級的、不同狀況的、不同複雜程度的客戶所負責),一個相對簡單的解決方法,便是利用 SRP(單一職責原則)完成程式上的「分而治之」:
interface WithdrawStrategy {
double withdraw(double amount) throws IllegalArgumentException;
}
// 普通客戶提款策略
class RegularWithdrawStrategy implements WithdrawStrategy {
private static final double MAX_SINGLE_WITHDRAWAL = 1000;
private static final double MAX_DAILY_WITHDRAWAL = 5000;
private static final double SMS_VERIFICATION_THRESHOLD = 500;
@Override
public double withdraw(double amount) throws IllegalArgumentException {
if (amount > MAX_SINGLE_WITHDRAWAL)
throw new IllegalArgumentException("超出單次提款上限");
if (User.getDailyWithdrawalTotal() + amount > MAX_DAILY_WITHDRAWAL)
throw new IllegalArgumentException("超出每日提款總額限制");
if (User.getBalance() < amount)
throw new IllegalArgumentException("餘額不足");
if (amount > SMS_VERIFICATION_THRESHOLD) {
verifySMS();
}
User.setBalance(User.getBalance() - amount);
User.addToDailyWithdrawalTotal(amount);
return User.getBalance();
}
}
// 白金客戶提款策略
class PlatinumWithdrawStrategy implements WithdrawStrategy {
@Override
public double withdraw(double amount) throws IllegalArgumentException {
// 白金客戶的提款邏輯
}
}
// 鑽石客戶提款策略
class DiamondWithdrawStrategy implements WithdrawStrategy {
// 鑽石客戶的提款邏輯
}
// 其他等級、特殊卡號的提款邏輯 ...
class User {
private WithdrawStrategy withdrawStrategy;
public User(String name, double initialBalance, WithdrawStrategy strategy) {
// 建構子 ...
}
public double withdraw(double amount) throws IllegalArgumentException {
return withdrawStrategy.withdraw(this, amount);
}
// Getters and setters
public double getDailyWithdrawalTotal() {
return dailyWithdrawalTotal;
}
public void setWithdrawStrategy(WithdrawStrategy strategy) {
this.withdrawStrategy = strategy;
}
// 其他 getter setter ...
class Bank {
public double withdraw(User user, double amount) throws IllegalArgumentException {
// 使用用戶的提款策略執行提款操作
return user.getWithdrawStrategy().withdraw(user, amount);
}
// 其他關於銀行的業務邏輯 ...
}
在這裡我們定義了一個 WithdrawStrategy
的接口,且該接口包含一個 withdraw()
方法。不同客戶等級的提款策略,只要各自獨立為一個類別,並實例化 WithdrawStrategy
的接口、完成各自的 withdraw()
邏輯,再讓 User
根據不同的等級,持有不同種類的 WithdrawStrategy
,就可以簡化 Bank.withdraw()
方法、以及 withdraw()
本身的內容了。這便是利用了單一職責原則達到的程式改善。而這種「相對於把所有的可能性擠在一個方法內,對『方法』本身進行一次抽象,並實例化不同的『方法』類別,以達到不同方法可以根據情境進行實裝、調用」的行為,也一種常見的設計模式——策略模式的撰寫邏輯。