Azimjon Abdurasulov

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

  1. MVP nima va MVVM'dan farqi
  2. MVP tarkibiy qismlari
  3. View Protocol — nega muhim?
  4. Amaliy loyiha: Product List MVP
  5. MVP-C: Coordinator qo'shish
  6. Unit Testing Presenter
  7. MVP vs MVVM taqqoslash
  8. MVP qachon ishlatiladi?
  9. Xulosa
  10. 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 UIViewController va uning tarkibidagi turli xil UIView'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:

  1. 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.
  2. Testability (Testlash): Unit test yozish vaqtida biz og'ir va ishlashi qiyin bo'lgan asl UIViewController larni 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, ProductListViewController ichida 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 XususiyatiMVP (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 boshqaruviImperativ (Do this, do that yondashuvi).Deklarativ (UI State'ga bog'liq holatda yashaydi).
SwiftUI bilan integratsiyaQiyin. (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:

  1. UIKit bilan ishlovchi loyihalarda: UIKit asosan imperativ usulda UI elementlarini yaratish va yangilashga asoslangan. Shuning uchun UIKit loyihalar MVP bilan mukammal ishlash imkonini beradi.
  2. Katta, Legacy (Eski) Loyihalarda: Ko'pgina moliya, bank va yirik korporativ loyihalar uzoq yillar davomida ishlab chiqilgan va MVP ularning asosini tashkil qilgan.
  3. 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.
  4. 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}

Markdown manba

iOS Lessons