本章將薪資系統的資料庫層從靜態的記憶體實作重構為真正的 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——這讓它無法被替換或模擬 - 重構步驟:
- 將
PayrollDatabase從靜態類別改為介面 - 建立
InMemoryPayrollDatabase實作原有的記憶體邏輯 - 建立
SqlPayrollDatabase實作真正的資料庫存取 - 透過 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 來處理不同表格的查詢邏輯
- 這減少了重複的資料庫存取程式碼
剩餘工作#
- 本章僅實作了
AddEmployee與GetEmployee的資料庫版本 - 其他操作(如修改員工資料、計算薪資、工會管理等)的資料庫實作留作練習
- 作者強調:模式已建立——後續的實作只是重複相同的 SaveOperation/LoadOperation 模式
重點: 資料庫層的實作暴露了一個根本性的設計張力——物件導向的多型機制(Strategy Pattern 的 Classification/Schedule/Method)在轉換為關聯式資料庫時,不可避免地需要型別判斷。這就是所謂的 Object-Relational Impedance Mismatch(物件-關聯阻抗不匹配)。
本章小結#
本章展示了從記憶體資料庫到真實 SQL 資料庫的遷移過程。關鍵步驟是先將靜態類別重構為介面(啟用替換能力),再實作具體的 SQL 版本。交易管理確保了資料一致性,而 SaveOperation/LoadOperation 模式提供了結構化的持久化邏輯。雖然過程中暴露了一些 OCP 違反,但這是物件-關聯映射中常見的務實妥協。