MVP (Model-View-Presenter)
"The presenter acts as the middleman, mediating between the view and the model, ensuring that the view remains completely passive and testable." — Mike Potel, MVP: Model-View-Presenter (1996)
MVP (Model-View-Presenter) là một software architecture pattern được phát triển bởi Mike Potel tại Taligent (một công ty con của IBM) vào những năm 1990. MVP ra đời như một sự kế thừa và cải tiến của MVC (Model-View-Controller), giải quyết vấn đề chính của MVC: khả năng kiểm thử (testability) và tách biệt hoàn toàn giữa View và Model.
Tổng quan
Lịch sử và nguồn gốc
MVP được giới thiệu lần đầu tiên trong bài viết "Model-View-Presenter — The Taligent Programming Model for C++ and Java" của Mike Potel. Sau đó, nó được phổ biến rộng rãi bởi:
- Martin Fowler: Phân biệt giữa Passive View và Supervising Controller trong bài viết kinh điển
- Google: Sử dụng MVP làm kiến trúc chính cho các ứng dụng Android trước khi MVVM ra đời
- Microsoft: Áp dụng MVP trong Web Forms (.NET) — mô hình code-behind
- GWT (Google Web Toolkit): Framework Java cho web ứng dụng MVP làm pattern chính
So sánh với MVC
Trong MVC, Controller nhận input từ user và cập nhật Model; View quan sát Model để tự động cập nhật (qua Observer pattern). Trong MVP:
- View hoàn toàn thụ động (Passive View) — không biết gì về Model
- Presenter chịu trách nhiệm xử lý tất cả logic, cập nhật View qua interface
- Model chỉ đơn thuần là data/business logic
- View và Presenter giao tiếp qua một interface chung (thường gọi là
IView)
Các biến thể của MVP
| Biến thể | Mô tả |
|---|---|
| Passive View | View không chứa logic nào, Presenter điều khiển hoàn toàn |
| Supervising Controller | View xử lý data-binding đơn giản, Presenter xử lý logic phức tạp |
| Presentation Model | Presenter duy trì state của View (giống MVVM hơn) |
Bài toán
Hệ thống Dashboard Phân tích Dữ liệu Thương mại Điện tử
Giả sử bạn đang xây dựng một Dashboard phân tích dữ liệu thời gian thực cho một sàn thương mại điện tử lớn tại Việt Nam (Tiki, ShopeeFood, hoặc Sendo). Dashboard cần hiển thị:
- Tổng doanh thu hôm nay (real-time)
- Top 10 sản phẩm bán chạy
- Tỷ lệ chuyển đổi theo giờ
- Bản đồ nhiệt đơn hàng theo khu vực
- Thống kê lỗi thanh toán
- Cảnh báo bất thường (anomaly detection)
Thách thức trong phát triển
Một Dashboard điển hình trên web thường được xây dựng với kiến trúc MVC truyền thống. Tuy nhiên, khi ứng dụng phát triển, các vấn đề sau xuất hiện:
Vấn đề 1 — View phình to: Trong MVC, View thường chứa cả template rendering, event handling, và một phần logic hiển thị. Khi Dashboard có 50+ widget khác nhau, file View trở nên khổng lồ (3000+ dòng), khó bảo trì.
# MVC — View bị phình to
class DashboardView:
def render(self):
# 100 dòng render layout
self._render_header()
self._render_revenue_chart() # 50 dòng JS + HTML
self._render_top_products() # 30 dòng xử lý data
self._render_heatmap() # 80 dòng
self._render_alerts() # 40 dòng + logic filter
# ... cứ thế, view dần trở thành god object
Vấn đề 2 — Không thể unit test View: Trong MVC, View thường gắn chặt với DOM/UI framework. Bạn không thể test logic hiển thị nếu không chạy trình duyệt. Ví dụ:
# MVC — View logic không thể test
class RevenueWidget:
def update(self, data):
if data["growth_rate"] > 0.2:
self.element.style.color = "green"
self.show_arrow("up")
else:
self.element.style.color = "red"
self.show_arrow("down")
Làm sao để test dòng self.element.style.color = "green" mà không cần DOM?
Vấn đề 3 — Model và View gắn chặt: Khi Model thay đổi, View cần được cập nhật. Trong MVC, View thường đăng ký lắng nghe trực tiếp Model (Observer). Điều này tạo ra circular dependency và khó debug khi có 50+ widget cùng lắng nghe model.
Vấn đề 4 — Logic hiển thị phân tán: Khi business logic liên quan đến hiển thị (format tiền tệ, hiển thị % tăng trưởng, quyết định màu sắc dựa trên threshold) nằm rải rác cả ở View lẫn Controller, việc maintain consistency gần như bất khả thi.
MVP giải quyết vấn đề như thế nào
MVP giải quyết tất cả các vấn đề trên bằng cách:
- View chỉ là interface: View chỉ định nghĩa một interface (
IView), không chứa logic - Presenter xử lý mọi logic: Presenter quyết định hiển thị gì, khi nào, màu gì
- View hoàn toàn thụ động: View chỉ làm đúng những gì Presenter bảo (qua interface calls)
- Model độc lập: Model không biết gì về View hay Presenter
- Testability tối đa: Có thể mock View và test Presenter hoàn toàn bằng unit test
Nguyên lý thiết kế
1. Tách biệt hoàn toàn Presentation Layer
Mọi logic liên quan đến hiển thị (presentation logic) phải nằm trong Presenter, không phải View. Presenter là cầu nối duy nhất giữa Model và View.
2. View là Passive (Passive View)
View không chứa business logic. View chỉ:
- Hiển thị dữ liệu khi Presenter yêu cầu
- Chuyển tiếp sự kiện người dùng cho Presenter
- View không tự query Model, không tự format dữ liệu
3. Model là Domain Layer thuần túy
Model chỉ chứa business logic và data. Model không biết sự tồn tại của View hay Presenter. Model có thể được reuse ở bất kỳ đâu (API service, background job, CLI tool).
4. Giao tiếp qua Interface
View và Presenter giao tiếp qua interface:
IView: Định nghĩa các method mà Presenter có thể gọi trên ViewIPresenter: Định nghĩa các method mà View có thể gọi trên Presenter (event handlers)
5. Single Responsibility cho mỗi Presenter
Mỗi Presenter chỉ quản lý một màn hình / một widget. Nếu màn hình quá phức tạp, chia thành nhiều Presenter nhỏ.
6. Unit of Work pattern
Presenter thường kết hợp với Unit of Work để quản lý transaction khi thao tác với nhiều Model objects.
Cấu trúc chi tiết
Các thành phần
┌─────────────────────────────────────────────────┐
│ APPLICATION │
├─────────────────────────────────────────────────┤
│ ┌──────────┐ ┌────────────┐ │
│ │ View │────▶│ Presenter │ │
│ │(Interface)│◀────│ (Logic) │ │
│ └──────────┘ └─────┬──────┘ │
│ │ │
│ ┌────▼──────┐ │
│ │ Model │ │
│ │ (Domain) │ │
│ └───────────┘ │
├─────────────────────────────────────────────────┤
│ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ Services │ │ Data │ │ External │ │
│ │ │ │ Access │ │ Integrations │ │
│ └──────────┘ └───────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
1. View (Interface)
- Định nghĩa contract dưới dạng abstract class hoặc Protocol
- Các method setter để Presenter đẩy dữ liệu xuống
- Các event/signal để báo cho Presenter về hành động user
- Không chứa implementation logic
2. Presenter
- Nhận event từ View, xử lý logic, gọi Model
- Quyết định trạng thái hiển thị của View
- Quản lý lifecycle của màn hình
- Có thể inject Model qua constructor (DI)
3. Model
- Domain entities, business rules
- Data access, repositories
- Validation logic
- Hoàn toàn độc lập với UI
4. Services Layer
- API services
- Cache services
- Logging, analytics
- Authentication, authorization
Luồng tương tác
User Action → View → Presenter.handleEvent()
↓
Presenter calls Model
↓
Presenter updates View
View.displayData(data)
Quy tắc vàng
- View không bao giờ gọi Model trực tiếp
- Presenter không bao giờ tham chiếu đến View implementation — chỉ dùng interface
- Model không bao giờ import View hoặc Presenter
- Mỗi Presenter quản lý đúng một View
- View interface nên fine-grained (nhiều method nhỏ) thay vì một method render() lớn
- Presenter nên stateless, state lưu trong Model hoặc View state object
Sơ đồ kiến trúc
┌──────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌──────────────────────────────┐ ┌────────────────────────────┐ │
│ │ View (Passive) │ │ Presenter │ │
│ │ │ │ │ │
│ │ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │ │
│ │ │ DashboardView │ │ │ │ DashboardPresenter │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ + displayRevenue(d) │◀───┼───┼──│ + onViewLoaded() │ │ │
│ │ │ + displayTopProds() │ │ │ │ + onRefreshClick() │ │ │
│ │ │ + showAlert(msg) │◀───┼───┼──│ + onFilterChange(f) │ │ │
│ │ │ + showLoading(b) │ │ │ │ + onExportClick() │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ event onRefresh ────┼────┼───▶│ │ │ │
│ │ │ event onFilter ─────┼────┼───▶│ │ │ │
│ │ └──────────────────────┘ │ │ └──────────┬───────────┘ │ │
│ └──────────────────────────────┘ └─────────────┼──────────────┘ │
└───────────────────────────────────────────────────┼─────────────────┘
│
│ calls
▼
┌──────────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER (Model) │
│ ┌──────────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ RevenueService │ │ ProductService │ │ AlertService │ │
│ │ │ │ │ │ │ │
│ │ + getTodayRevenue()│ │ + getTopProducts()│ │ + getAlerts()│ │
│ │ + getRevenueByHour()│ │ + searchProduct()│ │ + acknowledge│ │
│ └──────────┬───────────┘ └────────┬─────────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────┬───────────┴───────────┬────────┘ │
│ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Repository │ │ External │ │
│ │ (Data) │ │ API │ │
│ └─────────────┘ └─────────────┘ │
└───────────────────────────────────────────────────── ─────────────────┘
Ví dụ code hoàn chỉnh
Cấu trúc project
dashboard/
├── __init__.py
├── model/
│ ├── __init__.py
│ ├── entities.py # Domain entities
│ ├── services.py # Business logic services
│ └── repositories.py # Data access
├── presenter/