MVP (Model-View-Presenter) — الگوی معماری که در آن Presenter از طریق واسط ViewContract بین Model و View میانجیگری میکند. برخلاف MVC که در آن Controller مستقیماً View را از طریق UIKit مدیریت میکند، Presenter به چارچوب وابسته نیست — از طریق انتزاع کار میکند که آن را بدون Android SDK یا UIKit قابل آزمایش میسازد. MVP به طور گسترده در توسعه Android قبل از ظهور Jetpack استفاده میشد و همچنان برای پروژههای legacy مرتبط است. بیشتر در مقاله مارتین فاولر.
نکات اصلی
MVP (Model-View-Presenter) — الگوی معماری است که توسط مارتین فاولر در اوایل دهه ۲۰۰۰ به عنوان تکامل MVC برای بهبود قابلیت آزمایش رابط کاربری پیشنهاد شد. Model دادهها و منطق تجاری را مدیریت میکند، View مسئول نمایش و پردازش ورودی کاربر است، Presenter — مؤلفه مرکزی که رویدادها را از View دریافت میکند، دادهها را از Model استخراج میکند و وضعیت را برای نمایش تشکیل میدهد.
تفاوت اصلی MVP با MVC — Presenter ارجاع مستقیم به View ندارد. در عوض Presenter از طریق واسط ViewContract با View تعامل میکند. View این واسط را پیادهسازی میکند و خود را به Presenter منتقل میکند. این کار وابستگی به UIKit (iOS) یا Android Framework را قطع میکند — Presenter به صورت ایزوله با پیادهسازی mock از ViewContract آزمایش میشود. در MVC کنترلر UIViewController مستقیماً UILabel را بهروز میکند، در MVP Presenter متد view.showName(name) را فراخوانی میکند و View تصمیم میگیرد چگونه نمایش دهد.
| مؤلفه | مسئولیت | قابلیت آزمایش |
|---|---|---|
| Model | دادهها، منطق تجاری، فراخوانیهای شبکه | تستهای واحد (وابسته به UI نیست) |
| View | نمایش UI، انتقال رویدادها به Presenter | پیادهسازی mock از طریق واسط |
| Presenter | منطق تجاری، مدیریت وضعیت، ناوبری | تستهای واحد (از طریق mock ViewContract) |
اصل مسئولیت واحد در MVP دقیقتر از MVC رعایت میشود: View فقط مسئول رندرینگ است، Model — مسئول دادهها، Presenter — مسئول منطق و هماهنگی. در پروژههای واقعی Presenter 40–60٪ کد صفحه را تشکیل میدهد، View — 20–30٪، Model — 20–30٪. چنین تقسیمبندی اجازه میدهد منطق تجاری کلیدی را بدون راهاندازی شبیهساز Android یا شبیهساز iOS آزمایش کنید.
MVP در Android از Activity یا Fragment به عنوان View که ViewContract را پیادهسازی میکند استفاده میکند — واسطی با متدهای نمایش دادهها. Presenter در Activity ایجاد میشود، View را به خود مشترک میکند و بارگذاری دادهها را مدیریت میکند. هنگام چرخش صفحه Activity بازآفرینی میشود — Presenter میتواند از طریق retain-fragment یا ذخیرهسازی خارجی حفظ شود که مشکل از دست دادن وضعیت مشخصه MVC خالص را حل میکند.
// ViewContract — واسط برای ارتباط Presenter با View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — لایه منطق قابل آزمایش
class UserPresenter(
private val repository: UserRepository
) {
private var view: UserView? = null
fun attachView(view: UserView) {
this.view = view
}
fun detachView() {
view = null
}
fun loadUser(userId: Int) {
view?.showLoading()
repository.getUser(userId) { result ->
view?.hideLoading()
result.onSuccess { user ->
view?.showUser(user)
}.onFailure { e ->
view?.showError(e.message ?: "خطای ناشناخته")
}
}
}
}
// View (Activity) واسط را پیادهسازی میکند
class UserActivity : AppCompatActivity(), UserView {
private val presenter = UserPresenter(UserRepository())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
presenter.loadUser(42)
}
override fun onDestroy() {
presenter.detachView()
super.onDestroy()
}
override fun showUser(user: User) { /* بهروزرسانی UI */ }
override fun showLoading() { /* نمایش ProgressBar */ }
override fun hideLoading() { /* مخفی کردن ProgressBar */ }
override fun showError(message: String) { /* نمایش Snackbar */ }
}
مدیریت چرخه حیات — مشکل کلیدی MVP در Android. Activity هنگام چرخش صفحه از بین میرود و presenter.attachView() دوباره در onCreate() فراخوانی میشود. اگر بارگذاری داده ناهمگام باشد (RxJava، کروتینها)، در زمان تکمیل ممکن است View جدا شده باشد. راهحل — لغو اشتراکها در detachView() یا استفاده از Loader از Support Library (برای پروژههای بدون Jetpack). در IT Sectr ما سالها از ترکیب MVP + RxJava در پروژههای تجاری استفاده کردیم — الگو پایدار است اما نیاز به انضباط در مدیریت اشتراکها دارد.
Retain-fragmentها — مکانیزم حفظ Presenter هنگام چرخش صفحه. Fragment بدون UI (setRetainInstance(true)) بیشتر از Activity عمر میکند و ارجاع به Presenter را نگه میدارد. هنگام بازآفرینی Activity، fragment همان Presenter را به Activity جدید منتقل میکند. Retain-fragmentها از AndroidX منسوخ شدهاند، اما معادل pre-Jetpack آنها (Fragment.setRetainInstance) هنوز در پروژههای legacy کار میکند. در توسعه مدرن Google به جای retain-fragmentها ViewModel را توصیه میکند.
MVP در iOS از طریق پروتکل View ساخته میشود. UIViewController پروتکل را پیادهسازی میکند، Presenter UIKit را وارد نمیکند و به طور خالص آزمایش میشود. برخلاف Apple MVC، که در آن UIViewController خود منطق و اتصالات مستقیم IBOutlet را شامل میشود، Presenter وضعیت را مدیریت میکند و از طریق متدهای پروتکل به View دستور میدهد. View تصمیم نمیگیرد — دستورات Presenter را اجرا میکند: showUser، showLoading، navigateToProfile.
import Foundation
// View Protocol — انتزاع برای Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — منطق خالص، بدون UIKit
final class UserPresenter {
private weak var view: UserViewProtocol?
private let service: UserServiceProtocol
init(service: UserServiceProtocol) {
self.service = service
}
func attach(view: UserViewProtocol) {
self.view = view
}
func detach() {
view = nil
}
func loadUser(id: Int) {
view?.showLoading()
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
self.view?.hideLoading()
switch result {
case .success(let user):
self.view?.display(user: user)
case .failure(let error):
self.view?.displayError(message: error.localizedDescription)
}
}
}
}
// View (UIViewController) پروتکل را پیادهسازی میکند
final class UserViewController: UIViewController, UserViewProtocol {
private let presenter = UserPresenter(service: UserService())
override func viewDidLoad() {
super.viewDidLoad()
presenter.attach(view: self)
presenter.loadUser(id: 42)
}
func display(user: User) {
nameLabel.text = user.name
}
// ... بقیه متدهای پروتکل
}
Weak reference به View — شرط اجباری در iOS MVP. UIViewController ممکن است از بین برود (pop از پشته ناوبری) و closure آن در Presenter یک retain cycle ایجاد میکند. ارجاع ضعیف (weak var) تضمین میکند که View وقتی از صفحه خارج میشود، صرفنظر از وجود عملیاتهای ناهمگام در Presenter، آزاد میشود. در Android مشکل مشابه از طریق detachView() حل میشود — فراخوانی در onDestroy() ارجاع به View را صفر میکند.
Passive View vs Supervising Controller — دو نسخه MVP از مارتین فاولر. Passive View: View منطقی ندارد، Presenter کاملاً وضعیت را مدیریت میکند. Supervising Controller: View خود اتصال ساده داده را انجام میدهد (مثلاً از طریق data binding)، Presenter فقط در سناریوهای پیچیده دخالت میکند. در توسعه موبایل Passive View بیشتر استفاده میشود — حداکثر قابلیت آزمایش و پیشبینیپذیری وضعیت صفحه را میدهد.
تفاوت اصلی MVP و MVC — روش ارتباط با View. در MVC Controller ارجاع مستقیم به View دارد (UIViewController.IBOutlets، Activity.findViewById). در MVP Presenter از طریق واسط ViewContract با View تعامل میکند. این تفاوت قابلیت آزمایش را به طور اساسی تغییر میدهد: یک شیء Mock که ViewContract را پیادهسازی میکند امکان بررسی منطق Presenter را بدون راهاندازی برنامه، شبیهساز و چارچوب UI فراهم میکند.
| معیار | MVC | MVP |
|---|---|---|
| ارتباط با View | مستقیم (Controller → View) | از طریق واسط (Presenter → ViewContract) |
| آزمایش منطق | نیاز به UIKit/Android Framework دارد | تستهای واحد بدون وابستگیهای پلتفرمی |
| چرخه حیات | Controller با صفحه زندگی میکند | Presenter میتواند بیشتر عمر کند (retain) |
| پیچیدگی | حداقل | +1 واسط به ازای هر صفحه |
| Massive Controller | مشکل معمول | منطق در Presenter، View نازک |
نمونه تست واحد Presenter در Kotlin: یک mock UserView ساخته میشود، به Presenter منتقل میشود، loadUser فراخوانی میشود، بررسی میشود که showUser با دادههای صحیح فراخوانی شده است. تست در میلیثانیه اجرا میشود، به شبیهساز نیاز ندارد. در iOS به طور مشابه — OCMock یا stub پروتکل فراخوانیهای متد UserViewProtocol را بررسی میکند. در پروژههای IT Sectr با MVP پوشش منطق تجاری با تستهای واحد به 85–90٪ میرسید که 2–3 برابر بیشتر از پروژههای مشابه MVC است.
زمانی که MVP بر MVC ترجیح داده میشود — در پروژههایی با الزامات سختگیرانه برای پایداری: برنامههای بانکی، سیستمهای پزشکی، پایانههای پرداخت. در این حوزهها هزینه خطا بالا است و تستهای واحد حیاتی هستند. در پروژههای Post-MVP (زمانی که محصول در بازار است اما پایگاه کد legacy است) MVP اجازه میدهد منطق به تدریج از Massive View Controller به لایه قابل آزمایش منتقل شود بدون بازنویسی کامل معماری.
معایب اصلی MVP — افزایش تعداد واسطها و مدیریت دستی اشتراکها. هر صفحه حداقل به یک ViewContract + Presenter نیاز دارد، برای ۵۰ صفحه — ۵۰ واسط و ۵۰ کلاس Presenter. در MVVM ViewModel جایگزین Presenter میشود و از مکانیزمهای واکنشی (LiveData، StateFlow، ObservableObject) استفاده میکند که نیاز به attach/detach دستی و واسطهای ViewContract را از بین میبرد.
RxJava و MVP — ترکیب محبوب در Android 2015–2019. Presenter در Observable از Repository مشترک میشود، نتیجه را از طریق ViewContract نمایش میدهد. مشکل: disposable باید به صراحت در detachView() لغو شود وگرنه نشت اشتراک هنگام بهروزرسانی View جدا شده باعث crash میشود. کتابخانههای RxLifecycle و AutoDispose تا حدودی لغو اشتراک را خودکار کردند اما وابستگی اضافه کردند. در IT Sectr در سال ۲۰۲۰ از MVP+RxJava به MVVM+Flow مهاجرت کردیم — کد به دلیل حذف ViewContract 25–30٪ کوتاهتر شد.
مهاجرت از MVP به MVVM — فرآیند مرحلهای. ۱) جایگزینی ViewContract با LiveData/StateFlow در Presenter. ۲) حذف متدهای attach/detach — اشتراک از طریق observe() انجام میشود. ۳) تغییر نام Presenter به ViewModel. ۴) یکپارچهسازی DI (Hilt/Koin) برای ViewModelFactory. مهاجرت یک صفحه ۲–۴ ساعت طول میکشد، کل پایگاه کد — ۲–۴ هفته برای پروژهای با ۵۰–۱۰۰ صفحه. پس از مهاجرت واسطهای ViewContract حذف میشوند، کد کوتاه میشود، تستها باقی میمانند.
MVP در توسعه مدرن — الگو زنده است اما به MVVM و MVI میبازد. Google رسماً MVVM با Jetpack را برای پروژههای جدید توصیه میکند. Apple — MVVM با SwiftUI. با این حال آشنایی با MVP برای کار با کد legacy ضروری است: صدها برنامه Android در Google Play هنوز بر روی MVP کار میکنند، از جمله برنامههای بانکهای بزرگ، خردهفروشان و شرکتهای حمل و نقل. درک MVP پایهای برای تسلط بر MVI و Clean Architecture است زیرا Presenter جد مستقیم Use Case در اصطلاحات رابرت مارتین است.
سوالات متداول
در MVP Presenter از طریق واسط ViewContract با View تعامل میکند نه مستقیماً. در MVC Controller ارجاع مستقیم به View از طریق IBOutlet/findViewById دارد. MVP اجازه میدهد Presenter با تستهای واحد بدون iOS Simulator یا Android Emulator آزمایش شود زیرا Presenter به UIKit یا Android Framework وابسته نیست. MVC برای آزمایش کنترلر نیاز به راهاندازی برنامه دارد.
MVP در پروژههای legacy که قبلاً بر این الگو ساخته شدهاند و در برنامههایی بدون پشتیبانی از مکانیزمهای واکنشی (LiveData، StateFlow، Combine) توجیه دارد. برای پروژههای جدید Google MVVM با Jetpack (Android) و Apple MVVM با SwiftUI (iOS) را توصیه میکند. MVP بهترین انتخاب برای پروژههای UIKit خالص بدون Combine با نیاز به تستهای واحد منطق تجاری باقی میماند.
در Android — استفاده از retain-fragment (setRetainInstance(true)) یا ViewModel از Jetpack. Retain-fragment Presenter را هنگام چرخش حفظ میکند و به Activity جدید منتقل میکند. ViewModel گوگل — جایگزین مدرنی که به طور خودکار وضعیت را هنگام چرخش بدون retain-fragment حفظ میکند. در iOS — Presenter در هر viewDidLoad دوباره ایجاد میشود اما در یک سرویس هماهنگکننده جداگانه کش میشود.
حداقل ۴: واسط ViewContract، پیادهسازی ViewContract (Activity/Fragment)، Presenter، Model (Repository). اگر از Dagger/Hilt استفاده شود، یک ماژول DI اضافه میشود. برای ۵۰ صفحه این ۲۰۰+ کلاس است. MVVM تعداد را ۱ فایل به ازای هر صفحه کاهش میدهد (ViewContract نیاز نیست)، MVI کلاسهای State و Intent را اضافه میکند. تعداد کلاسها — استدلال اصلی علیه MVP در پروژههای بزرگ.
Passive View — View منطقی ندارد، Presenter کاملاً وضعیت و دادهها را مدیریت میکند. Supervising Controller — View خود اتصال ساده را انجام میدهد (data binding)، Presenter در سناریوهای پیچیده دخالت میکند. در توسعه موبایل Passive View غالب است — حداکثر قابلیت آزمایش و پیشبینیپذیری را میدهد. Supervising Controller در چارچوبهای وب (ASP.NET Web Forms، GWT) استفاده میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید