本章將薪資系統的資料庫層從靜態的記憶體實作重構為真正的 SQL Server 資料庫實作,過程中展示了如何在保持設計原則的前提下處理實際的持久化問題。

建構資料庫 Schema#

Figure 37.1: Payroll database schema

  • 資料庫 Schema 的設計反映了領域模型,但並非一對一對應:
    • Employee 表:存放員工基本資料(EmpId、Name、Address)
    • DirectDepositAccount 表:銀行帳戶直接存款的支付方式
    • PaycheckAddress 表:郵寄支票的支付方式
    • HourlyClassification 表SalariedClassification 表CommissionedClassification 表:三種薪資分類
    • TimeCard 表SalesReceipt 表:時薪員工的工時卡與佣金員工的銷售收據
    • UnionMember 表ServiceCharge 表:工會會員與服務費

重構 PayrollDatabase#

從靜態到介面#

  • 原本的 PayrollDatabase 是一個靜態類別,所有方法都是 static——這讓它無法被替換或模擬
  • 重構步驟:
    1. PayrollDatabase 從靜態類別改為介面
    2. 建立 InMemoryPayrollDatabase 實作原有的記憶體邏輯
    3. 建立 SqlPayrollDatabase 實作真正的資料庫存取
    4. 透過 FACTORY 模式注入正確的實作
public interface PayrollDatabase
{
    void AddEmployee(int id, Employee employee);
    Employee GetEmployee(int id);
    void DeleteEmployee(int id);
    void AddUnionMember(int memberId, Employee employee);
    Employee GetUnionMember(int memberId);
    void RemoveUnionMember(int memberId);
    ArrayList GetAllEmployeeIds();
}

補充: 這次重構的觸發點是要加入資料庫支援。這是敏捷開發的典型做法——不預先設計抽象,而是在需求出現時才進行重構。靜態類別在只有記憶體實作時完全可行,但當需要多種實作時就必須抽象化。

全系統的影響#

  • PayrollDatabase 改為介面後,所有使用 PayrollDatabase.Method() 的程式碼都必須修改為透過實例呼叫
  • 所有 Transaction 類別都需要持有 PayrollDatabase 的參考
  • 這是一次大範圍但機械式的重構——每個修改本身都很簡單,但涉及的檔案很多

SqlPayrollDatabase 實作#

SaveEmployee 與交易#

  • 儲存員工資料涉及多個表格(Employee、PaymentClassification、PaymentSchedule、PaymentMethod),必須使用資料庫交易(Transaction)確保一致性
public void AddEmployee(int id, Employee employee)
{
    SqlConnection connection = new SqlConnection(connectionString);
    connection.Open();
    SqlTransaction transaction = connection.BeginTransaction();

    try
    {
        SaveEmployeeOperation op = new SaveEmployeeOperation(
            employee, id, connection, transaction);
        op.Execute();
        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
    finally
    {
        connection.Close();
    }
}

SaveEmployeeOperation#

  • 將儲存邏輯封裝在 SaveEmployeeOperation 中,使用 COMMAND 模式
  • 內部根據員工的 Classification 類型決定如何儲存:
public void Execute()
{
    PrepareInsert();  // 插入 Employee 表
    SaveClassification(employee.Classification);
    SaveSchedule(employee.Schedule);
    SavePaymentMethod(employee.Method);
}

private void SaveClassification(PaymentClassification classification)
{
    if (classification is HourlyClassification hourly)
        SaveHourlyClassification(hourly);
    else if (classification is SalariedClassification salaried)
        SaveSalariedClassification(salaried);
    else if (classification is CommissionedClassification commissioned)
        SaveCommissionedClassification(commissioned);
}
flowchart TD
    A([AddEmployee]) --> B[開啟連線]
    B --> C[開始交易]
    C --> D[SaveEmployeeOperation.Execute]
    D --> E["PrepareInsert<br/>插入 Employee 表"]
    E --> F["SaveClassification<br/>儲存薪資分類"]
    F --> G["SaveSchedule<br/>儲存支付排程"]
    G --> H["SavePaymentMethod<br/>儲存支付方式"]
    H --> I{成功?}
    I -->|是| J[Commit 交易]
    I -->|否| K[Rollback 交易]
    J --> L[關閉連線]
    K --> L

注意: SaveClassification 中的 if/else 型別檢查違反了 OCP——新增 Classification 類型時必須修改此方法。作者承認這是一個已知的設計瑕疵,但在此階段選擇接受它,因為新增 Classification 類型的可能性很低。

LoadEmployeeOperation#

  • 從資料庫載入員工同樣複雜,需要根據資料判斷 Classification、Schedule、PaymentMethod 的具體類型
  • LoadEmployeeOperation 從各表格讀取資料,並組裝出完整的 Employee 物件
public Employee Execute()
{
    string sql = "SELECT * FROM Employee WHERE EmpId = @empId";
    // 執行查詢,取得 DataRow
    Employee employee = CreateEmployee(row);
    LoadClassification(employee);
    LoadSchedule(employee);
    LoadPaymentMethod(employee);
    return employee;
}

LoadPaymentMethodOperation 與 Delegate#

  • 支付方式的載入使用了 C# 的 delegate 來處理不同表格的查詢邏輯
  • 這減少了重複的資料庫存取程式碼

剩餘工作#

  • 本章僅實作了 AddEmployeeGetEmployee 的資料庫版本
  • 其他操作(如修改員工資料、計算薪資、工會管理等)的資料庫實作留作練習
  • 作者強調:模式已建立——後續的實作只是重複相同的 SaveOperation/LoadOperation 模式

重點: 資料庫層的實作暴露了一個根本性的設計張力——物件導向的多型機制(Strategy Pattern 的 Classification/Schedule/Method)在轉換為關聯式資料庫時,不可避免地需要型別判斷。這就是所謂的 Object-Relational Impedance Mismatch(物件-關聯阻抗不匹配)。

本章小結#

本章展示了從記憶體資料庫到真實 SQL 資料庫的遷移過程。關鍵步驟是先將靜態類別重構為介面(啟用替換能力),再實作具體的 SQL 版本。交易管理確保了資料一致性,而 SaveOperation/LoadOperation 模式提供了結構化的持久化邏輯。雖然過程中暴露了一些 OCP 違反,但這是物件-關聯映射中常見的務實妥協。