VIPER (View-Interactor-Presenter-Entity-Router) — معماری ماژولار توسعهیافته در شرکت Mutual Mobile برای برنامههای iOS. VIPER برنامه را به پنج لایه تقسیم میکند: View مسئول نمایش، Interactor مسئول منطق کسبوکار، Presenter مسئول آمادهسازی دادهها، Entity مسئول مدلهای داده، Router مسئول ناوبری بین ماژولها. VIPER دقیقترین پیادهسازی اصل مسئولیت واحد در میان معماریهای موبایل است. بیشتر — در مقاله در objc.io.
نکات اصلی
VIPER (View-Interactor-Presenter-Entity-Router) — الگوی معماری توسعهیافته در سالهای ۲۰۱۳–۲۰۱۴ در شرکت Mutual Mobile برای پروژههای بزرگ iOS. هر صفحه برنامه یک ماژول مجزا از پنج مؤلفه با وظایف کاملاً مشخص است. VIPER سختگیرانهترین پیادهسازی اصل مسئولیت واحد (Single Responsibility Principle) در توسعه موبایل است: هیچ مؤلفهای کاری را که مؤلفه دیگر میتواند انجام دهد، انجام نمیدهد.
View — مؤلفه منفعل که فقط مسئول نمایش دادههای ارسالشده توسط Presenter است. View حاوی منطق کسبوکار نیست، ناوبری را مدیریت نمیکند، درخواستهای شبکه را انجام نمیدهد. در iOS — UIViewController با پروتکل ViewProtocol. Interactor — لایه منطق کسبوکار که با Entity و سرویسها (شبکه، دیتابیس، GPS) کار میکند. Interactor UIKit را import نمیکند. Presenter — واسط بین View و Interactor: دادهها را از Interactor دریافت میکند، برای نمایش قالببندی میکند و به View منتقل میکند. Presenter نیز UIKit را import نمیکند. Entity — مدلهای داده (struct، class). Router — ناوبری را مدیریت میکند: ماژولها را ایجاد میکند، صفحهها را باز میکند، دادهها را بین ماژولها منتقل میکند.
| مؤلفه | مسئولیت | وابستگیها |
|---|---|---|
| View | نمایش، انیمیشنها، ژستها | UIKit (فقط View) |
| Interactor | منطق کسبوکار، شبکه، دیتابیس | Entity، سرویسها |
| Presenter | قالببندی دادهها، دستورات View | ViewProtocol، Interactor |
| Entity | مدلهای داده | ندارد |
| Router | ناوبری، ایجاد ماژولها | UIViewController (برای انتقال) |
ارتباطات بین مؤلفهها توسط پروتکلها توصیف میشوند. ViewProtocol متدهای نمایش، InteractorProtocol — متدهای منطق کسبوکار، PresenterProtocol — متدهای پردازش رویداد، RouterProtocol — متدهای ناوبری را تعریف میکند. هر مؤلفه فقط از طریق پروتکل با دیگری ارتباط برقرار میکند که امکان تعویض آسان پیادهسازیها و تست ایزوله را فراهم میکند. به طور متوسط یک ماژول VIPER از یک صفحه شامل ۵ پروتکل + ۵ کلاس + ۱ Builder/Assembler = ۱۱ فایل در هر صفحه است.
ساخت ماژول VIPER در Builder (یا Assembler) انجام میشود که هر پنج مؤلفه را ایجاد کرده و از طریق پروتکلها به هم متصل میکند. Builder تنها جایی است که مؤلفهها از انواع خاص یکدیگر مطلع هستند. پس از ساخت، View برای نمایش به بیرون بازگردانده میشود و بقیه زنجیره تمیز بوده و به صورت ایزوله تست میشود.
// پروتکل — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// پروتکل — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// پروتکل — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — منطق کسبوکار
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 — آمادهسازی دادهها
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):
// پردازش خطا
}
}
}
}
// Router — ناوبری
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 — ساخت ماژول
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 — عنصر کلیدی VIPER که تزریق وابستگی (Dependency Injection) را به صورت دستی پیادهسازی میکند. تزریق وابستگی از طریق سازنده (constructor injection) تضمین میکند که مؤلفه بدون وابستگیهای خود نمیتواند ایجاد شود. در VIPER مدرن، Builder ممکن است از Swinject (کانتینر DI) استفاده کند، اما ساخت دستی برای تست شفافتر باقی میماند. در IT Sectr ما از VIPER با ساخت دستی برای ماژولهای با منطق پیچیده استفاده میکنیم — این کار خواندن کد را برای توسعهدهندگان جدید آسانتر میکند.
Mapper (Formatter) — مؤلفه ششم اختیاری VIPER. Mapper Entity (مدلهای دیتابیس/سرور) را به ViewModel (مدلهای نمایش) تبدیل میکند. Entity شامل UserDTO با فیلدهای id، first_name، last_name، email است. ViewModel — UserDisplayItem با name (first_name + last_name) و email. Mapper در Presenter اجرا میشود. اگر نگاشت داده پیچیده باشد (چندین Entity → یک ViewModel)، Mapper برای تست به یک کلاس جداگانه منتقل میشود.
ماژولهای VIPER ایزوله هستند و از یکدیگر اطلاعی ندارند. ارتباط بین ماژولها از طریق Router انجام میشود. هنگامی که کاربر دکمه «پروفایل» را در صفحه کاربر کلیک میکند، Presenter router.navigateToProfile(userId: 42) را فراخوانی میکند. Router یک ماژول جدید از طریق ProfileModuleBuilder.build(userId: 42) ایجاد کرده و آن را از طریق navigationController.push باز میکند. جریان داده: ماژول A → Router A → ماژول B Builder → ماژول B ایجاد و باز میشود.
انتقال داده به عقب (مثلاً شهری در صفحه انتخاب انتخاب شد → به صفحه ویرایش پروفایل برگردانده شد) در VIPER از طریق دلیگیتها یا بلاکها (closures) پیادهسازی میشود. ماژول B پروتکل ModuleBDelegate را با متد didSelectCity(_ city: City) تعریف میکند. ماژول A این پروتکل را پیادهسازی میکند. Router A دلیگیت را به Module B Builder منتقل میکند. هنگام انتخاب شهر، ماژول B delegate?.didSelectCity(city) را فراخوانی میکند. این یک روش استاندارد iOS است که برای هر توسعهدهنده UIKit آشناست.
| سناریو | مکانیزم | مثال |
|---|---|---|
| انتقال به جلو | Router → Builder | navigateToProfile(userId:) |
| انتقال داده به عقب | Delegate | didSelectCity(_:) |
| اعلان سیستمی | NotificationCenter | UserDidLogout |
| رویداد از Interactor | Presenter → View | WebSocket message |
NotificationCenter برای رویدادهای سیستمی (خروج از حساب، تغییر تعرفه، اعلانهای پوش) که همزمان چند ماژول را تحت تأثیر قرار میدهند استفاده میشود. Router یا AppDelegate در Notification مشترک شده، ماژول مورد نیاز را ایجاد یا وضعیت را بهروزرسانی میکند. VIPER NotificationCenter را منع نمیکند — مهم این است که فقط برای رویدادهای یک-به-چند استفاده شود و برای ارتباط یک-به-یک از دلیگیتها یا بلاکها استفاده گردد.
VIPER vs MVVM — VIPER به ۲–۳ برابر کد بیشتر در هر صفحه نیاز دارد، اما ایزولاسیون مطلق مؤلفهها را فراهم میکند. MVVM با ViewModel + SwiftUI سادهتر و سریعتر است، اما در تیمهای ۵+ توسعهدهنده مقیاسپذیری کمتری دارد. VIPER به طور سخت مشخص میکند چه کسی مسئول چه چیزی است: Interactor — فقط منطق کسبوکار، Presenter — قالببندی، Router — ناوبری. در MVVM، ViewModel اغلب بزرگ شده و ناوبری و منطق کسبوکار را به خود میگیرد.
VIPER vs Clean Architecture — VIPER یک مورد خاص از Clean Architecture است که برای iOS UIKit تطبیق داده شده است. Interactor = Use Case، Entity = Domain Model، Presenter = Presentation، Router = Controller در اصطلاحات رابرت مارتین. Clean Architecture لایه Gateway/Repository را بین Interactor و دادهها اضافه میکند که در VIPER معمولاً جدا نمیشود. برای پروژههای مدرن در SwiftUI، اکثر تیمها Clean Architecture (The Composable Architecture) یا MVVM را انتخاب کرده و VIPER را برای کدهای قدیمی UIKit نگه میدارند.
چه زمانی VIPER را انتخاب کنیم — تیمهای از ۵ توسعهدهنده، پروژه UIKit از ۵۰ صفحه به بالا، الزامات تست بالای ۸۰٪، فقط iOS (VIPER بدون بازنویسی به Android منتقل نمیشود). VIPER ساختاری قابل پیشبینی ارائه میدهد: یک توسعهدهنده جدید ماژول را در ۱۵ دقیقه درک میکند. با این حال سرعت توسعه به دلیل تعداد بیشتر فایلها ۲۰–۳۰٪ کمتر از MVVM است. در IT Sectr ما از VIPER برای پروژههای سازمانی UIKit با تیمهای از ۳ نفر استفاده میکنیم و Clean Architecture را برای پروژههای جدید در SwiftUI ترجیح میدهیم.
VIPER برای تست طراحی شده است — هر مؤلفه به صورت ایزوله از طریق پروتکلها تست میشود. Interactor با سرویسهای mock تست میشود: بررسی میشود که fetchUser با ID صحیح فراخوانی شده و نتیجه به Presenter منتقل شده است. Presenter با اشیاء mock View و Interactor تست میشود. Router با ناوبری mock تست میشود: بررسی میشود که navigateToProfile با userId صحیح فراخوانی شده و ماژول صحیح ایجاد شده است. View با تستهای UI (XCUITest) تست میشود.
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 برای VIPER به صورت دستی (کلاس با ویژگیهای ذخیرهسازی capture) یا از طریق کتابخانههای Cuckoo / Mockingbird ایجاد میشوند. کلاسهای mock دستی سادهتر و قابلفهمتر هستند، به ویژه برای آموزش توسعهدهندگان جدید. هر mock مقادیر ضبط شده (capturedUserId، displayedName) و پرچمهای فراخوانی (didShowLoading) را ذخیره میکند. در پایان تست نه تنها اینکه متد فراخوانی شده، بلکه با چه پارامترهایی فراخوانی شده نیز بررسی میشود — این اطمینان از صحت جریان داده را فراهم میکند.
پوشش کد در پروژههای VIPER IT Sectr به ۸۵–۹۵٪ برای Interactor، ۹۰–۹۵٪ برای Presenter، ۷۰–۸۰٪ برای Router، ۳۰–۵۰٪ برای View (از طریق تستهای UI) میرسد. View با تستهای اسکرینشات (SnapshotTesting، ۱٫۵K ستاره) تست میشود — این سریعتر از XCUITest است و موارد بیشتری را پوشش میدهد. پوشش کلی پروژه VIPER معمولاً ۷۰–۸۰٪ است که بالاتر از پروژه MVVM (۵۰–۶۵٪) است، اما به زمان بیشتری برای نوشتن تست نیاز دارد (۳۰–۴۰٪ زمان توسعه در مقابل ۲۰–۲۵٪ در MVVM).
سؤالات متداول
حداقل ۱۱ فایل: ۵ پروتکل (ViewProtocol، InteractorProtocol، PresenterProtocol، RouterProtocol، Entity)، ۵ پیادهسازی (ViewController، Interactor، Presenter، Router، Entity) و Builder/Assembler. با Mapper (Formatter) — ۱۲–۱۳. برای یک پروژه با ۵۰ صفحه این ۵۵۰–۶۵۰ فایل فقط برای ماژولهای VIPER است. MVVM به ۳ فایل در هر صفحه نیاز دارد (ViewModel، View، Model) — ۱۵۰ فایل برای ۵۰ صفحه.
بله، از نظر تئوری VIPER به Android قابل انتقال است، اما در عمل استفاده نمیشود — Google MVVM با Jetpack را توصیه میکند. VIPER برای iOS UIKit ایجاد شده است، جایی که ViewController به دلیل چرخه حیات به سختی تست میشود. در Android، Jetpack ViewModel مشکل تست را بدون ایزولاسیون VIPER حل میکند. معادل Android VIPER — Clean Architecture با تقسیم به module/feature است.
VIPER یک پیادهسازی خاص iOS از Clean Architecture است. Interactor معادل Use Case، Entity معادل Domain Model، Presenter معادل لایه Presentation است. Clean Architecture لایه Repository/Gateway را بین Interactor و دادهها اضافه میکند که در VIPER معمولاً در داخل Interactor پیادهسازی میشود. Clean Architecture Router را تجویز نمیکند — ناوبری به تصمیم پیادهسازی واگذار میشود.
خیر — SwiftUI برای MVVM + Combine طراحی شده است. VIPER در SwiftUI بیش از حد است: پنج مؤلفه برای یک صفحه با UI اعلانی سرباری بدون فایده است. برای SwiftUI MVVM یا TCA (The Composable Architecture) را انتخاب کنید. VIPER برای کدهای قدیمی UIKit و پروژههایی که iOS 12 و پایینتر حداقل نسخه هستند، همچنان مرتبط است.
از طریق Router. ماژول A router.navigateToProfile(userId: id) را فراخوانی میکند. Router A ماژول B را از طریق Builder ایجاد کرده و userId را منتقل میکند. انتقال برگشتی — از طریق دلیگیت: ماژول B پروتکل ModuleBDelegate را تعریف میکند، ماژول A آن را پیادهسازی کرده و از طریق Router منتقل میکند. رویدادهای سیستمی (خروج) — از طریق NotificationCenter.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.