概述#
本章將上一章的核心模型付諸實作。所有操作都透過交易物件(Transaction)驅動,遵循 Command 模式。作者以測試驅動開發(TDD)的方式逐步建構系統,每個交易都先寫測試再寫實作。
Transaction 介面#
所有交易都實作 Transaction 介面,只有一個 Execute() 方法——這正是 Command 模式:

Figure 27.1: Transaction interface
新增員工(AddEmployeeTransaction)#
結構#
AddEmployeeTransaction 是一個使用 Template Method 模式的抽象類別。它定義了新增員工的通用流程,但將薪資分類和排程的建立延遲到子類別:

Figure 27.2: Static model of AddEmployeeTransaction
public abstract class AddEmployeeTransaction : Transaction
{
private int empId;
private string name;
private string address;
public void Execute()
{
PaymentClassification pc = MakeClassification();
PaymentSchedule ps = MakeSchedule();
PaymentMethod pm = new HoldMethod();
Employee e = new Employee(empId, name, address);
e.Classification = pc;
e.Schedule = ps;
e.Method = pm;
PayrollDatabase.AddEmployee(empId, e);
}
protected abstract PaymentClassification MakeClassification();
protected abstract PaymentSchedule MakeSchedule();
}三個具體子類別分別建立對應的分類和排程:
AddSalariedEmployee:建立SalariedClassification+MonthlyScheduleAddHourlyEmployee:建立HourlyClassification+WeeklyScheduleAddCommissionedEmployee:建立CommissionedClassification+BiweeklySchedule

Figure 27.4: Dynamic model for adding an employee
PayrollDatabase#
PayrollDatabase 是一個 Facade,使用靜態 Hashtable 作為全域資料儲存:
public class PayrollDatabase
{
private static Hashtable itsEmployees = new Hashtable();
private static Hashtable itsUnionMembers = new Hashtable();
public static void AddEmployee(int id, Employee e)
{
itsEmployees[id] = e;
}
public static Employee GetEmployee(int id)
{
return itsEmployees[id] as Employee;
}
}補充: 作者坦言
PayrollDatabase實質上是一個全域變數(global variable),這不是好的設計。但在此階段,它足以讓系統運作。資料庫的具體實作是可以延遲的決策——先讓系統正確運作,之後再替換成真正的資料庫。
刪除員工(DeleteEmployeeTransaction)#

Figure 27.5: Static model for DeleteEmployee transaction

Figure 27.6: Dynamic model for DeleteEmployee TRansaction
DeleteEmployeeTransaction 很直接——呼叫 PayrollDatabase.DeleteEmployee(empId) 即可。
登記時卡(TimeCardTransaction)#

Figure 27.7: Static structure of TimeCardTransaction
TimeCardTransaction 需要從資料庫取得員工,然後取得其 PaymentClassification,向下轉型(downcast)為 HourlyClassification,再加入時卡:
Employee e = PayrollDatabase.GetEmployee(empId);
HourlyClassification hc = e.Classification as HourlyClassification;
hc.AddTimeCard(new TimeCard(date, hours));注意: 這裡的向下轉型是不可避免的——只有
HourlyClassification才有時卡的概念。作者認為這是可以接受的務實做法,因為替代方案(如在PaymentClassification中加入所有子類別的方法)會更糟糕。
登記銷售收據(SalesReceiptTransaction)#

Figure 27.10: Dynamic model for SalesReceiptTransaction
類似 TimeCardTransaction,向下轉型為 CommissionedClassification 後加入銷售收據。
登記工會服務費(ServiceChargeTransaction)#

Figure 27.11: Static model for ServiceChargeTransaction

Figure 27.12: Dynamic model for ServiceChargeTransaction
ServiceChargeTransaction 使用的是工會會員 ID(memberId),而非員工 ID。它透過 PayrollDatabase.GetUnionMember(memberId) 取得員工,再向下轉型 Affiliation 為 UnionAffiliation 後加入服務費。
變更員工資料(ChangeEmployeeTransaction)#
結構#
變更員工資料的交易同樣使用 Template Method 模式。ChangeEmployeeTransaction 是抽象基底類別,定義通用流程(取得員工 -> 執行變更),具體變更內容延遲到子類別:

Figure 27.13: Static model for ChangeEmployeeTransaction

Figure 27.14: Dynamic model for ChangeEmployeeTransaction
public abstract class ChangeEmployeeTransaction : Transaction
{
private int empId;
public void Execute()
{
Employee e = PayrollDatabase.GetEmployee(empId);
if (e != null)
Change(e);
}
protected abstract void Change(Employee e);
}簡單變更#
ChangeNameTransaction:變更姓名

Figure 27.15: Dynamic model for ChangeNameTransaction
ChangeAddressTransaction:變更地址

Figure 27.16: Dynamic model for ChangeAddressTransaction
變更薪資分類(ChangeClassificationTransaction)#
ChangeClassificationTransaction 又是一層 Template Method——它設定新的 PaymentClassification 和 PaymentSchedule,但將具體類型的建立延遲到子類別:
ChangeHourlyTransaction:設定HourlyClassification+WeeklySchedule

Figure 27.18: Dynamic model of ChangeHourlyTransaction
ChangeSalariedTransaction:設定SalariedClassification+MonthlySchedule

Figure 27.19: Dynamic model of ChangeSalariedTransaction
ChangeCommissionedTransaction:設定CommissionedClassification+BiweeklySchedule

Figure 27.20: Dynamic Model of ChangeCommissionedTransaction
變更支付方式(ChangeMethodTransaction)#
同樣的 Template Method 結構:
ChangeDirectTransaction:設定DirectMethod

Figure 27.22: Dynamic model of ChangeDirectTransaction
ChangeMailTransaction:設定MailMethod

Figure 27.23: Dynamic model of ChangeMailTransaction
ChangeHoldTransaction:設定HoldMethod

Figure 27.24: Dynamic model of ChangeHoldTransaction
變更工會會籍(ChangeAffiliationTransaction)#

Figure 27.25: Dynamic model of ChangeAffiliationTransaction
ChangeMemberTransaction:將員工加入工會——設定UnionAffiliation,並在PayrollDatabase中記錄 memberId 對應關係ChangeUnaffiliatedTransaction:將員工從工會移除——設定NoAffiliation,並從PayrollDatabase中移除 memberId 記錄

Figure 27.27: Dynamic model of ChangeUnaffiliatedTransaction
重點: 作者在實作
ChangeMemberTransaction時發現,原本的 UML 圖遺漏了「在PayrollDatabase中記錄 memberId」這一步。他坦率地說:「我畫圖時到底在想什麼?」(What was I smoking?)這說明了 UML 圖是分析工具而非完美的規格書——實作時發現設計遺漏是正常的。
發放薪資(PaydayTransaction)#
結構#

Figure 27.28: Static model of PaydayTransaction
PaydayTransaction 遍歷所有員工,檢查今天是否為其發薪日。如果是,就建立 Paycheck 物件並讓員工計算薪資:
public void Execute()
{
foreach (int empId in PayrollDatabase.GetAllEmployeeIds())
{
Employee e = PayrollDatabase.GetEmployee(empId);
if (e.IsPayDate(payDate))
{
Paycheck pc = new Paycheck(e.GetPayPeriodStartDate(payDate), payDate);
itsPaychecks[empId] = pc;
e.Payday(pc);
}
}
}發薪流程#

Figure 27.30: Dynamic model scenario: "Payday is not today."

Figure 27.31: Dynamic model scenario: "Payday is today."

Figure 27.32: Dynamic model scenario: Posting payment
Employee.Payday() 方法委派給三個策略物件:
flowchart TD
A([PaydayTransaction.Execute]) --> B[遍歷所有員工]
B --> C{今天是發薪日?}
C -->|否| B
C -->|是| D[建立 Paycheck]
D --> E["Classification.CalculatePay()<br/>計算應付總額"]
E --> F["Affiliation.CalculateDeductions()<br/>計算扣款"]
F --> G["淨額 = 總額 − 扣款"]
G --> H["PaymentMethod.Pay()<br/>執行支付"]
H --> BPaymentClassification.CalculatePay(pc):計算應付總額Affiliation.CalculateDeductions(pc):計算扣款(工會會費 + 服務費)- 計算淨額:
grossPay - deductions PaymentMethod.Pay(pc):執行支付
支付時薪員工#
時薪員工的薪資計算需要遍歷所有時卡,找出落在本次支付期間內的時卡,加總工時:
- 正常工時(<= 8 小時)以基本時薪計算
- 超時工時(> 8 小時的部分)以 1.5 倍時薪計算
支付期間的設計問題#
在實作過程中,作者遇到一個設計問題:如何判斷一張時卡是否屬於當前支付期間?
最初考慮在 PaymentClassification 中加入 IsInPayPeriod() 方法,但後來發現這個資訊應該放在 Paycheck 物件中(它已經持有支付期間的起始和結束日期),最後抽取到一個 DateUtil 工具類別。
技巧: 這是一個很好的例子,說明在實作過程中發現更好的設計是正常且值得歡迎的。不要害怕重構——當你發現職責放錯了地方,就移動它。
工會扣款計算#
UnionAffiliation.CalculateDeductions() 需要計算兩部分:
- 會費:計算支付期間內有多少個週五(因為工會會費按週計算),乘以每週會費費率
- 服務費:遍歷所有服務費記錄,找出落在本次支付期間內的,加總金額
Employee 類別#
Employee 是系統的核心類別,它持有四個策略物件並將職責委派給它們:
public class Employee
{
private int empId;
private string name;
private string address;
private PaymentClassification classification;
private PaymentSchedule schedule;
private PaymentMethod method;
private Affiliation affiliation = new NoAffiliation();
public bool IsPayDate(DateTime date)
{
return schedule.IsPayDate(date);
}
public void Payday(Paycheck pc)
{
double grossPay = classification.CalculatePay(pc);
double deductions = affiliation.CalculateDeductions(pc);
double netPay = grossPay - deductions;
pc.GrossPay = grossPay;
pc.Deductions = deductions;
pc.NetPay = netPay;
method.Pay(pc);
}
}補充:
Affiliation的預設值是NoAffiliation——這是 Null Object 模式的應用。不屬於工會的員工不需要特別處理,NoAffiliation.CalculateDeductions()直接回傳零。
主程式#

Figure 27.33: Static model for the main program
PayrollApplication 使用 TransactionSource 介面來取得交易物件。這讓交易的來源(可能是文字檔、GUI 或網路)與薪資系統邏輯完全解耦。主程式的迴圈很簡單:
TransactionSource source = // ...
Transaction t = source.GetTransaction();
while (t != null)
{
t.Execute();
t = source.GetTransaction();
}資料庫#
作者在結論中強調:資料庫是實作細節。到目前為止,系統使用簡單的 Hashtable 運作良好。未來可以選擇:
- 物件導向資料庫(OODBMS)
- 平面檔案(Flat Files)
- 關聯式資料庫(RDBMS)
無論選擇哪種,核心薪資邏輯都不需要變更。
結論#
重點: 本章展示了大量的抽象和多型的使用。
Employee類別對薪資分類、支付排程、支付方式和工會會籍的變更都是封閉的(closed)——符合 OCP(開放封閉原則)。新增新的薪資類型、支付方式或會籍類型,只需要新增衍生類別,不需要修改Employee。整個系統使用了前面章節介紹的多種設計模式:Command(交易)、Template Method(AddEmployee、ChangeEmployee 階層)、Strategy(四個策略物件)、Facade(PayrollDatabase)、Null Object(NoAffiliation)。這些模式不是為了使用而使用,而是在解決具體問題的過程中自然浮現的。