VIPER: แนวคิดหลัก รูปแบบ View-Interactor-Presenter-Entity-Router

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-02-16 เวลาอ่าน: 9 นาที

VIPER (View-Interactor-Presenter-Entity-Router) — สถาปัตยกรรมแบบโมดูลที่พัฒนาโดย Mutual Mobile สำหรับแอปพลิเคชัน iOS VIPER แบ่งแอปพลิเคชันออกเป็นห้าชั้น: View รับผิดชอบการแสดงผล, Interactor รับผิดชอบตรรกะทางธุรกิจ, Presenter รับผิดชอบการเตรียมข้อมูล, Entity รับผิดชอบโมเดลข้อมูล, Router รับผิดชอบการนำทางระหว่างโมดูล VIPER เป็นการนำหลักการความรับผิดชอบเดียว (Single Responsibility Principle) ไปใช้อย่างละเอียดที่สุดในบรรดาสถาปัตยกรรมมือถือ อ่านเพิ่มเติมในบทความบน objc.io

ประเด็นสำคัญ

  • VIPER — ห้าองค์ประกอบ: View, Interactor, Presenter, Entity, Router พร้อมขอบเขตความรับผิดชอบที่ชัดเจน
  • ความเป็นโมดูล — แต่ละหน้าจอ (โมดูล) ถูกแยกออกจากกัน สื่อสารผ่านโปรโตคอล
  • Router — นำการนำทางออกจาก Presenter แก้ปัญหาการนำทางของ iOS
  • Interactor — มีตรรกะทางธุรกิจและไม่ขึ้นอยู่กับ UIKit ทดสอบได้ด้วย unit test
  • iOS-native — VIPER ถูกสร้างขึ้นสำหรับ UIKit ก่อน SwiftUI และยังคงเป็นมาตรฐานสำหรับโปรเจกต์ iOS ขนาดใหญ่

VIPER คืออะไร: ห้าองค์ประกอบของสถาปัตยกรรมแบบโมดูล

VIPER (View-Interactor-Presenter-Entity-Router) — รูปแบบสถาปัตยกรรมที่พัฒนาขึ้นในปี 2013–2014 ที่ Mutual Mobile สำหรับโปรเจกต์ iOS ขนาดใหญ่ แต่ละหน้าจอของแอปพลิเคชันเป็นโมดูลแยกจากกันของห้าองค์ประกอบที่มีความรับผิดชอบที่กำหนดไว้อย่างเคร่งครัด VIPER เป็นการนำหลักการความรับผิดชอบเดียว (Single Responsibility Principle) ไปใช้อย่างเข้มงวดที่สุดในการพัฒนามือถือ: ไม่มีองค์ประกอบใดทำสิ่งที่องค์ประกอบอื่นสามารถทำได้

View เป็นองค์ประกอบแบบพาสซีฟที่รับผิดชอบเฉพาะการแสดงข้อมูลที่ส่งโดย Presenter View ไม่มีตรรกะทางธุรกิจ ไม่จัดการนำทาง ไม่ทำคำขอเครือข่าย ใน iOS — UIViewController พร้อม ViewProtocol Interactor เป็นชั้นตรรกะทางธุรกิจที่ทำงานกับ Entity และบริการต่างๆ (เครือข่าย, DB, GPS) Interactor ไม่นำเข้า UIKit Presenter เป็นตัวกลางระหว่าง View และ Interactor: รับข้อมูลจาก Interactor จัดรูปแบบสำหรับการแสดงผล ส่งต่อไปยัง View Presenter ก็ไม่นำเข้า UIKit เช่นกัน Entity — โมเดลข้อมูล (struct, class) Router — จัดการการนำทาง: สร้างโมดูล เปิดหน้าจอ ส่งข้อมูลระหว่างโมดูล

องค์ประกอบความรับผิดชอบการพึ่งพา
Viewการแสดงผล, แอนิเมชัน, ท่าทางUIKit (เฉพาะ View)
Interactorตรรกะทางธุรกิจ, เครือข่าย, DBEntity, บริการ
Presenterการจัดรูปแบบข้อมูล, คำสั่ง ViewViewProtocol, Interactor
Entityโมเดลข้อมูลไม่มี
Routerการนำทาง, การสร้างโมดูลUIViewController (สำหรับการเปลี่ยนหน้าจอ)

ความสัมพันธ์ระหว่างองค์ประกอบ อธิบายโดยโปรโตคอล ViewProtocol กำหนดวิธีการแสดงผล, InteractorProtocol กำหนดวิธีการตรรกะทางธุรกิจ, PresenterProtocol กำหนดวิธีการจัดการเหตุการณ์, RouterProtocol กำหนดวิธีการนำทาง แต่ละองค์ประกอบสื่อสารกับองค์ประกอบอื่นผ่านโปรโตคอลเท่านั้น ซึ่งช่วยให้สามารถเปลี่ยนการนำไปใช้ได้ง่ายและทดสอบแบบแยกส่วน โดยเฉลี่ย โมดูล VIPER สำหรับหนึ่งหน้าจอประกอบด้วย 5 โปรโตคอล + 5 คลาส + 1 Builder/Assembler = 11 ไฟล์ต่อหน้าจอ

VIPER ใน Swift: โมดูล, Router และ Presenter

การสร้างโมดูล VIPER ทำใน Builder (หรือ Assembler) ซึ่งสร้างองค์ประกอบทั้งห้าและเชื่อมต่อผ่านโปรโตคอล Builder เป็นที่เดียวที่องค์ประกอบต่างๆ รู้จักประเภทคอนกรีตของกันและกัน หลังจากประกอบเสร็จ View จะถูกส่งกลับออกไปสำหรับการแสดงผล ส่วนที่เหลือของห่วงโซ่สะอาดและทดสอบแบบแยกส่วน

swift
// โปรโตคอล — 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 ที่ implement Dependency Injection ด้วยตนเอง การฉีดการพึ่งพาผ่านคอนสตรัคเตอร์ (constructor injection) รับประกันว่าองค์ประกอบไม่สามารถถูกสร้างขึ้นได้โดยปราศจากการพึ่งพาของมัน ใน VIPER สมัยใหม่ Builder สามารถใช้ Swinject (คอนเทนเนอร์ DI) แต่การประกอบด้วยตนเองยังคงโปร่งใสกว่าสำหรับการทดสอบ ที่ IT Sectr เราประยุกต์ใช้ VIPER ด้วยการประกอบด้วยตนเองสำหรับโมดูลที่มีตรรกะซับซ้อน — ซึ่งช่วยให้ผู้พัฒนารายใหม่อ่านโค้ดได้ง่ายขึ้น

Mapper (Formatter) เป็นองค์ประกอบที่หกเสริมของ VIPER Mapper แปลง Entity (โมเดล DB/เซิร์ฟเวอร์) เป็น ViewModel (โมเดลการแสดงผล) Entity ประกอบด้วย UserDTO พร้อมฟิลด์ id, first_name, last_name, email ViewModel — UserDisplayItem พร้อม name (first_name + last_name) และ email Mapper ทำงานใน Presenter หากการแมปข้อมูลซับซ้อน (หลาย Entity → หนึ่ง ViewModel) Mapper จะถูกแยกออกเป็นคลาสแยกต่างหากสำหรับการทดสอบ

การสื่อสารระหว่างโมดูล VIPER

โมดูล VIPER ถูกแยกออกจากกัน และไม่รู้จักกันและกัน การสื่อสารระหว่างโมดูลเกิดขึ้นผ่าน Router เมื่อผู้ใช้คลิกปุ่ม "โปรไฟล์" บนหน้าจอผู้ใช้ Presenter เรียก router.navigateToProfile(userId: 42) Router สร้างโมดูลใหม่ผ่าน ProfileModuleBuilder.build(userId: 42) และเปิดผ่าน navigationController.push การไหลของข้อมูล: โมดูล A → Router A → Builder โมดูล B → โมดูล B ถูกสร้างและเปิด

การส่งข้อมูลกลับ (ตัวอย่างเช่น เลือกเมืองบนหน้าจอเลือก → กลับไปยังหน้าจอแก้ไขโปรไฟล์) ใน VIPER implement ผ่านตัวแทนหรือคลosure โมดูล B กำหนดโปรโตคอล ModuleBDelegate ด้วยเมธอด didSelectCity(_ city: City) โมดูล A implement โปรโตคอลนี้ Router A ส่งตัวแทนไปยัง Builder โมดูล B เมื่อเลือกเมือง โมดูล B เรียก delegate?.didSelectCity(city) นี่เป็นแนวทางปฏิบัติมาตรฐานของ iOS ที่นักพัฒนา UIKit ทุกคนคุ้นเคย

สถานการณ์กลไกตัวอย่าง
การนำทางไปข้างหน้าRouter → BuildernavigateToProfile(userId:)
การส่งข้อมูลกลับDelegatedidSelectCity(_:)
การแจ้งเตือนระบบNotificationCenterUserDidLogout
เหตุการณ์จาก InteractorPresenter → Viewข้อความ WebSocket

NotificationCenter ใช้สำหรับเหตุการณ์ของระบบ (ออกจากระบบ, เปลี่ยนแผน, การแจ้งเตือนแบบพุช) ที่ส่งผลกระทบต่อหลายโมดูลพร้อมกัน Router หรือ AppDelegate สมัครรับ Notification สร้างโมดูลที่ต้องการหรืออัปเดตสถานะ VIPER ไม่ห้าม NotificationCenter — สิ่งสำคัญคือใช้เฉพาะสำหรับเหตุการณ์ 1-to-many ในขณะที่การสื่อสาร 1-to-1 ใช้ตัวแทนหรือคลosure

การเปรียบเทียบ VIPER กับ MVVM และ Clean Architecture

VIPER vs MVVM — VIPER ต้องใช้โค้ดมากกว่า 2–3 เท่าต่อหน้าจอ แต่ให้การแยกองค์ประกอบอย่างสมบูรณ์ MVVM กับ ViewModel + SwiftUI ง่ายและเร็วกว่า แต่ขยายขนาดได้แย่กว่าสำหรับทีมที่มีนักพัฒนา 5+ คน 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 ในคำศัพท์ของ Robert Martin Clean Architecture เพิ่มชั้น Gateway/Repository ระหว่าง Interactor และข้อมูล ซึ่งโดยปกติแล้วจะไม่แยกออกจากกันใน VIPER สำหรับโปรเจกต์ SwiftUI สมัยใหม่ ทีมส่วนใหญ่เลือก Clean Architecture (The Composable Architecture) หรือ MVVM โดยปล่อยให้ VIPER สำหรับ UIKit ที่เป็นมรดกตกทอด

เมื่อใดควรเลือก VIPER — ทีมที่มีนักพัฒนา 5+ คน, โปรเจกต์ UIKit ที่มี 50+ หน้าจอ, ข้อกำหนดการทดสอบสูงกว่า 80%, iOS เท่านั้น (VIPER ไม่สามารถพอร์ตไปยัง Android โดยไม่ต้องเขียนใหม่) VIPER ให้โครงสร้างที่คาดการณ์ได้: นักพัฒนาใหม่เข้าใจโมดูลภายใน 15 นาที อย่างไรก็ตาม ความเร็วในการพัฒนาต่ำกว่า 20–30% เมื่อเทียบกับ MVVM เนื่องจากมีไฟล์จำนวนมากกว่า ที่ IT Sectr เราใช้ VIPER สำหรับโปรเจกต์ระดับองค์กรบน UIKit กับทีมที่มี 3+ คนและเลือก Clean Architecture สำหรับโปรเจกต์ SwiftUI ใหม่

การทดสอบโมดูล VIPER

VIPER ถูกออกแบบมาสำหรับการทดสอบ — แต่ละองค์ประกอบถูกทดสอบแบบแยกส่วนผ่านโปรโตคอล Interactor ถูกทดสอบด้วยบริการจำลอง: ตรวจสอบว่า fetchUser ถูกเรียกด้วย ID ที่ถูกต้องและผลลัพธ์ถูกส่งไปยัง Presenter Presenter ถูกทดสอบด้วยออบเจกต์จำลองของ View และ Interactor Router ถูกทดสอบด้วยการนำทางจำลอง: ตรวจสอบว่า navigateToProfile ถูกเรียกด้วย userId ที่ถูกต้องและโมดูลที่ถูกต้องถูกสร้างขึ้น View ถูกทดสอบด้วยการทดสอบ 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)
    }
}

ออบเจกต์จำลอง สำหรับ VIPER ถูกสร้างขึ้นด้วยตนเอง (คลาสพร้อมคุณสมบัติการจับภาพที่เก็บไว้) หรือผ่านไลบรารีเช่น Cuckoo / Mockingbird คลาสจำลองแบบทำด้วยตนเองง่ายและชัดเจนกว่า โดยเฉพาะสำหรับการฝึกอบรมนักพัฒนาใหม่ การจำลองแต่ละครั้งจะเก็บค่าที่จับไว้ (capturedUserId, displayedName) และแฟล็กการเรียก (didShowLoading) เมื่อสิ้นสุดการทดสอบ ไม่เพียงตรวจสอบว่าเมธอดถูกเรียก แต่ยังตรวจสอบด้วยว่าถูกเรียกด้วยพารามิเตอร์ใด — ซึ่งให้ความมั่นใจในความถูกต้องของการไหลของข้อมูล

ความครอบคลุมโค้ด ในโปรเจกต์ VIPER ของ IT Sectr ถึง 85–95% สำหรับ Interactor, 90–95% สำหรับ Presenter, 70–80% สำหรับ Router, 30–50% สำหรับ View (ผ่านการทดสอบ UI) View ถูกทดสอบด้วยการทดสอบภาพหน้าจอ (SnapshotTesting, 1.5K ดาว) — ซึ่งเร็วกว่า XCUITest และครอบคลุมกรณีต่างๆ มากกว่า ความครอบคลุมโดยรวมของโปรเจกต์ VIPER โดยทั่วไปคือ 70–80% ซึ่งสูงกว่าโปรเจกต์ MVVM (50–65%) แต่ใช้เวลาในการเขียนทดสอบมากกว่า (30–40% ของเวลาในการพัฒนากับ 20–25% ใน MVVM)

คำถามที่พบบ่อย

โมดูล VIPER หนึ่งโมดูลมีกี่ไฟล์?

อย่างน้อย 11 ไฟล์: 5 โปรโตคอล (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 การ implement (ViewController, Interactor, Presenter, Router, Entity) และ Builder/Assembler พร้อม Mapper (Formatter) — 12–13 สำหรับโปรเจกต์ 50 หน้าจอ นั่นคือ 550–650 ไฟล์เฉพาะโมดูล VIPER MVVM ต้องการ 3 ไฟล์ต่อหน้าจอ (ViewModel, View, Model) — 150 ไฟล์สำหรับ 50 หน้าจอ

สามารถใช้ VIPER บน Android ได้หรือไม่?

ได้ ในทางทฤษฎี VIPER สามารถพอร์ตไปยัง Android ได้ แต่ในทางปฏิบัติไม่ถูกใช้ — Google แนะนำ MVVM กับ Jetpack VIPER ถูกสร้างขึ้นสำหรับ iOS UIKit ซึ่ง ViewController ทดสอบได้ยากเนื่องจากวงจรชีวิตของมัน บน Android Jetpack ViewModel แก้ปัญหาการทดสอบโดยไม่ต้องแยก VIPER สิ่งที่เทียบเท่ากับ VIPER บน Android คือ Clean Architecture พร้อมการแบ่งโมดูล/ฟีเจอร์

ความแตกต่างระหว่าง VIPER และ Clean Architecture คืออะไร?

VIPER เป็นการ implement เฉพาะของ iOS สำหรับ Clean Architecture Interactor สอดคล้องกับ Use Case, Entity — Domain Model, Presenter — ชั้น Presentation Clean Architecture เพิ่ม Repository/Gateway ระหว่าง Interactor และข้อมูล ซึ่งใน VIPER โดยปกติจะ implement ภายใน Interactor Clean Architecture ไม่กำหนด Router — การนำทางขึ้นอยู่กับการ implement

จำเป็นต้องใช้ VIPER สำหรับโปรเจกต์ SwiftUI หรือไม่?

ไม่ — SwiftUI ถูกออกแบบมาสำหรับ MVVM + Combine VIPER ใน SwiftUI ซ้ำซ้อน: ห้าองค์ประกอบต่อหน้าจอด้วย UI แบบ declarative เป็นภาระที่ไม่มีประโยชน์ สำหรับ SwiftUI เลือก MVVM หรือ TCA (The Composable Architecture) VIPER ยังคงเกี่ยวข้องสำหรับ UIKit ที่เป็นมรดกตกทอดและโปรเจกต์ที่ iOS 12 และต่ำกว่าเป็นเวอร์ชันขั้นต่ำ

จะส่งข้อมูลระหว่างโมดูล VIPER ได้อย่างไร?

ผ่าน Router โมดูล A เรียก router.navigateToProfile(userId: id) Router A สร้างโมดูล B ผ่าน Builder ส่ง userId การสื่อสารตอบกลับ — ผ่านตัวแทน: โมดูล B กำหนดโปรโตคอล ModuleBDelegate, โมดูล A implement และส่งผ่าน Router เหตุการณ์ของระบบ (ออกจากระบบ) — ผ่าน NotificationCenter

สรุป

  • VIPER — ห้าองค์ประกอบที่มีการแยกอย่างเคร่งครัด: View, Interactor, Presenter, Entity, Router
  • ความเป็นโมดูล — แต่ละหน้าจอถูกแยกออกจากกัน, Builder ประกอบการพึ่งพาผ่าน constructor injection
  • Router — นำการนำทางออกจาก Presenter แก้ปัญหาการนำทางบน iOS
  • Interactor — ตรรกะทางธุรกิจที่สะอาดโดยไม่มี UIKit ทดสอบได้ด้วย unit test
  • ปริมาณโค้ด — 11+ ไฟล์ต่อหน้าจอ, พัฒนาช้ากว่า MVVM 20–30%
  • การทดสอบ — ความครอบคลุม 70–80%, Interactor และ Presenter ทดสอบผ่านออบเจกต์จำลอง
  • SwiftUI vs UIKit — VIPER สำหรับ UIKit (มรดกตกทอด), MVVM/TCA สำหรับ SwiftUI

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม