04. MVP — Model-View-Presenter va Testability
Arxitektura
iOS Arxitektura 4: MVP — Model-View-Presenter va Testability
Holat: Ushbu dars 2026-yil oktabr holatiga ko'ra iOS dasturlashdagi MVP (Model-View-Presenter) arxitekturasining zamonaviy holatini va testlash yondashuvlarini aks ettiradi.
Mundarija
- MVP nima va MVVM'dan farqi
- MVP tarkibiy qismlari
- View Protocol — nega muhim?
- Amaliy loyiha: Product List MVP
- MVP-C: Coordinator qo'shish
- Unit Testing Presenter
- MVP vs MVVM taqqoslash
- MVP qachon ishlatiladi?
- Xulosa
- Manbalar
1. MVP nima va MVVM'dan farqi {#mvp-vs-mvvm}
MVP (Model-View-Presenter) — bu foydalanuvchi interfeysini (UI) ajratib ko'rsatish va biznes logikasini test qilishni osonlashtirish uchun mo'ljallangan mashhur arxitektura namunalaridan biridir. MVP Apple tomonidan uzoq yillar ilgari surilgan an'anaviy MVC (Model-View-Controller) ning kamchiliklarini bartaraf etish maqsasida paydo bo'lgan.
MVP va MVVM o'rtasidagi asosiy farqlar:
- Reference (Havola): MVP'da Presenter View'ga (odatda protocol orqali) aniq reference (havola) saqlaydi va kerakli vaqtda uni chaqiradi. MVVM'da ViewModel View haqida umuman hech qanday ma'lumotga ega emas; u faqat ma'lumotlar (state) o'zgarishini e'lon qiladi va View o'zi o'sha o'zgarishlarga moslashadi (data binding orqali).
- View roli: MVP'da View odatda juda "passiv" (Passive View) hisoblanadi. U o'z ichida hech qanday qaror qabul qilmaydi, faqat Presenter'dan kelgan to'g'ridan-to'g'ri buyruqlarni (masalan,
view.showLoading(),view.showProducts()) bajaradi. MVVM'da esa View ma'lumotlarni o'ziga xos tarzda qabul qilib oladi. - Aloqa yo'nalishi: MVP arxitekturasida Presenter barcha logikani boshqaradi, foydalanuvchi harakatlariga (user actions) javob beradi va natijaga ko'ra View'ga aynan nima qilish kerakligini buyuradi.
Senior eslatmasi: Garchi bugungi kunda, ayniqsa SwiftUI va Combine texnologiyalari kengayishi sababli, MVVM ko'proq mashhur bo'lsa-da, UIKit bilan ishlovchi yirik loyihalarda (masalan, bank, moliya va enterprise dasturlarida) MVP hali ham keng qo'llanilmoqda. Buning eng katta sababi — Protocol'ga asoslangan Presenter'ni Unit Test qilish juda oson va kodning ishonchliligi (coverage) yuqori bo'ladi.
2. MVP tarkibiy qismlari {#components}
MVP asosan quyidagi uchta ajralmas qismdan iborat:
- Model: Ma'lumotlarni saqlovchi obyektlar, DTO lar, ma'lumotlar bazasi abstraksiyalari, API xizmatlari va dastur biznes logikasi joylashgan qatlam. Model UI haqida hech qanday ma'lumotga ega emas.
- View: Interfeys (UI) qatlami. iOS ekotizimida bunga odatda
UIViewControllerva uning tarkibidagi turli xilUIView'lar (masalan, UIButton, UITableView) kiradi. MVP da eng muhim qoida: View o'z interfeysini Protocol orqali Presenter'ga taqdim etadi. - Presenter: View va Model o'rtasidagi aqlli vositachi. U View'dan foydalanuvchi qadamlarini (tugma bosilishi, ekranning birinchi marta yuklanishi) qabul qiladi. Keyin Model orqali kerakli ma'lumotlarni o'zgartiradi yoki o'qiydi. Va nihoyat, olingan natijani View ko'rsata oladigan oddiy ma'lumot turlariga o'girib, View protocol'idagi metodlarni chaqiradi.
Quyidagi diagrammada ma'lumotlar oqimi (Data Flow) yaqqol ko'rsatilgan:
MVP Ma'lumot Oqimi
3. View Protocol — nega muhim? {#view-protocol}
MVP'ning "yuragi" bu — View Protocol. Presenter obyekti to'g'ridan-to'g'ri qaysidir maxsus UIViewController bilan ishlamasligi kerak, chunki unday bo'lsa, kodlar bir-biriga qattiq bog'lanib qoladi (tight coupling). Buning o'rniga, u faqat mavhum protocol'ni biladi.
Misol:
protocol UserListViewProtocol: AnyObject {
func showUsers(users: [UserViewModel])
func showError(message: String)
func showLoading()
func hideLoading()
}
Nima uchun protocol muhim:
- Ajratish (Decoupling): Presenter UI framework (masalan, UIKit) detallaridan ajralib turadi. U faqat "Nima ko'rsatilishi kerak"ligini aytadi, "Qanday ko'rsatiladi" degan savol esa View ga havola.
- Testability (Testlash): Unit test yozish vaqtida biz og'ir va ishlashi qiyin bo'lgan asl
UIViewControllerlarni yaratmaymiz. Uning o'rniga xuddi shu protocol'ni amalga oshiruvchi yengil soxta (Mock) obyekt yaratamiz. Shu orqali, Presenter'ning aniq holatlarda to'g'ri metodlarni chaqirayotganini kafolatlaymiz.
4. Amaliy loyiha: Product List MVP {#practical}
Quyida biz to'liq ishlaydigan MVP arxitekturasidagi "Mahsulotlar ro'yxati" (ProductList) funksionalligini quramiz.
Model va Service
Dastlab, Model qismini va API simulatsiyasi uchun Service qismini yozib olamiz. Bu yerda yangi async/await paradigmasidan foydalanamiz.
import Foundation
struct Product: Equatable {
let id: UUID
let name: String
let price: Double
}
protocol ProductServiceProtocol {
func fetchProducts() async throws -> [Product]
}
final class ProductService: ProductServiceProtocol {
func fetchProducts() async throws -> [Product] {
// Tarmoq so'roviga simulyatsiya qilish uchun biroz kutamiz (1 soniya)
try await Task.sleep(nanoseconds: 1_000_000_000)
return [
Product(id: UUID(), name: "MacBook Pro M3", price: 1999.0),
Product(id: UUID(), name: "iPhone 15 Pro", price: 999.0),
Product(id: UUID(), name: "AirPods Pro 2", price: 249.0),
Product(id: UUID(), name: "iPad Pro 13-inch", price: 1299.0)
]
}
}
View Protocol va Presenter Protocol
Arxitekturamizning ikkita eng asosiy kelishuvlari (contracts).
import Foundation
// View bajarishi kerak bo'lgan ishlar (Presenter ushbu metodlarni chaqiradi)
@MainActor
protocol ProductListViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func showProducts(products: [Product])
func showError(message: String)
}
// Presenter bajarishi kerak bo'lgan ishlar (View ushbu metodlarni chaqiradi)
@MainActor
protocol ProductListPresenterProtocol: AnyObject {
func viewDidLoad()
func didSelectProduct(at index: Int)
}
Presenter
Presenter qatlami View dagi amallarni qabul qiladi va biznes logikani boshqaradi. U xatoliklarni va muvaffaqiyatli xolatlarni ajratib, View ga tegishli tarzda xabar beradi.
import Foundation
@MainActor
final class ProductListPresenter: ProductListPresenterProtocol {
// View ga havola (weak reference bo'lishi shart! Aks holda memory leak bo'ladi)
private weak var view: ProductListViewProtocol?
private let service: ProductServiceProtocol
private var products: [Product] = []
init(view: ProductListViewProtocol, service: ProductServiceProtocol) {
self.view = view
self.service = service
}
func viewDidLoad() {
// Ekran ochilishi bilan ma'lumotlarni yuklashni boshlaymiz
fetchProducts()
}
private func fetchProducts() {
// Avval loader'ni ko'rsatamiz
view?.showLoading()
Task {
do {
// Ma'lumotlarni tortish
let fetchedProducts = try await service.fetchProducts()
self.products = fetchedProducts
// Muvaffaqiyatli bo'lsa, loaderni yashirib, natijani beramiz
self.view?.hideLoading()
self.view?.showProducts(products: fetchedProducts)
} catch {
// Xatolik yuz bersa, uning o'zini qabul qilamiz
self.view?.hideLoading()
self.view?.showError(message: "Ma'lumotlarni yuklashda xatolik yuz berdi. Iltimos tekshirib ko'ring.")
}
}
}
func didSelectProduct(at index: Int) {
guard index >= 0 && index < products.count else { return }
let selectedProduct = products[index]
// Bu joyda navigatsiya logikasi bo'lishi mumkin. (Ko'pincha Coordinator ishlatiladi)
print("Foydalanuvchi quyidagi mahsulotni tanladi: \(selectedProduct.name)")
}
}
ViewController (View)
Bu qatlam sof UIKit. View hech narsani mustaqil o'ylamaydi, faqatgina Presenter'dan buyruq kutadi. Shuning uchun uni "Passive View" deymiz. UI elementlarini yaratish uchun native Auto Layout dan foydalandik.
import UIKit
final class ProductListViewController: UIViewController {
// UI Elements
private lazy var tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "ProductCell")
table.translatesAutoresizingMaskIntoConstraints = false
table.delegate = self
table.dataSource = self
// Dastlabki holatda yashirin turishi mumkin
table.isHidden = true
return table
}()
private lazy var activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .large)
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.hidesWhenStopped = true
return indicator
}()
// Presenter havolasi (Property injection yoki Init orqali berilishi kerak)
var presenter: ProductListPresenterProtocol!
// UI ko'rsatishi uchun kerakli state
private var products: [Product] = []
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
// Presenter'ga user ekran ga kirganini aytamiz
presenter.viewDidLoad()
}
private func setupUI() {
title = "Mahsulotlar"
view.backgroundColor = .systemBackground
view.addSubview(tableView)
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
// MARK: - ProductListViewProtocol Implementation
// Aynan shu kengaytma orqali ViewController o'zining "View" sifatidagi majburiyatini bajaradi
extension ProductListViewController: ProductListViewProtocol {
func showLoading() {
activityIndicator.startAnimating()
tableView.isHidden = true
}
func hideLoading() {
activityIndicator.stopAnimating()
tableView.isHidden = false
}
func showProducts(products: [Product]) {
self.products = products
tableView.reloadData()
}
func showError(message: String) {
let alert = UIAlertController(title: "Xatolik", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - UITableView DataSource & Delegate
extension ProductListViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
let product = products[indexPath.row]
// Zamonaviy iOS 14+ UIListContentConfiguration dan foydalanamiz
var content = cell.defaultContentConfiguration()
content.text = product.name
content.secondaryText = "$\(product.price)"
cell.contentConfiguration = content
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Tanlangan element indeksini Presenter'ga xabar beramiz
presenter.didSelectProduct(at: indexPath.row)
}
}
Muhim: E'tibor bering,
ProductListViewControllerichida qandaydir network request yoki filterlash amallari yo'q. Hatto xatolik xabarini matni (message) ham to'g'ridan to'g'ri Presenter'dan keladi.
5. MVP-C: Coordinator qo'shish {#coordinator}
Katta dasturlarda navigatsiyani ViewController yoki Presenter ichida boshqarish noqulay va arxitekturaga zid bo'lishi mumkin. Shu maqsadda MVP-C (Model-View-Presenter with Coordinator) naqshi ko'p qo'llaniladi.
Bunda Coordinator qatlami barcha navigatsiyani o'z bo'yniga oladi. Presenter faqat Coordinator bilan aloqa qiladi va "yangi ekranga o'tish kerakligi" haqida aytadi. Natijada Presenter UIKit'ning UINavigationController'dan to'liq ajraladi.
// Misol uchun:
protocol ProductListCoordinatorProtocol {
func navigateToProductDetail(product: Product)
}
// Presenter initializer'iga shu protocol qabul qilinadi.
6. Unit Testing Presenter {#testing}
MVP arxitekturasining eng katta kuchi uning test qilish (Testability) qanchalik sodda ekanligidadir.
MVP Unit Testing
Quyida ProductListPresenter uchun XCTest yordamida Unit Test yozamiz.
import XCTest
@testable import YourApp // Loyihangiz asosiy modulini yozing
@MainActor
final class ProductListPresenterTests: XCTestCase {
var presenter: ProductListPresenter!
var mockView: MockProductListView!
var mockService: MockProductService!
override func setUp() {
super.setUp()
// Test boshida Mock (soxta) obyektlarni yaratamiz
mockView = MockProductListView()
mockService = MockProductService()
presenter = ProductListPresenter(view: mockView, service: mockService)
}
override func tearDown() {
// Test oxirida xotirani bo'shatamiz
presenter = nil
mockView = nil
mockService = nil
super.tearDown()
}
func testViewDidLoad_FetchesAndShowsProductsSuccessfully() async {
// Arrange (Tayyorgarlik)
let expectedProducts = [
Product(id: UUID(), name: "Test Product", price: 10.0)
]
mockService.resultToReturn = .success(expectedProducts)
// Act (Harakat)
presenter.viewDidLoad()
// Kutamiz (Task'lar to'liq bajarilishi uchun qisqa pause zarur bo'lishi mumkin)
try? await Task.sleep(nanoseconds: 100_000_000)
// Assert (Tekshirish)
XCTAssertTrue(mockView.isShowLoadingCalled, "Loader birinchi bo'lib ko'rsatilishi kerak")
XCTAssertTrue(mockView.isHideLoadingCalled, "Ma'lumot kelgach loader yashirilishi kerak")
XCTAssertTrue(mockView.isShowProductsCalled, "Ma'lumot view'ga yuborilishi kerak")
XCTAssertEqual(mockView.shownProducts, expectedProducts, "Kelgan mahsulotlar to'g'ri uzatilishi kerak")
}
func testViewDidLoad_ShowsErrorOnFailure() async {
// Arrange (Tayyorgarlik)
let testError = NSError(domain: "test_error", code: 404, userInfo: nil)
mockService.resultToReturn = .failure(testError)
// Act (Harakat)
presenter.viewDidLoad()
try? await Task.sleep(nanoseconds: 100_000_000)
// Assert (Tekshirish)
XCTAssertTrue(mockView.isShowLoadingCalled)
XCTAssertTrue(mockView.isHideLoadingCalled)
XCTAssertTrue(mockView.isShowErrorCalled, "Xatolik yuz berganda xato xabari ko'rsatilishi kerak")
XCTAssertEqual(mockView.shownErrorMessage, "Ma'lumotlarni yuklashda xatolik yuz berdi. Iltimos tekshirib ko'ring.")
}
}
// MARK: - Mocks (Soxta Obyektlar)
// Test paytida asl UI o'rnini bosuvchi Mock View
final class MockProductListView: ProductListViewProtocol {
var isShowLoadingCalled = false
var isHideLoadingCalled = false
var isShowProductsCalled = false
var isShowErrorCalled = false
var shownProducts: [Product] = []
var shownErrorMessage: String?
func showLoading() {
isShowLoadingCalled = true
}
func hideLoading() {
isHideLoadingCalled = true
}
func showProducts(products: [Product]) {
isShowProductsCalled = true
shownProducts = products
}
func showError(message: String) {
isShowErrorCalled = true
shownErrorMessage = message
}
}
// Test paytida haqiqiy tarmoq so'rovlarini to'suvchi Mock Service
final class MockProductService: ProductServiceProtocol {
var resultToReturn: Result<[Product], Error>?
func fetchProducts() async throws -> [Product] {
guard let result = resultToReturn else {
throw NSError(domain: "mock_not_setup", code: 0)
}
switch result {
case .success(let products):
return products
case .failure(let error):
throw error
}
}
}
7. MVP vs MVVM taqqoslash {#comparison}
Ko'pchilik jamoalar o'z loyihalari uchun qachon MVP va qachon MVVM arxitekturasini tanlash masalasida ikkilanishadi. Quyidagi taqqoslash jadvali ularning amaliy farqini tushunishga yordam beradi:
| Arxitektura Xususiyati | MVP (Model-View-Presenter) | MVVM (Model-View-ViewModel) |
|---|---|---|
| View bilan ishlash (Binding) | Mavjud emas. Presenter Protocol orqali to'g'ridan-to'g'ri buyruq beradi. | Bor. Combine, RxSwift yoki iOS 17 @Observable orqali UI bilan bog'lanadi. |
| Testability (Test qilish osonligi) | Juda kuchli. Mock Protocol bilan barcha senariylar qoplanadi. | Kuchli. ViewModel state'i va Publisherlar test qilinadi. |
| Boilerplate kod (Takrorlanuvchi kod) | Ko'p. Har bir yangi harakat uchun View protocolida yangi metod yozish kerak. | Nisbatan kamroq. Asosan State o'zgaradi va UI reaktiv tarzda yangilanadi. |
| State boshqaruvi | Imperativ (Do this, do that yondashuvi). | Deklarativ (UI State'ga bog'liq holatda yashaydi). |
| SwiftUI bilan integratsiya | Qiyin. (SwiftUI deklarativ, MVP esa imperativ). Odatda UIKit uchun qo'llaniladi. | Ajoyib. (Ayniqsa @StateObject, @Observable bilan juda mos keladi). |
8. MVP qachon ishlatiladi? {#when-to-use}
MVP naqshi bugungi kunda asosan quyidagi hollarda qo'llaniladi:
- UIKit bilan ishlovchi loyihalarda: UIKit asosan imperativ usulda UI elementlarini yaratish va yangilashga asoslangan. Shuning uchun UIKit loyihalar MVP bilan mukammal ishlash imkonini beradi.
- Katta, Legacy (Eski) Loyihalarda: Ko'pgina moliya, bank va yirik korporativ loyihalar uzoq yillar davomida ishlab chiqilgan va MVP ularning asosini tashkil qilgan.
- Android'dan kelgan jamoalar bilan ishlaganda: Android ekotizimida ko'p yillar davomida MVP asosiy sanoat standarti bo'lgan (hozir MVI/MVVM o'rnini egallayotgan bo'lsa ham). Shuning uchun cross-platform jamoalar bitta arxitektura atamasidan foydalanishni ma'qul ko'rganda.
- 100% Test Coverage qat'iy talab qilinganda: Barcha UIni Mock obyektlar bilan osongina test qilish mumkin bo'lgani uchun biznes mantig'idagi xatolikni juda erta (kompilyatsiya bosqichida) ushlashga yordam beradi.
9. Xulosa {#xulosa}
MVP (Model-View-Presenter) arxitekturasi MVC'ning asosiy kamchiligi hisoblangan "Massive View Controller" (Yirik View Controller) muammosini biznes logikani Presenter qatlamiga to'liq o'tkazish orqali hal qiladi. Garchi SwiftUI davriga kelib MVVM yoki undan ham zamonaviy (masalan, TCA) yondashuvlar dominant arxitekturaga aylanayotgan bo'lsa-da, MVP o'zining mustahkam, "Protocol-oriented" tabiati va ajoyib testlash imkoniyatlari sababli hali ham o'z o'rnini saqlab turibdi. Mukammal test qilinadigan va aniq ajratilgan qatlamli kod yozishni o'rganishni maqsad qilgan har bir iOS dasturchisi MVP arxitekturasini tushunishi kerak.
10. Manbalar {#manbalar}
- Martin Fowler - GUI Architectures (Asosiy konseptlar)
- Apple Developer Documentation - Testing your apps in Xcode
- Design Patterns - Model View Presenter on Wikipedia
- Clean Architecture mutaxassisi Uncle Bob'ning kitoblari va blog postlari.