MVP (Model-View-Presenter) — αρχιτεκτονικό μοτίβο στο οποίο ο Presenter λειτουργεί ως μεσολαβητής μεταξύ Model και View μέσω της διεπαφής ViewContract. Σε αντίθεση με το MVC, όπου ο Controller διαχειρίζεται άμεσα το View μέσω UIKit, ο Presenter δεν εξαρτάται από το πλαίσιο — λειτουργεί μέσω αφαίρεσης, καθιστώντας το δοκιμάσιμο χωρίς Android SDK ή UIKit. Το MVP χρησιμοποιήθηκε ευρέως στην ανάπτυξη Android πριν από την εμφάνιση του Jetpack και παραμένει σχετικό για έργα legacy. Περισσότερα στο άρθρο του Martin Fowler.
Κύρια σημεία
MVP (Model-View-Presenter) — αρχιτεκτονικό μοτίβο που προτάθηκε από τον Martin Fowler στις αρχές της δεκαετίας του 2000 ως εξέλιξη του MVC για τη βελτίωση της δοκιμασιμότητας της διεπαφής χρήστη. Το Model διαχειρίζεται δεδομένα και επιχειρηματική λογική, το View είναι υπεύθυνο για την εμφάνιση και επεξεργασία εισόδου χρήστη, ο Presenter — κεντρικό συστατικό που λαμβάνει γεγονότα από το View, εξάγει δεδομένα από το Model και διαμορφώνει κατάσταση για εμφάνιση.
Η κύρια διαφορά μεταξύ MVP και MVC — ο Presenter δεν έχει άμεση αναφορά στο View. Αντίθετα, ο Presenter αλληλεπιδρά με το View μέσω της διεπαφής ViewContract. Το View υλοποιεί αυτή τη διεπαφή και μεταδίδει τον εαυτό του στον Presenter. Αυτό σπάει την εξάρτηση από το UIKit (iOS) ή το Android Framework — ο Presenter ελέγχεται απομονωμένα με mock υλοποίηση του ViewContract. Στο MVC, ο controller UIViewController ενημερώνει άμεσα το UILabel, στο MVP ο Presenter καλεί τη μέθοδο view.showName(name), και το View αποφασίζει πώς να εμφανίσει.
| Συστατικό | Ευθύνη | Δοκιμασιμότητα |
|---|---|---|
| Model | Δεδομένα, επιχειρηματική λογική, κλήσεις δικτύου | Unit tests (δεν εξαρτάται από UI) |
| View | Εμφάνιση UI, μεταβίβαση γεγονότων στον Presenter | Mock υλοποίηση μέσω διεπαφής |
| Presenter | Επιχειρηματική λογική, διαχείριση κατάστασης, πλοήγηση | Unit tests (μέσω 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, 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 είναι deprecated από το AndroidX, αλλά το pre-Jetpack αντίστοιχό τους (Fragment.setRetainInstance) εξακολουθεί να λειτουργεί σε legacy έργα. Στη σύγχρονη ανάπτυξη, η 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 — υποχρεωτική προϋπόθεση στο iOS MVP. Το UIViewController μπορεί να καταστραφεί (pop από τη στοίβα πλοήγησης) και το closure του στον Presenter θα δημιουργήσει retain cycle. Η ασθενής αναφορά (weak var) εγγυάται ότι το View απελευθερώνεται όταν φεύγει από την οθόνη, ανεξάρτητα από την παρουσία ασύγχρονων λειτουργιών στον Presenter. Σε Android, παρόμοιο πρόβλημα λύνεται μέσω detachView() — η κλήση στο onDestroy() μηδενίζει την αναφορά στο View.
Passive View vs Supervising Controller — δύο παραλλαγές του MVP από τον Martin Fowler. 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 | Unit tests χωρίς εξαρτήσεις πλατφόρμας |
| Κύκλος ζωής | Controller ζει με την οθόνη | Presenter μπορεί να ζήσει περισσότερο (retain) |
| Πολυπλοκότητα | Ελάχιστη | +1 διεπαφή ανά οθόνη |
| Massive Controller | Τυπικό πρόβλημα | Λογική στον Presenter, View λεπτό |
Παράδειγμα unit test Presenter σε Kotlin: δημιουργείται ένα mock UserView, μεταβιβάζεται στον Presenter, καλείται το loadUser, ελέγχεται αν το showUser κλήθηκε με σωστά δεδομένα. Το τεστ εκτελείται σε χιλιοστά του δευτερολέπτου, δεν απαιτεί εξομοιωτή. Σε iOS ανάλογα — το OCMock ή το stub πρωτοκόλλου ελέγχει κλήσεις μεθόδων του UserViewProtocol. Στα έργα της IT Sectr με MVP, η κάλυψη επιχειρηματικής λογικής με unit tests έφτανε 85–90%, 2–3 φορές υψηλότερη από ό,τι σε παρόμοια έργα MVC.
Πότε το MVP είναι προτιμότερο από το MVC — σε έργα με αυστηρές απαιτήσεις σταθερότητας: τραπεζικές εφαρμογές, ιατρικά συστήματα, τερματικά πληρωμών. Σε αυτούς τους τομείς, το κόστος σφάλματος είναι υψηλό και τα unit tests είναι κρίσιμα. Σε έργα Post-MVP (όταν το προϊόν είναι ήδη στην αγορά, αλλά η βάση κώδικα είναι legacy), το 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(), διαφορετικά η διαρροή συνδρομής θα προκαλέσει crash κατά την ενημέρωση αποσυνδεδεμένου 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 είναι υποχρεωτική για εργασία με legacy κώδικα: εκατοντάδες εφαρμογές Android στο Google Play εξακολουθούν να λειτουργούν σε MVP, συμπεριλαμβανομένων εφαρμογών μεγάλων τραπεζών, λιανεμπόρων και μεταφορικών εταιρειών. Η κατανόηση του MVP είναι το θεμέλιο για την κατάκτηση του MVI και της Clean Architecture, καθώς ο Presenter είναι ο άμεσος πρόγονος του Use Case στην ορολογία του Robert Martin.
Συχνές Ερωτήσεις
Στο MVP, ο Presenter αλληλεπιδρά με το View μέσω της διεπαφής ViewContract, όχι άμεσα. Στο MVC, ο Controller έχει άμεση αναφορά στο View μέσω IBOutlet/findViewById. Το MVP επιτρέπει τον έλεγχο του Presenter με unit tests χωρίς iOS Simulator ή Android Emulator, καθώς ο Presenter δεν εξαρτάται από UIKit ή Android Framework. Το MVC απαιτεί εκκίνηση της εφαρμογής για τον έλεγχο του controller.
Το MVP δικαιολογείται σε legacy έργα που είναι ήδη χτισμένα σε αυτό το μοτίβο και σε εφαρμογές χωρίς υποστήριξη αντιδραστικών μηχανισμών (LiveData, StateFlow, Combine). Για νέα έργα, η Google συνιστά MVVM με Jetpack (Android) και η Apple συνιστά MVVM με SwiftUI (iOS). Το MVP παραμένει η καλύτερη επιλογή για έργα σε καθαρό UIKit χωρίς Combine όταν απαιτείται unit testing επιχειρηματικής λογικής.
Σε 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, προστίθεται ένα module DI. Για 50 οθόνες, αυτό είναι 200+ κλάσεις. Το MVVM μειώνει τον αριθμό κατά 1 αρχείο ανά οθόνη (ViewContract δεν χρειάζεται), το MVI προσθέτει κλάσεις State και Intent. Ο αριθμός κλάσεων — το κύριο επιχείρημα κατά του MVP σε μεγάλα έργα.
Passive View — το View δεν περιέχει λογική, ο Presenter διαχειρίζεται πλήρως την κατάσταση και τα δεδομένα. Supervising Controller — το View το ίδιο κάνει απλή σύνδεση (data binding), ο Presenter παρεμβαίνει σε σύνθετα σενάρια. Στην κινητή ανάπτυξη κυριαρχεί το Passive View — παρέχει μέγιστη δοκιμασιμότητα και προβλεψιμότητα. Το Supervising Controller χρησιμοποιείται σε web frameworks (ASP.NET Web Forms, GWT).
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης