VIPER (View-Interactor-Presenter-Entity-Router) — Mutual Mobile şirkəti tərəfindən iOS tətbiqləri üçün hazırlanmış modul arxitekturası. VIPER tətbiqi beş təbəqəyə ayırır: View göstərimə cavabdehdir, Interactor — biznes məntiqinə, Presenter — məlumatların hazırlanmasına, Entity — məlumat modellərinə, Router — modullar arasında naviqasiyaya. VIPER mobil arxitekturalar arasında tək məsuliyyət prinsipinin ən detallı reallaşdırmasıdır. Ətraflı — objc.io saytındakı məqalədə.
Əsas məqamlar
VIPER (View-Interactor-Presenter-Entity-Router) — 2013–2014-cü illərdə Mutual Mobile şirkəti tərəfindən böyük iOS layihələri üçün hazırlanmış arxitektura nümunəsidir. Tətbiqin hər ekranı ciddi müəyyən edilmiş vəzifələri olan beş komponentdən ibarət ayrıca moduldur. VIPER mobil inkişafda tək məsuliyyət prinsipinin (Single Responsibility Principle) ən sərt reallaşdırmasıdır: heç bir komponent digərinin edə biləcəyi işi görmür.
View — yalnız Presenter tərəfindən ötürülən məlumatların göstərilməsinə cavabdeh olan passiv komponent. View biznes məntiqi ehtiva etmir, naviqasiyanı idarə etmir, şəbəkə sorğuları göndərmir. iOS-da — ViewProtocol protokolu ilə UIViewController. Interactor — Entity və xidmətlər (şəbəkə, verilənlər bazası, GPS) ilə işləyən biznes məntiqi təbəqəsi. Interactor UIKit-i import etmir. Presenter — View və Interactor arasında vasitəçi: Interactor-dan məlumat alır, göstərim üçün formatlaşdırır, View-ə ötürür. Presenter də UIKit-i import etmir. Entity — məlumat modelləri (struct, class). Router — naviqasiyanı idarə edir: modullar yaradır, ekranları açır, modullar arasında məlumat ötürür.
| Komponent | Məsuliyyət | Asılılıqlar |
|---|---|---|
| View | Göstərim, animasiyalar, jestlər | UIKit (yalnız View) |
| Interactor | Biznes məntiqi, şəbəkə, verilənlər bazası | Entity, xidmətlər |
| Presenter | Məlumatların formatlaşdırılması, View komandaları | ViewProtocol, Interactor |
| Entity | Məlumat modelləri | Yoxdur |
| Router | Naviqasiya, modulların yaradılması | UIViewController (keçidlər üçün) |
Komponentlər arasında əlaqələr protokollarla təsvir olunur. ViewProtocol göstərim metodlarını, InteractorProtocol — biznes məntiqi metodlarını, PresenterProtocol — hadisələrin işlənməsi metodlarını, RouterProtocol — naviqasiya metodlarını müəyyən edir. Hər bir komponent digəri ilə yalnız protokol vasitəsilə əlaqə qurur ki, bu da reallaşdırmaları asanlıqla əvəz etməyə və təcrid olunmuş şəkildə test etməyə imkan verir. Orta hesabla bir ekranlı VIPER modulu 5 protokol + 5 sinif + 1 Builder/Assembler = ekran başına 11 fayl ehtiva edir.
VIPER modulunun qurulması Builder (və ya Assembler) tərəfindən həyata keçirilir, o, bütün beş komponenti yaradır və onları protokollar vasitəsilə birləşdirir. Builder — komponentlərin bir-birinin konkret tiplərini bildiyi yeganə yerdir. Qurulumadan sonra View göstərim üçün kənara qaytarılır, qalan zəncir təmizdir və təcrid olunmuş şəkildə test edilir.
// Protokol — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// Protokol — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// Protokol — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — biznes məntiqi
final class UserInteractor: UserInteractorProtocol {
private let service: UserService
init(service: UserService) { self.service = service }
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
service.fetchUser(id: id, completion: completion)
}
}
// Presenter — məlumatların hazırlanması
final class UserPresenter {
private weak var view: UserViewProtocol?
private let interactor: UserInteractorProtocol
private let router: UserRouterProtocol
init(interactor: UserInteractorProtocol, router: UserRouterProtocol) {
self.interactor = interactor
self.router = router
}
func setView(_ view: UserViewProtocol) {
self.view = view
}
func viewDidLoad() {
view?.showLoading()
interactor.fetchUser(id: 42) { [weak self] result in
guard let self else { return }
self.view?.hideLoading()
switch result {
case .success(let user):
self.view?.display(name: user.name)
self.view?.display(email: user.email)
case .failure(let error):
// xətanın işlənməsi
}
}
}
}
// Router — naviqasiya
final class UserRouter: UserRouterProtocol {
private weak var viewController: UIViewController?
func setViewController(_ vc: UIViewController) {
viewController = vc
}
func navigateToProfile(userId: Int) {
let profileModule = ProfileModuleBuilder.build(userId: userId)
viewController?.navigationController?.pushViewController(profileModule, animated: true)
}
}
// Builder — modulun qurulması
enum UserModuleBuilder {
static func build(userId: Int) -> UIViewController {
let service = UserService()
let interactor = UserInteractor(service: service)
let router = UserRouter()
let presenter = UserPresenter(interactor: interactor, router: router)
let viewController = UserViewController(presenter: presenter)
presenter.setView(viewController)
router.setViewController(viewController)
return viewController
}
}
Builder/Assembler — asılılıqların əl ilə yeridilməsini (Dependency Injection) həyata keçirən VIPER-in əsas elementidir. Konstruktor vasitəsilə asılılıqların yeridilməsi (constructor injection) komponentin asılılıqları olmadan yaradıla bilməyəcəyinə zəmanət verir. Müasir VIPER-də Builder Swinject (DI konteyneri) istifadə edə bilər, lakin əl ilə qurulma test üçün daha şəffaf qalır. IT Sectr-də biz mürəkkəb məntiqli modullar üçün əl ilə qurulma ilə VIPER tətbiq edirik — bu, yeni tərtibatçılar üçün kodu oxumağı asanlaşdırır.
Mapper (Formatter) — VIPER-in isteğe bağlı altıncı komponenti. Mapper Entity-ni (verilənlər bazası/server modelləri) ViewModel-ə (göstərim modelləri) çevirir. Entity id, first_name, last_name, email sahələri ilə UserDTO ehtiva edir. ViewModel — name (first_name + last_name) və email ilə UserDisplayItem. Mapper Presenter-də icra olunur. Məlumat xəritələşdirilməsi mürəkkəbdirsə (çoxsaylı Entity → bir ViewModel), Mapper test etmək üçün ayrıca sinfə çıxarılır.
VIPER modulları təcrid olunub və bir-biri haqqında bilmirlər. Modullar arasında kommunikasiya Router vasitəsilə baş verir. İstifadəçi istifadəçi ekranında „Profil" düyməsini kliklədikdə, Presenter router.navigateToProfile(userId: 42) çağırır. Router ProfileModuleBuilder.build(userId: 42) vasitəsilə yeni modul yaradır və onu navigationController.push ilə açır. Data flow: Modul A → Router A → Modul B Builder → Modul B yaradılır və açılır.
Məlumatların geri ötürülməsi (məsələn, seçim ekranında şəhər seçildi → profil redaktə ekranına qaytarıldı) VIPER-də deleqatlar və ya bağlanmalar (closure) vasitəsilə həyata keçirilir. Modul B didSelectCity(_ city: City) metodu ilə ModuleBDelegate protokolunu müəyyən edir. Modul A bu protokolu reallaşdırır. Router A deleqatı Module B Builder-ə ötürür. Şəhər seçildikdə Modul B delegate?.didSelectCity(city) çağırır. Bu, hər bir UIKit tərtibatçısına tanış olan standart iOS təcrübəsidir.
| Ssenari | Mexanizm | Nümunə |
|---|---|---|
| İrəli keçid | Router → Builder | navigateToProfile(userId:) |
| Məlumatın geri ötürülməsi | Delegate | didSelectCity(_:) |
| Sistem bildirişi | NotificationCenter | UserDidLogout |
| Interactor-dan hadisə | Presenter → View | WebSocket message |
NotificationCenter eyni anda bir neçə modula təsir edən sistem hadisələri (çıxış, tarif dəyişikliyi, push bildirişləri) üçün istifadə olunur. Router və ya AppDelegate Notification-a abunə olur, lazımi modulu yaradır və ya vəziyyəti yeniləyir. VIPER NotificationCenter-i qadağan etmir — vacibdir ki, o yalnız 1-to-many hadisələri üçün istifadə olunsun, 1-to-1 kommunikasiya üçün isə deleqatlar və ya bağlanmalar tətbiq edilsin.
VIPER vs MVVM — VIPER ekran başına 2–3 dəfə çox kod tələb edir, lakin komponentlərin mütləq təcrid olunmasını təmin edir. ViewModel + SwiftUI ilə MVVM daha sadə və sürətlidir, lakin 5+ tərtibatçıdan ibarət komandalarda daha pis miqyaslanır. VIPER kimin nəyə cavabdeh olduğunu ciddi müəyyən edir: Interactor — yalnız biznes məntiqi, Presenter — formatlaşdırma, Router — naviqasiya. MVVM-də ViewModel tez-tez böyüyərək naviqasiyanı və biznes məntiqini özünə götürür.
VIPER vs Clean Architecture — VIPER iOS UIKit üçün uyğunlaşdırılmış Clean Architecture-ın xüsusi halıdır. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller Robert Martinin terminologiyasında. Clean Architecture Interactor və məlumatlar arasında Gateway/Repository təbəqəsi əlavə edir, bu VIPER-də adətən ayrılmır. SwiftUI-də müasir layihələr üçün əksər komandalar Clean Architecture (The Composable Architecture) və ya MVVM seçir, VIPER-i UIKit-də köhnə kod üçün saxlayır.
VIPER nə vaxt seçilməlidir — 5 tərtibatçıdan ibarət komandalar, 50 ekrandan UIKit layihəsi, test tələbləri 80%-dən yuxarı, yalnız iOS (VIPER yenidən yazılmadan Android-ə keçmir). VIPER proqnozlaşdırıla bilən struktur təmin edir: yeni tərtibatçı modulu 15 dəqiqəyə başa düşür. Lakin inkişaf sürəti daha çox fayl sayı səbəbindən MVVM ilə müqayisədə 20–30% aşağıdır. IT Sectr-də biz 3 nəfərdən ibarət komandalarla UIKit-də enterprise layihələri üçün VIPER istifadə edirik və SwiftUI-də yeni layihələr üçün Clean Architecture-a üstünlük veririk.
VIPER test üçün nəzərdə tutulub — hər bir komponent protokollar vasitəsilə təcrid olunmuş şəkildə test edilir. Interactor mock-xidmətlərlə test edilir: fetchUser-in düzgün ID ilə çağırıldığı və nəticənin Presenter-ə ötürüldüyü yoxlanılır. Presenter mock View və Interactor obyektləri ilə test edilir. Router mock-naviqasiya ilə test edilir: navigateToProfile-in düzgün userId ilə çağırıldığı və düzgün modulun yaradıldığı yoxlanılır. View UI testləri (XCUITest) ilə test edilir.
import XCTest
final class UserPresenterTests: XCTestCase {
func testViewDidLoad_callsFetchUserAndUpdatesView() {
// Given
let view = MockUserView()
let interactor = MockUserInteractor()
let router = MockUserRouter()
let presenter = UserPresenter(interactor: interactor, router: router)
presenter.setView(view)
let expectedUser = User(id: 42, name: "John", email: "john@test.com")
interactor.result = .success(expectedUser)
// When
presenter.viewDidLoad()
// Then
XCTAssertEqual(interactor.capturedUserId, 42)
XCTAssertEqual(view.displayedName, "John")
XCTAssertTrue(view.didShowLoading)
}
}
Mock-obyektlər VIPER üçün əl ilə (saxlanılan capture xassələri olan sinif) və ya Cuckoo / Mockingbird kitabxanaları vasitəsilə yaradılır. Əl ilə mock sinifləri daha sadə və başa düşüləndir, xüsusən yeni tərtibatçıların öyrədilməsi üçün. Hər bir mock captured dəyərləri (capturedUserId, displayedName) və çağırış bayraqlarını (didShowLoading) saxlayır. Testin sonunda metodun nəinki çağırıldığı, həm də hansı parametrlərlə çağırıldığı yoxlanılır — bu, məlumat axınının düzgünlüyünə əminlik verir.
Kod əhatəsi IT Sectr-in VIPER layihələrində Interactor üçün 85–95%, Presenter üçün 90–95%, Router üçün 70–80%, View üçün 30–50% (UI testləri vasitəsilə) təşkil edir. View snapshot testləri ilə (SnapshotTesting, 1,5K ulduz) test edilir — bu, XCUITest-dən daha sürətlidir və daha çox halı əhatə edir. VIPER layihəsinin ümumi əhatəsi adətən 70–80% təşkil edir ki, bu da MVVM layihəsindən (50–65%) yüksəkdir, lakin test yazmaq üçün daha çox vaxt tələb olunur (inkişaf vaxtının 30–40%-i MVVM-də 20–25%-ə qarşı).
Tez-tez verilən suallar
Minimum 11 fayl: 5 protokol (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 reallaşdırma (ViewController, Interactor, Presenter, Router, Entity) və Builder/Assembler. Mapper (Formatter) ilə — 12–13. 50 ekranlı layihə üçün bu, yalnız VIPER modulları üçün 550–650 fayl deməkdir. MVVM ekran başına 3 fayl tələb edir (ViewModel, View, Model) — 50 ekran üçün 150 fayl.
Bəli, nəzəri olaraq VIPER Android-ə keçirilə bilər, lakin praktikada tətbiq olunmur — Google Jetpack ilə MVVM tövsiyə edir. VIPER iOS UIKit üçün yaradılmışdır, burada ViewController həyat dövrünə görə test etmək çətindir. Android-də Jetpack ViewModel VIPER təcridi olmadan test problemini həll edir. VIPER-in Android analoqu — module/feature bölgüsü ilə Clean Architecture.
VIPER iOS-a spesifik Clean Architecture reallaşdırmasıdır. Interactor Use Case-ə, Entity — Domain Model-ə, Presenter — Presentation təbəqəsinə uyğun gəlir. Clean Architecture Interactor və məlumatlar arasında Repository/Gateway əlavə edir, bu VIPER-də adətən Interactor daxilində reallaşdırılır. Clean Architecture Router təyin etmir — naviqasiya reallaşdırmanın ixtiyarına buraxılır.
Xeyr — SwiftUI MVVM + Combine üçün nəzərdə tutulub. SwiftUI-də VIPER həddindən artıqdır: deklarativ UI ilə bir ekran üçün beş komponent faydasız yükdür. SwiftUI üçün MVVM və ya TCA (The Composable Architecture) seçin. VIPER UIKit köhnə kodu və iOS 12 və daha aşağı minimal versiya olan layihələr üçün aktual olaraq qalır.
Router vasitəsilə. Modul A router.navigateToProfile(userId: id) çağırır. Router A Builder vasitəsilə modul B yaradır, userId ötürür. Geri ötürmə — deleqat vasitəsilə: modul B ModuleBDelegate protokolunu müəyyən edir, modul A onu reallaşdırır və Router vasitəsilə ötürür. Sistem hadisələri (çıxış) — NotificationCenter vasitəsilə.
Nəticə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun