VIPER: các khái niệm chính, mẫu View-Interactor-Presenter-Entity-Router

Tác giả: IT Sectr Đã đăng: 2026-02-16 Thời gian đọc: 9 phút

VIPER (View-Interactor-Presenter-Entity-Router) là một kiến trúc mô-đun được phát triển tại Mutual Mobile cho các ứng dụng iOS. VIPER chia ứng dụng thành năm lớp: View chịu trách nhiệm hiển thị, Interactor chịu trách nhiệm logic nghiệp vụ, Presenter chịu trách nhiệm chuẩn bị dữ liệu, Entity chịu trách nhiệm mô hình dữ liệu, Router chịu trách nhiệm điều hướng giữa các mô-đun. VIPER là triển khai chi tiết nhất của nguyên tắc đơn trách nhiệm trong các kiến trúc di động. Đọc thêm trong bài viết trên objc.io.

Những điểm chính

  • VIPER — năm thành phần: View, Interactor, Presenter, Entity, Router với ranh giới trách nhiệm rõ ràng
  • Tính mô-đun — mỗi màn hình (mô-đun) được cô lập, giao tiếp qua giao thức
  • Router — đưa điều hướng ra khỏi Presenter, giải quyết vấn đề điều hướng iOS
  • Interactor — chứa logic nghiệp vụ và không phụ thuộc vào UIKit, có thể kiểm thử bằng unit test
  • iOS-native — VIPER được tạo cho UIKit trước SwiftUI và vẫn là tiêu chuẩn cho các dự án iOS lớn

VIPER là gì: năm thành phần của kiến trúc mô-đun

VIPER (View-Interactor-Presenter-Entity-Router) là một mẫu kiến trúc được phát triển vào năm 2013–2014 tại Mutual Mobile cho các dự án iOS lớn. Mỗi màn hình ứng dụng là một mô-đun riêng biệt gồm năm thành phần với trách nhiệm được xác định chặt chẽ. VIPER là triển khai nghiêm ngặt nhất của Nguyên tắc Đơn trách nhiệm (Single Responsibility Principle) trong phát triển di động: không thành phần nào làm việc mà thành phần khác có thể làm.

View là thành phần thụ động chỉ chịu trách nhiệm hiển thị dữ liệu do Presenter truyền đến. View không chứa logic nghiệp vụ, không xử lý điều hướng, không thực hiện yêu cầu mạng. Trong iOS — UIViewController với ViewProtocol. Interactor là lớp logic nghiệp vụ làm việc với Entity và các dịch vụ (mạng, DB, GPS). Interactor không import UIKit. Presenter là trung gian giữa View và Interactor: nhận dữ liệu từ Interactor, định dạng để hiển thị, truyền cho View. Presenter cũng không import UIKit. Entity — mô hình dữ liệu (struct, class). Router — quản lý điều hướng: tạo mô-đun, mở màn hình, truyền dữ liệu giữa các mô-đun.

Thành phầnTrách nhiệmPhụ thuộc
ViewHiển thị, hoạt ảnh, cử chỉUIKit (chỉ View)
InteractorLogic nghiệp vụ, mạng, DBEntity, dịch vụ
PresenterĐịnh dạng dữ liệu, lệnh ViewViewProtocol, Interactor
EntityMô hình dữ liệuKhông
RouterĐiều hướng, tạo mô-đunUIViewController (cho chuyển tiếp)

Mối quan hệ giữa các thành phần được mô tả bằng giao thức. ViewProtocol định nghĩa phương thức hiển thị, InteractorProtocol định nghĩa phương thức logic nghiệp vụ, PresenterProtocol định nghĩa phương thức xử lý sự kiện, RouterProtocol định nghĩa phương thức điều hướng. Mỗi thành phần giao tiếp với thành phần khác chỉ thông qua giao thức, cho phép dễ dàng thay thế triển khai và kiểm thử cô lập. Trung bình, một mô-đun VIPER cho một màn hình chứa 5 giao thức + 5 lớp + 1 Builder/Assembler = 11 tệp cho mỗi màn hình.

VIPER trong Swift: Mô-đun, Router và Presenter

Xây dựng mô-đun VIPER được thực hiện trong Builder (hoặc Assembler), tạo ra cả năm thành phần và kết nối chúng qua các giao thức. Builder là nơi duy nhất các thành phần biết kiểu cụ thể của nhau. Sau khi lắp ráp, View được trả ra bên ngoài để hiển thị, phần còn lại của chuỗi sạch sẽ và được kiểm thử cô lập.

swift
// Giao thức — View
protocol UserViewProtocol: AnyObject {
    func display(name: String)
    func display(email: String)
    func showLoading()
    func hideLoading()
}

// Giao thức — Interactor
protocol UserInteractorProtocol {
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}

// Giao thức — Router
protocol UserRouterProtocol {
    func navigateToProfile(userId: Int)
}

// Interactor — logic nghiệp vụ
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 — chuẩn bị dữ liệu
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ử lý lỗi
            }
        }
    }
}

// Router — điều hướng
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 — lắp ráp mô-đun
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 là thành phần chính của VIPER, triển khai Dependency Injection thủ công. Việc tiêm phụ thuộc qua hàm tạo (constructor injection) đảm bảo rằng một thành phần không thể được tạo mà không có phụ thuộc của nó. Trong VIPER hiện đại, Builder có thể sử dụng Swinject (container DI), nhưng lắp ráp thủ công vẫn minh bạch hơn cho việc kiểm thử. Tại IT Sectr, chúng tôi áp dụng VIPER với lắp ráp thủ công cho các mô-đun có logic phức tạp — điều này giúp đơn giản hóa việc đọc mã cho nhà phát triển mới.

Mapper (Formatter) là thành phần thứ sáu tùy chọn của VIPER. Mapper chuyển đổi Entity (mô hình DB/máy chủ) thành ViewModel (mô hình hiển thị). Entity chứa UserDTO với các trường id, first_name, last_name, email. ViewModel — UserDisplayItem với name (first_name + last_name) và email. Mapper được thực thi trong Presenter. Nếu ánh xạ dữ liệu phức tạp (nhiều Entity → một ViewModel), Mapper được trích xuất thành một lớp riêng để kiểm thử.

Giao tiếp giữa các mô-đun VIPER

Các mô-đun VIPER được cô lập và không biết về nhau. Giao tiếp giữa các mô-đun xảy ra qua Router. Khi người dùng nhấp vào nút "Hồ sơ" trên màn hình người dùng, Presenter gọi router.navigateToProfile(userId: 42). Router tạo một mô-đun mới qua ProfileModuleBuilder.build(userId: 42) và mở nó qua navigationController.push. Luồng dữ liệu: Mô-đun A → Router A → Builder Mô-đun B → Mô-đun B được tạo và mở.

Truyền dữ liệu trở lại (ví dụ: chọn thành phố trên màn hình lựa chọn → quay lại màn hình chỉnh sửa hồ sơ) trong VIPER được triển khai qua delegate hoặc closure. Mô-đun B định nghĩa giao thức ModuleBDelegate với phương thức didSelectCity(_ city: City). Mô-đun A triển khai giao thức này. Router A truyền delegate cho Builder Mô-đun B. Khi một thành phố được chọn, Mô-đun B gọi delegate?.didSelectCity(city). Đây là thực tiễn tiêu chuẩn của iOS, quen thuộc với mọi nhà phát triển UIKit.

Kịch bảnCơ chếVí dụ
Điều hướng tiếpRouter → BuildernavigateToProfile(userId:)
Truyền dữ liệu trở lạiDelegatedidSelectCity(_:)
Thông báo hệ thốngNotificationCenterUserDidLogout
Sự kiện từ InteractorPresenter → ViewTin nhắn WebSocket

NotificationCenter được sử dụng cho các sự kiện hệ thống (đăng xuất, thay đổi gói, thông báo đẩy) ảnh hưởng đến nhiều mô-đun cùng lúc. Router hoặc AppDelegate đăng ký Notification, tạo mô-đun cần thiết hoặc cập nhật trạng thái. VIPER không cấm NotificationCenter — điều quan trọng là nó chỉ được sử dụng cho các sự kiện 1-nhiều, trong khi giao tiếp 1-1 sử dụng delegate hoặc closure.

So sánh VIPER với MVVM và Clean Architecture

VIPER vs MVVM — VIPER yêu cầu lượng mã gấp 2–3 lần cho mỗi màn hình nhưng cung cấp sự cô lập thành phần tuyệt đối. MVVM với ViewModel + SwiftUI đơn giản và nhanh hơn nhưng khả năng mở rộng kém hơn cho nhóm 5+ nhà phát triển. VIPER xác định chặt chẽ ai chịu trách nhiệm cho việc gì: Interactor — chỉ logic nghiệp vụ, Presenter — định dạng, Router — điều hướng. Trong MVVM, ViewModel thường phình to, đảm nhận điều hướng và logic nghiệp vụ.

VIPER vs Clean Architecture — VIPER là một trường hợp cụ thể của Clean Architecture được điều chỉnh cho iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller theo thuật ngữ của Robert Martin. Clean Architecture thêm lớp Gateway/Repository giữa Interactor và dữ liệu, thường không được tách riêng trong VIPER. Đối với các dự án SwiftUI hiện đại, hầu hết các nhóm chọn Clean Architecture (The Composable Architecture) hoặc MVVM, để lại VIPER cho các dự án UIKit cũ.

Khi nào chọn VIPER — nhóm 5+ nhà phát triển, dự án UIKit có 50+ màn hình, yêu cầu kiểm thử trên 80%, chỉ iOS (VIPER không thể chuyển sang Android mà không viết lại). VIPER cung cấp cấu trúc có thể dự đoán: một nhà phát triển mới hiểu một mô-đun trong 15 phút. Tuy nhiên, tốc độ phát triển thấp hơn 20–30% so với MVVM do số lượng tệp lớn hơn. Tại IT Sectr, chúng tôi sử dụng VIPER cho các dự án doanh nghiệp UIKit với nhóm từ 3 người và ưu tiên Clean Architecture cho các dự án SwiftUI mới.

Kiểm thử mô-đun VIPER

VIPER được thiết kế cho kiểm thử — mỗi thành phần được kiểm thử cô lập qua giao thức. Interactor được kiểm thử với dịch vụ giả: kiểm tra rằng fetchUser được gọi với ID chính xác và kết quả được truyền cho Presenter. Presenter được kiểm thử với đối tượng giả của View và Interactor. Router được kiểm thử với điều hướng giả: kiểm tra rằng navigateToProfile được gọi với userId chính xác và mô-đun chính xác được tạo. View được kiểm thử bằng kiểm thử UI (XCUITest).

swift
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)
    }
}

Đối tượng giả cho VIPER được tạo thủ công (lớp với thuộc tính capture được lưu trữ) hoặc qua thư viện như Cuckoo / Mockingbird. Các lớp giả thủ công đơn giản và rõ ràng hơn, đặc biệt cho việc đào tạo nhà phát triển mới. Mỗi đối tượng giả lưu trữ giá trị capture (capturedUserId, displayedName) và cờ gọi (didShowLoading). Kết thúc kiểm thử, không chỉ kiểm tra rằng phương thức đã được gọi, mà còn với tham số nào — điều này mang lại sự tự tin về tính đúng đắn của luồng dữ liệu.

Độ phủ mã trong các dự án VIPER của IT Sectr đạt 85–95% cho Interactor, 90–95% cho Presenter, 70–80% cho Router, 30–50% cho View (qua kiểm thử UI). View được kiểm thử bằng kiểm thử ảnh chụp màn hình (SnapshotTesting, 1,5K sao) — nhanh hơn XCUITest và bao phủ nhiều trường hợp hơn. Độ phủ tổng thể của một dự án VIPER thường là 70–80%, cao hơn dự án MVVM (50–65%), nhưng cần nhiều thời gian hơn để viết kiểm thử (30–40% thời gian phát triển so với 20–25% trong MVVM).

Câu hỏi thường gặp

Có bao nhiêu tệp trong một mô-đun VIPER?

Tối thiểu 11 tệp: 5 giao thức (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 triển khai (ViewController, Interactor, Presenter, Router, Entity) và Builder/Assembler. Với Mapper (Formatter) — 12–13. Cho một dự án 50 màn hình, đó là 550–650 tệp chỉ riêng mô-đun VIPER. MVVM yêu cầu 3 tệp cho mỗi màn hình (ViewModel, View, Model) — 150 tệp cho 50 màn hình.

Có thể sử dụng VIPER trên Android không?

Có, về mặt lý thuyết VIPER có thể chuyển sang Android, nhưng trong thực tế không được sử dụng — Google khuyến nghị MVVM với Jetpack. VIPER được tạo cho iOS UIKit, nơi ViewController khó kiểm thử do vòng đời của nó. Trên Android, Jetpack ViewModel giải quyết vấn đề kiểm thử mà không cần cô lập VIPER. Tương đương của VIPER trên Android là Clean Architecture với phân chia mô-đun/tính năng.

Sự khác biệt giữa VIPER và Clean Architecture là gì?

VIPER là triển khai dành riêng cho iOS của Clean Architecture. Interactor tương ứng với Use Case, Entity — Domain Model, Presenter — lớp Presentation. Clean Architecture thêm Repository/Gateway giữa Interactor và dữ liệu, trong VIPER thường được triển khai bên trong Interactor. Clean Architecture không quy định Router — điều hướng được để tùy theo triển khai.

Có cần VIPER cho dự án SwiftUI không?

Không — SwiftUI được thiết kế cho MVVM + Combine. VIPER trong SwiftUI là dư thừa: năm thành phần cho mỗi màn hình với UI khai báo là chi phí không có lợi ích. Cho SwiftUI, hãy chọn MVVM hoặc TCA (The Composable Architecture). VIPER vẫn phù hợp cho các dự án UIKit cũ và các dự án có iOS 12 trở xuống là phiên bản tối thiểu.

Làm thế nào để truyền dữ liệu giữa các mô-đun VIPER?

Qua Router. Mô-đun A gọi router.navigateToProfile(userId: id). Router A tạo mô-đun B qua Builder, truyền userId. Giao tiếp trả về — qua delegate: mô-đun B định nghĩa giao thức ModuleBDelegate, mô-đun A triển khai nó và truyền qua Router. Sự kiện hệ thống (đăng xuất) — qua NotificationCenter.

Tổng kết

  • VIPER — năm thành phần với sự tách biệt chặt chẽ: View, Interactor, Presenter, Entity, Router
  • Tính mô-đun — mỗi màn hình được cô lập, Builder lắp ráp phụ thuộc qua constructor injection
  • Router — đưa điều hướng ra khỏi Presenter, giải quyết vấn đề điều hướng trên iOS
  • Interactor — logic nghiệp vụ sạch không UIKit, kiểm thử bằng unit test
  • Khối lượng mã — 11+ tệp cho mỗi màn hình, phát triển chậm hơn 20–30% so với MVVM
  • Kiểm thử — độ phủ 70–80%, Interactor và Presenter được kiểm thử qua đối tượng giả
  • SwiftUI vs UIKit — VIPER cho UIKit (cũ), MVVM/TCA cho SwiftUI

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm