概述#

本章開始進入薪資系統(Payroll System)的案例研究。這是一個簡單的批次薪資應用程式,透過交易(transaction)驅動。系統需要根據指定的排程支付員工薪水,並依照各種規則計算應付金額和扣款。

初步規格#

系統需要處理三種員工類型:

  • 時薪員工(Hourly):依照時卡(time card)記錄的工時計薪,每週五支付。時薪超過 8 小時的部分以 1.5 倍計算。
  • 月薪員工(Salaried):固定月薪,每月最後一個工作日支付
  • 底薪加佣金員工(Commissioned):固定底薪加上銷售收據(sales receipt)的佣金比例,每隔一週的週五支付

支付方式有三種選擇:

  • 郵寄支票(Mail)到指定地址
  • 銀行轉帳(Direct Deposit)到指定帳戶
  • 留存於出納(Hold),由出納保管

部分員工屬於工會(Union)。工會會費按週從薪水扣除,工會也可能對個別會員徵收服務費(service charge),從下次薪資中扣除。

Use Cases#

Use Case 1:新增員工#

系統接收 AddEmp 交易,包含員工的基本資料(ID、名字、地址)和薪資分類資訊。

Use Case 2:刪除員工#

系統接收 DelEmp 交易,以員工 ID 刪除指定員工。

Use Case 3:登記時卡#

系統接收 TimeCard 交易,記錄時薪員工的工作日期和時數。

Use Case 4:登記銷售收據#

系統接收 SalesReceipt 交易,記錄佣金員工的銷售日期和金額。

Use Case 5:登記工會服務費#

系統接收 ServiceCharge 交易,記錄工會會員的服務費。

Use Case 6:變更員工資料#

系統接收 ChgEmp 交易,可變更員工的姓名、地址、薪資分類、支付方式、工會會籍等。

Use Case 7:發放薪資#

系統接收 Payday 交易,產生當天應付薪資的員工薪資記錄,包含應付金額和扣款。

初步設計反思#

從 Use Case 思考類別結構#

最直覺的做法是根據員工薪資類型建立繼承階層:

Figure 26.2: Possible Employee class hierarchy

但作者在分析 Use Case 6(變更員工資料)時發現這個設計有問題——如果時薪員工需要變更為月薪員工,在繼承體系下意味著要銷毀原有物件並建立新物件,這是很不方便的。

重點: 作者指出,Use Case 6 暗示著不應該用繼承來區分員工薪資類型。當員工的薪資分類可以動態變更時,應該使用 Strategy 模式——將薪資計算、支付方式等抽取為獨立的策略物件,讓 Employee 透過委派來使用它們。這樣變更薪資類型只需要替換策略物件,不需要替換員工物件本身。

資料庫是實作細節#

作者提出一個重要的設計原則:資料庫是實作細節,應該盡可能延遲決定。在初步設計中不應該考慮使用什麼資料庫技術,因為抽象的薪資系統概念不需要依賴資料庫的存在。

補充: 在此階段,作者選擇將員工資料暫存在簡單的資料結構中(如 Hashtable),將資料庫的具體實作推遲到系統運作正確之後再處理。

核心模型#

經過反覆分析,最終設計出薪資系統的核心模型:

Figure 26.6: Revised class diagram for Payroll: the core model

Employee 類別持有三個策略物件:

  • PaymentClassification:負責計算薪資(如何算錢)
  • PaymentSchedule:負責判斷支付時程(何時發錢)
  • PaymentMethod:負責處理支付方式(怎麼發錢)

PaymentClassification 抽象#

PaymentClassification 是一個抽象類別,有三個衍生類別:

  • HourlyClassification:持有時薪費率和時卡集合,依工時計算薪資

Figure 26.8: Calculating an hourly employee's pay

  • CommissionedClassification:持有底薪、佣金比例和銷售收據集合

Figure 26.9: Calculating a commissioned employee's pay

  • SalariedClassification:持有月薪金額

Figure 26.10: Calculating a salaried employee's pay

PaymentSchedule 抽象#

在初步分析 Use Case 時,支付排程似乎不需要獨立抽象——時薪員工總是每週五付、月薪員工總是月底付。但作者深入思考後發現,支付排程與薪資分類並非總是綁定的:如果客戶未來想要月薪員工也改成每週發薪呢?

Figure 26.11: Static model of a Schedule abstraction

抽取 PaymentSchedule 後,三個衍生類別各自負責判斷特定日期是否為發薪日:

  • WeeklySchedule:每週五
  • MonthlySchedule:每月最後一個工作日
  • BiweeklySchedule:每隔一週的週五

技巧: 這展示了尋找底層抽象(Finding Underlying Abstractions)的重要性。表面上看起來緊密綁定的概念(薪資分類與支付排程),在深入分析後可能是獨立的維度。將它們分離後,系統對未來需求的變更具有更好的擴展性。

PaymentMethod 抽象#

支付方式是 Use Case 中已經明確描述的三種選項。PaymentMethod 介面有三個實作:

  • HoldMethod:將支票留存於出納
  • DirectMethod:銀行轉帳
  • MailMethod:郵寄支票

Affiliation 抽象#

工會會籍也被抽取為獨立的抽象——Affiliation 介面:

Figure 26.13: Static structure of Affiliation abstraction

Figure 26.14: Dynamic structure of Affiliation abstraction

  • UnionAffiliation:持有會費費率和服務費集合,負責計算工會相關的扣款
  • NoAffiliation:代表不屬於任何工會,扣款計算回傳零(這是一種 Null Object 模式的應用)

關鍵設計決策#

  1. Strategy 優於繼承Employee 不繼承不同的薪資子類別,而是持有可替換的策略物件
  2. 資料庫是實作細節:延遲資料庫決策,先用記憶體資料結構
  3. 分離獨立的變化維度:薪資分類、支付排程、支付方式、工會會籍各自獨立變化
  4. Null Object 的應用:未加入工會的員工使用 NoAffiliation,避免到處檢查 null
  5. 交易驅動設計:所有操作都透過交易物件執行,這是 Command 模式的應用

補充: 本章還沒有寫任何程式碼。作者透過分析 Use Cases 和反覆推敲設計,建立了系統的核心抽象模型。這正是敏捷設計的精神——在開始編碼前,透過適度的分析建立足夠的理解,但不過度設計。下一章將開始實作這個模型。