MVP (Model-View-Presenter) — نمط معماري حيث يعمل Presenter كوسيط بين Model وView عبر واجهة ViewContract. على عكس MVC، حيث يتحكم Controller مباشرة في View عبر UIKit، لا يعتمد Presenter على الإطار — فهو يعمل من خلال التجريد، مما يجعله قابلًا للاختبار بدون Android SDK أو UIKit. يُستخدم MVP على نطاق واسع في تطوير Android قبل ظهور Jetpack ولا يزال ملائمًا للمشاريع القديمة. للمزيد — في مقال مارتن فاولر.
الرئيسي
MVP (Model-View-Presenter) — نمط معماري اقترحه مارتن فاولر في أوائل العقد 2000 كتطور لـ MVC لتحسين قابلية اختبار واجهة المستخدم. يدير Model البيانات ومنطق الأعمال، View مسؤولة عن العرض ومعالجة إدخال المستخدم، Presenter هو المكون المركزي الذي يستقبل الأحداث من View، يسترجع البيانات من Model ويشكل الحالة للعرض.
الفرق الرئيسي بين MVP وMVC — Presenter ليس لديه مرجع مباشر إلى View. بدلاً من ذلك، يتفاعل Presenter مع View عبر واجهة ViewContract. تقوم 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 من خلال 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 ?: "Unknown error")
}
}
}
}
// 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) { /* تحديث واجهة المستخدم */ }
override fun showLoading() { /* إظهار ProgressBar */ }
override fun hideLoading() { /* إخفاء ProgressBar */ }
override fun showError(message: String) { /* إظهار Snackbar */ }
}
إدارة دورة الحياة — مشكلة رئيسية لـ MVP على Android. يتم تدمير Activity عند تدوير الشاشة، ويُستدعى presenter.attachView() مرة أخرى في onCreate(). إذا كان تحميل البيانات غير متزامن (RxJava، coroutines)، عند اكتماله قد تكون View منفصلة. الحل — إلغاء الاشتراكات في detachView() أو استخدام Loader من Support Library (للمشاريع بدون Jetpack). في IT Sectr استخدمنا مزيج MVP + RxJava لسنوات في المشاريع التجارية — النمط مستقر لكنه يتطلب انضباطًا في إدارة الاشتراكات.
Retain fragments — آلية للحفاظ على Presenter عند تدوير الشاشة. Fragment بدون UI (setRetainInstance(true)) يعيش أطول من Activity ويحتفظ بمرجع إلى Presenter. عند إعادة إنشاء Activity، يمرر fragment نفس Presenter إلى Activity الجديدة. Retain fragments مهملة منذ AndroidX، لكن نظيرها قبل Jetpack (Fragment.setRetainInstance) لا يزال يعمل في المشاريع القديمة. في التطوير الحديث، توصي Google بـ ViewModel بدلاً من retain fragments.
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 — شرط إلزامي في MVP لنظام iOS. يمكن تدمير UIViewController (pop من navigation stack)، وإغلاقه في 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 مع View عبر واجهة ViewContract. هذا الفرق يغير بشكل جذري قابلية الاختبار: كائن 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 — في المشاريع ذات متطلبات الاستقرار الصارمة: التطبيقات المصرفية، الأنظمة الطبية، محطات الدفع. في هذه المجالات، تكلفة الخطأ عالية، واختبارات الوحدة حاسمة. في مشاريع ما بعد MVP (عندما يكون المنتج بالفعل في السوق لكن قاعدة الكود قديمة)، يسمح MVP باستخراج المنطق تدريجيًا من Massive View Controller إلى طبقة قابلة للاختبار دون إعادة كتابة معمارية كاملة.
العيوب الرئيسية لـ MVP — نمو عدد الواجهات والإدارة اليدوية للاشتراكات. كل شاشة تتطلب على الأقل ViewContract + Presenter واحد، لـ 50 شاشة — 50 واجهة و50 فئة Presenter. في MVVM، يحل ViewModel محل Presenter ويستخدم آليات تفاعلية (LiveData، StateFlow، ObservableObject)، مما يلغي الحاجة إلى attach/detach اليدوي وواجهات ViewContract.
RxJava وMVP — مزيج شائع في Android 2015–2019. يشترك Presenter في Observable من Repository، يعرض النتيجة عبر ViewContract. المشكلة: يجب إلغاء disposable صراحةً في detachView()، وإلا فإن تسرب الاشتراك سيسبب تعطلًا عند تحديث View منفصلة. مكتبتا RxLifecycle وAutoDispose أتمتتا إلغاء الاشتراك جزئيًا لكن أضافتا تبعيات. في IT Sectr انتقلنا من MVP+RxJava إلى MVVM+Flow في 2020 — أصبح الكود أقصر بنسبة 25–30% بسبب إلغاء ViewContract.
الانتقال من MVP إلى MVVM — عملية تدريجية. 1) استبدال ViewContract بـ LiveData/StateFlow في Presenter. 2) إزالة طرق attach/detach — الاشتراك يتم عبر observe(). 3) إعادة تسمية Presenter إلى ViewModel. 4) دمج DI (Hilt/Koin) لـ ViewModelFactory. يستغرق نقل شاشة واحدة 2–4 ساعات، قاعدة الكود بأكملها — 2–4 أسابيع لمشروع من 50–100 شاشة. بعد النقل، تُزال واجهات ViewContract، يتقلص الكود، تبقى الاختبارات.
MVP في التطوير الحديث — النمط حي لكنه يخضع لـ MVVM وMVI. توصي Google رسميًا بـ MVVM مع Jetpack للمشاريع الجديدة. Apple — MVVM مع SwiftUI. لكن معرفة MVP إلزامية للعمل مع الكود القديم: مئات تطبيقات Android على Google Play لا تزال تعمل على MVP، بما في ذلك تطبيقات البنوك الكبرى وتجار التجزئة وشركات النقل. فهم MVP هو الأساس لإتقان MVI وClean Architecture، حيث أن Presenter هو السلف المباشر لـ Use Case حسب مصطلحات روبرت مارتن.
الأسئلة المتكررة
في MVP، يتفاعل Presenter مع View عبر واجهة ViewContract، وليس بشكل مباشر. في MVC، Controller لديه مرجع مباشر إلى View عبر IBOutlet/findViewById. يسمح MVP باختبار Presenter باختبارات الوحدة بدون iOS Simulator أو Android Emulator، لأن Presenter لا يعتمد على UIKit أو Android Framework. MVC يتطلب تشغيل التطبيق لاختبار وحدة التحكم.
MVP مبرر في المشاريع القديمة المبنية بالفعل على هذا النمط، وفي التطبيقات بدون دعم الآليات التفاعلية (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 من Google هي بديل حديث يحافظ على الحالة تلقائيًا عند التدوير بدون retain fragments. على iOS — يُعاد إنشاء Presenter في كل viewDidLoad لكن يُخزن مؤقتًا في خدمة منسق منفصلة.
على الأقل 4: واجهة ViewContract، تنفيذ ViewContract (Activity/Fragment)، Presenter، Model (Repository). إذا تم استخدام Dagger/Hilt، يُضاف وحدة DI. لـ 50 شاشة — أكثر من 200 فئة. يقلل 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 تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.