概述#

本章將上一章的核心模型付諸實作。所有操作都透過交易物件(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 + MonthlySchedule
  • AddHourlyEmployee:建立 HourlyClassification + WeeklySchedule
  • AddCommissionedEmployee:建立 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) 取得員工,再向下轉型 AffiliationUnionAffiliation 後加入服務費。

變更員工資料(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——它設定新的 PaymentClassificationPaymentSchedule,但將具體類型的建立延遲到子類別:

  • 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 --> B
  1. PaymentClassification.CalculatePay(pc):計算應付總額
  2. Affiliation.CalculateDeductions(pc):計算扣款(工會會費 + 服務費)
  3. 計算淨額:grossPay - deductions
  4. 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)。這些模式不是為了使用而使用,而是在解決具體問題的過程中自然浮現的。