本章以薪資系統的使用者介面為例,介紹 Model View Presenter(MVP) 模式——一種將業務邏輯與 UI 呈現徹底分離的架構模式,使業務邏輯可以在不依賴任何 UI 框架的情況下被測試。
介面設計#

Figure 38.1: Initial payroll user interface
- 薪資系統的 UI 需要一個「新增員工」的視窗
- 包含欄位:EmpId、Name、Address、薪資類型(Hourly/Salaried/Commissioned)、以及對應的金額欄位
- 設計目標:UI 邏輯與業務邏輯完全分離
MODEL VIEW PRESENTER 架構#

Figure 38.4: Design of the Payroll view
- MVP 將系統分為三個角色:
- Model:業務邏輯與資料(在本例中是
PayrollDatabase與 Transaction 類別) - View:UI 呈現層,只負責顯示資料與接收使用者輸入(在本例中是
AddEmployeeWindow) - Presenter:協調者,從 View 取得使用者輸入,操作 Model,再更新 View
- Model:業務邏輯與資料(在本例中是
為什麼不是 MVC?#
- MVP 與 MVC 的差異在於控制流的方向:
- 在 MVC 中,View 直接觀察 Model 的變化
- 在 MVP 中,所有的互動都經過 Presenter——View 不直接與 Model 互動
- MVP 的優勢:Presenter 完全不依賴 UI 框架,可以用純粹的單元測試驗證
sequenceDiagram
actor User as 使用者
participant View as AddEmployeeWindow<br/>(View)
participant Presenter as AddEmployeePresenter<br/>(Presenter)
participant Tx as AddHourlyEmployee<br/>(Transaction)
participant DB as PayrollDatabase<br/>(Model)
User->>View: 填入員工資料
User->>View: 點擊 Submit
View->>Presenter: AddEmployee()
Presenter->>View: 讀取 EmpId, Name, Address
Presenter->>View: 讀取 IsHourly, HourlyRate
Presenter->>Tx: new AddHourlyEmployee(...)
Presenter->>Tx: Execute()
Tx->>DB: AddEmployee(id, employee)以測試驅動開發 Presenter#
AddEmployeeView 介面#
- 定義 View 的抽象介面,讓 Presenter 不依賴具體的 UI 實作:
public interface AddEmployeeView
{
int EmpId { get; }
string EmployeeName { get; }
string EmployeeAddress { get; }
bool IsHourly { get; }
bool IsSalaried { get; }
bool IsCommissioned { get; }
double HourlyRate { get; }
double Salary { get; }
double CommissionRate { get; }
double BaseSalary { get; }
}MockAddEmployeeView#
- 測試時使用 Mock View,直接設定屬性值:
public class MockAddEmployeeView : AddEmployeeView
{
public int EmpId { get; set; }
public string EmployeeName { get; set; }
public string EmployeeAddress { get; set; }
public bool IsHourly { get; set; }
public bool IsSalaried { get; set; }
public bool IsCommissioned { get; set; }
public double HourlyRate { get; set; }
public double Salary { get; set; }
public double CommissionRate { get; set; }
public double BaseSalary { get; set; }
}AddEmployeePresenter#
public class AddEmployeePresenter
{
private AddEmployeeView view;
private PayrollDatabase database;
public AddEmployeePresenter(
AddEmployeeView view, PayrollDatabase database)
{
this.view = view;
this.database = database;
}
public void AddEmployee()
{
int empId = view.EmpId;
string name = view.EmployeeName;
string address = view.EmployeeAddress;
if (view.IsHourly)
{
AddHourlyEmployee(empId, name, address);
}
else if (view.IsSalaried)
{
AddSalariedEmployee(empId, name, address);
}
else if (view.IsCommissioned)
{
AddCommissionedEmployee(empId, name, address);
}
}
private void AddHourlyEmployee(int id, string name, string address)
{
AddHourlyEmployee t = new AddHourlyEmployee(
id, name, address, view.HourlyRate, database);
t.Execute();
}
// 其餘方法同理
}測試 Presenter#
[Test]
public void AddHourlyEmployee()
{
MockAddEmployeeView view = new MockAddEmployeeView();
view.EmpId = 1;
view.EmployeeName = "Andrew";
view.EmployeeAddress = "123 Main St";
view.IsHourly = true;
view.HourlyRate = 25.0;
InMemoryPayrollDatabase database = new InMemoryPayrollDatabase();
AddEmployeePresenter presenter =
new AddEmployeePresenter(view, database);
presenter.AddEmployee();
Employee e = database.GetEmployee(1);
Assert.AreEqual("Andrew", e.Name);
Assert.IsTrue(e.Classification is HourlyClassification);
}技巧: MVP 的測試完全不需要啟動 UI 框架——所有的業務邏輯驗證都在 Presenter 層完成,使用 Mock View 提供輸入資料。這讓測試既快速又穩定。
建構實際的 UI#
AddEmployeeWindow#
AddEmployeeWindow繼承Form(Windows Forms)並實作AddEmployeeView介面- 它只負責:
- 將 UI 控制項的值暴露為
AddEmployeeView的屬性 - 在按鈕點擊時呼叫
Presenter.AddEmployee() - 根據選擇的薪資類型顯示/隱藏對應的輸入欄位
- 將 UI 控制項的值暴露為
public class AddEmployeeWindow : Form, AddEmployeeView
{
private AddEmployeePresenter presenter;
public AddEmployeeWindow()
{
presenter = new AddEmployeePresenter(
this, GpayrollDatabase.instance);
// 初始化 UI 控制項...
}
public int EmpId
{
get { return int.Parse(empIdTextBox.Text); }
}
public string EmployeeName
{
get { return nameTextBox.Text; }
}
private void submitButton_Click(object sender, EventArgs e)
{
presenter.AddEmployee();
}
// ...
}重點:
AddEmployeeWindow中幾乎沒有業務邏輯——所有的決策(如「選擇哪種 Transaction」)都在 Presenter 中完成。View 只是一個被動的資料容器與事件轉發器。
MVP 的優勢#
- 可測試性:Presenter 的所有邏輯都可以透過 Mock View 測試,不需要 UI 框架
- 可替換性:同一個 Presenter 可以搭配不同的 View 實作(Windows Forms、Web、Console)
- 關注點分離:View 專注於呈現,Presenter 專注於協調,Model 專注於業務規則
- 行為驅動:先寫 Presenter 的測試來定義行為,再建構 View 來滿足介面
補充: 作者在本章中選擇先寫測試、再寫 Presenter、最後才建構 UI——這是 TDD(測試驅動開發)在 UI 開發中的實踐。MVP 模式使得這種開發順序成為可能。
本章小結#
MVP 模式透過將 View 抽象為介面,讓 Presenter 可以在完全不依賴 UI 框架的情況下被開發與測試。View 是被動的——它只提供資料與接收事件,所有的業務決策都由 Presenter 做出。這種架構讓 UI 開發也能享受 TDD 的好處:先定義行為、再實作介面。