Software Design - Pattern - Model View Presenter (MVP)
Quick Chat
在 Unity 開發中,MVP(Model-View-Presenter)是我最常用的架構模式。相比之下,MVC(Model-View-Controller)在實作上常有模糊地帶,Controller 容易與 View 過度耦合;而 MVVM(Model-View-ViewModel)則仰賴 Data Binding 機制的支援,會限制 View 的寫法與彈性。
MVP 的概念相對純粹,能清楚劃分各層的責任。在「關注點分離」(Separation of Concerns)的原則下,MVP 的構成如下:
- Model:負責資料處理與業務邏輯,不涉及畫面呈現,專注於商業規則。
- View:負責 UI 與使用者互動。在理想情況下,View 是「被動的」,只根據 Presenter 指令更新畫面,並將使用者操作回報給 Presenter。
- Presenter:作為 View 與 Model 的中介者,從 Model 取得資料並整理後交給 View 顯示;同時處理來自 View 的事件並驅動 Model。
Guide
Dependency
在 MVP 模式中,Presenter 與 View 的互動方式主要有兩種,其核心差異在於「依賴方向」。
1. View 依賴 Presenter (Supervising Controller)
在這種做法中,View 會持有 Presenter 的引用。當發生使用者操作(如按鈕點擊),View 直接呼叫 Presenter 的方法處理。
- 流程:
使用者操作 → View → Presenter → Model → Presenter → View 更新
- 優點:實作直觀,邏輯流暢。
- 缺點:View 直接依賴具體 Presenter,降低可重用性,也讓 View 測試變得困難。
2. Presenter 依賴 View (Passive View)
此風格中,Presenter 持有 View 的引用。常見做法是透過介面(Interface)與 View 溝通,但是否需要抽象化,取決於專案需求。
View 的責任僅在於:
定義自身能提供的操作(方法)。
對外發出事件(例如
Observable
、UnityEvent
、delegate
)。流程:
使用者操作 → View 發出事件 → Presenter 監聽並處理 → Model → Presenter 呼叫 View 更新
優點:
- 解耦性:若使用介面,View 可完全獨立於 Presenter,具備替換或重用的彈性。
- 可測試性:可用 Mock View 單獨測試 Presenter。
缺點 / 實務考量:
- 直接依賴具體 View 雖增加耦合,但能減少程式複雜度。
- 如果僅是 UI「換皮」,通常不會更換整個 View 類別,介面抽象的價值有限。
- Presenter 的測試必要性值得思考:若 Model 已測試、View 也有測試,Presenter 多數僅扮演資料轉換與搬運的角色,單獨測 Presenter 的收益未必高。
Example - Passive View
public class View : MonoBehaviour
{
[SerializeField] private Text messageText;
[SerializeField] private Button[] playerChoices;
[SerializeField] private Button nextButton;
private readonly Subject<Choice> playerChoiceSelected = new Subject<Choice>();
private readonly CompositeDisposable disposables = new CompositeDisposable();
public IObservable<Choice> PlayerChoiceSelected => playerChoiceSelected;
public IObservable<Unit> PlayerNextRequested => nextButton.onClick.AsObservable();
void Awake()
{
disposables.Clear();
playerChoices[0].onClick.AsObservable().Subscribe(_ => SelecteChoice(Choice.Rock)).AddTo(disposables);
playerChoices[1].onClick.AsObservable().Subscribe(_ => SelecteChoice(Choice.Paper)).AddTo(disposables);
playerChoices[2].onClick.AsObservable().Subscribe(_ => SelecteChoice(Choice.Scissors)).AddTo(disposables);
}
void OnDestroy()
{
disposables.Clear();
}
public void SetReady(string message)
{
messageText.text = message;
nextButton.gameObject.SetActive(false);
}
public void SetResult(string message)
{
messageText.text = message;
nextButton.gameObject.SetActive(true);
}
private void SelecteChoice(Choice choice)
{
playerChoiceSelected.OnNext(choice);
}
}