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) — รูปแบบสถาปัตยกรรมที่พัฒนาขึ้นในปี 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 | ตรรกะทางธุรกิจ, เครือข่าย, DB | Entity, บริการ |
| Presenter | การจัดรูปแบบข้อมูล, คำสั่ง View | ViewProtocol, Interactor |
| Entity | โมเดลข้อมูล | ไม่มี |
| Router | การนำทาง, การสร้างโมดูล | UIViewController (สำหรับการเปลี่ยนหน้าจอ) |
ความสัมพันธ์ระหว่างองค์ประกอบ อธิบายโดยโปรโตคอล ViewProtocol กำหนดวิธีการแสดงผล, InteractorProtocol กำหนดวิธีการตรรกะทางธุรกิจ, PresenterProtocol กำหนดวิธีการจัดการเหตุการณ์, RouterProtocol กำหนดวิธีการนำทาง แต่ละองค์ประกอบสื่อสารกับองค์ประกอบอื่นผ่านโปรโตคอลเท่านั้น ซึ่งช่วยให้สามารถเปลี่ยนการนำไปใช้ได้ง่ายและทดสอบแบบแยกส่วน โดยเฉลี่ย โมดูล VIPER สำหรับหนึ่งหน้าจอประกอบด้วย 5 โปรโตคอล + 5 คลาส + 1 Builder/Assembler = 11 ไฟล์ต่อหน้าจอ
การสร้างโมดูล 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 ที่ 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 ถูกแยกออกจากกัน และไม่รู้จักกันและกัน การสื่อสารระหว่างโมดูลเกิดขึ้นผ่าน 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 → Builder | navigateToProfile(userId:) |
| การส่งข้อมูลกลับ | Delegate | didSelectCity(_:) |
| การแจ้งเตือนระบบ | NotificationCenter | UserDidLogout |
| เหตุการณ์จาก Interactor | Presenter → View | ข้อความ WebSocket |
NotificationCenter ใช้สำหรับเหตุการณ์ของระบบ (ออกจากระบบ, เปลี่ยนแผน, การแจ้งเตือนแบบพุช) ที่ส่งผลกระทบต่อหลายโมดูลพร้อมกัน Router หรือ AppDelegate สมัครรับ Notification สร้างโมดูลที่ต้องการหรืออัปเดตสถานะ VIPER ไม่ห้าม NotificationCenter — สิ่งสำคัญคือใช้เฉพาะสำหรับเหตุการณ์ 1-to-many ในขณะที่การสื่อสาร 1-to-1 ใช้ตัวแทนหรือคลosure
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 ถูกออกแบบมาสำหรับการทดสอบ — แต่ละองค์ประกอบถูกทดสอบแบบแยกส่วนผ่านโปรโตคอล Interactor ถูกทดสอบด้วยบริการจำลอง: ตรวจสอบว่า fetchUser ถูกเรียกด้วย ID ที่ถูกต้องและผลลัพธ์ถูกส่งไปยัง Presenter Presenter ถูกทดสอบด้วยออบเจกต์จำลองของ View และ Interactor Router ถูกทดสอบด้วยการนำทางจำลอง: ตรวจสอบว่า 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)
}
}
ออบเจกต์จำลอง สำหรับ 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)
คำถามที่พบบ่อย
อย่างน้อย 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 ได้ แต่ในทางปฏิบัติไม่ถูกใช้ — Google แนะนำ MVVM กับ Jetpack VIPER ถูกสร้างขึ้นสำหรับ iOS UIKit ซึ่ง ViewController ทดสอบได้ยากเนื่องจากวงจรชีวิตของมัน บน Android Jetpack ViewModel แก้ปัญหาการทดสอบโดยไม่ต้องแยก VIPER สิ่งที่เทียบเท่ากับ VIPER บน Android คือ 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
ไม่ — SwiftUI ถูกออกแบบมาสำหรับ MVVM + Combine VIPER ใน SwiftUI ซ้ำซ้อน: ห้าองค์ประกอบต่อหน้าจอด้วย UI แบบ declarative เป็นภาระที่ไม่มีประโยชน์ สำหรับ SwiftUI เลือก MVVM หรือ TCA (The Composable Architecture) VIPER ยังคงเกี่ยวข้องสำหรับ UIKit ที่เป็นมรดกตกทอดและโปรเจกต์ที่ iOS 12 และต่ำกว่าเป็นเวอร์ชันขั้นต่ำ
ผ่าน Router โมดูล A เรียก router.navigateToProfile(userId: id) Router A สร้างโมดูล B ผ่าน Builder ส่ง userId การสื่อสารตอบกลับ — ผ่านตัวแทน: โมดูล B กำหนดโปรโตคอล ModuleBDelegate, โมดูล A implement และส่งผ่าน Router เหตุการณ์ของระบบ (ออกจากระบบ) — ผ่าน NotificationCenter
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม