Repository Pattern — รูปแบบที่เพิ่มชั้นนามธรรมระหว่างตรรกะทางธุรกิจและแหล่งข้อมูล แทนที่จะเรียก API ฐานข้อมูล หรือแคชโดยตรง Repository จะจัดเตรียมอินเทอร์เฟซแบบรวมสำหรับการรับและจัดเก็บข้อมูล ซึ่งช่วยให้การทดสอบและการสลับระหว่างแหล่งข้อมูลง่ายขึ้น อ่านเพิ่มเติมได้ใน เอกสาร Android Data Layer
ประเด็นสำคัญ
Repository Pattern เป็นรูปแบบเชิงโครงสร้างที่แยกตรรกะทางธุรกิจออกจากการเข้าถึงแหล่งข้อมูลโดยตรง แทนที่ Activity, UIViewController หรือ ViewModel จะเรียก Retrofit, URLSession, Room หรือ CoreData โดยตรง พวกมันจะสื่อสารกับ Repository Repository ตัดสินใจว่าจะรับข้อมูลจากที่ไหน — จากเครือข่าย ฐานข้อมูล หรือแคช — และส่งคืนผลลัพธ์ในรูปแบบที่รวมเป็นหนึ่งเดียว ซึ่งใช้หลักการความรับผิดชอบเดียว — UI ไม่ทราบว่าข้อมูลถูกได้รับมาอย่างไรหรือจากที่ไหน
ส่วนประกอบของ Repository ประกอบด้วยอินเทอร์เฟซ (โปรโตคอล) การนำไปใช้ และ DataSource หนึ่งรายการขึ้นไป DataSource คือคลาสที่ทำงานกับแหล่งเดียว: RemoteDataSource เรียก API ผ่าน HTTP ไคลเอ็นต์, LocalDataSource อ่านและเขียนในฐานข้อมูล Repository รับ DataSources ผ่านคอนสตรักเตอร์ (Dependency Injection) และตัดสินใจว่าจะใช้แหล่งใด ตัวอย่างเช่น เมื่อขอรายชื่อผู้ใช้ Repository จะตรวจสอบแคชก่อน จากนั้นฐานข้อมูล จากนั้นเครือข่าย
ข้อดี ของ Repository Pattern: การแยกการเปลี่ยนแปลงแหล่งข้อมูล (การเปลี่ยนแปลง API, การย้าย DB) ไม่กระทบต่อชั้น UI; การทดสอบหน่วยผ่านการแทนที่ Repository หรือ DataSource; การแคชโปร่งใสต่อ UI; การสลับระหว่างโหมดออนไลน์และออฟไลน์โดยไม่เปลี่ยนแปลงตรรกะของหน้าจอ ชุมชน Android แนะนำ Repository เป็นชั้นบังคับใน Clean Architecture
การนำไปใช้ใน iOS ของ Repository สร้างขึ้นบนโปรโตคอลของ Swift โปรโตคอล Repository ประกาศวิธีการสำหรับการรับและจัดเก็บข้อมูล การนำไปใช้จริงถูกฉีดผ่านตัวเริ่มต้น — ซึ่งช่วยให้สามารถเปลี่ยนการนำไปใช้ในการทดสอบและตัวอย่างแสดง SwiftUI DataSources ก็ถูกประกาศเป็นโปรโตคอลเช่นกัน: Protocol RemoteDataSource, Protocol LocalDataSource ViewModel หรือ Interactor ไม่ทราบเกี่ยวกับการนำไปใช้เฉพาะ — ทราบเพียงโปรโตคอล Repository เท่านั้น
protocol UserRepository {
func getUsers() async throws -> [User]
}
protocol UserRemoteDataSource {
func fetchUsers() async throws -> [User]
}
protocol UserLocalDataSource {
func getCachedUsers() throws -> [User]
func saveUsers(_: [User]) throws
}
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
init(remote: UserRemoteDataSource, local: UserLocalDataSource) {
self.remote = remote
self.local = local
}
func getUsers() async throws -> [User] {
if let cached = try? local.getCachedUsers() {
return cached
}
let users = try await remote.fetchUsers()
try local.saveUsers(users)
return users
}
}
Dependency Injection ใน iOS สำหรับ Repository โดยทั่วไปกำหนดค่าผ่านโรงงานหรือคอนเทนเนอร์ DI (Swinject, Factory) ในการทดสอบ โปรโตคอล UserRepository จะถูกแทนที่ด้วยการนำไปใช้จำลองที่ส่งคืนข้อมูลที่กำหนดไว้ล่วงหน้า Async-awatt ทำให้โค้ดทำงานแบบซิงโครนัสและอ่านง่ายโดยไม่ต้องใช้ closures และ delegates สำหรับการตอบสนองแบบ Combine เมธอดของ Repository จะส่งคืน AnyPublisher แทน async throws
การนำไปใช้ใน Android ของ Repository ใช้ Kotlin Coroutines และ Flow อย่างกว้างขวางสำหรับการทำงานแบบอะซิงโครนัส Google แนะนำ Repository ในคู่มือสถาปัตยกรรม Android อย่างเป็นทางการ (Android Architecture Components) Repository รับ RemoteDataSource (Retrofit) และ LocalDataSource (Room) ผ่านคอนสตรักเตอร์ และ ViewModel สมัครสมาชิก Flow จาก Repository Repository จัดการกลยุทธ์ข้อมูล: แคชก่อน เครือข่ายก่อน หรือเครือข่ายเสมอพร้อมเขียนแคช
interface UserRepository {
fun getUsers(): Flow<Result<List<User>>>
}
interface UserRemoteDataSource {
suspend fun fetchUsers(): List<User>
}
interface UserLocalDataSource {
fun getCachedUsers(): Flow<List<User>>
suspend fun saveUsers(users: List<User>)
}
class UserRepositoryImpl(
private val remote: UserRemoteDataSource,
private val local: UserLocalDataSource
) : UserRepository {
override fun getUsers(): Flow<Result<List<User>>> = flow {
emit(Result.Loading)
local.getCachedUsers().collect { cached ->
if (cached.isNotEmpty()) {
emit(Result.Success(cached))
}
}
try {
val users = remote.fetchUsers()
local.saveUsers(users)
emit(Result.Success(users))
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}
ตัวห่อ Result ในตัวอย่างข้างต้นเป็นมาตรฐานสำหรับ Android: คลาส sealed Result แจ้ง ViewModel เกี่ยวกับสถานะการโหลด (กำลังโหลด สำเร็จ ข้อผิดพลาด) ViewModel สมัครสมาชิกผ่าน collect และอัปเดต StateFlow หรือ LiveData Repository กับ Flow แจ้ง UI โดยอัตโนมัติเกี่ยวกับการเปลี่ยนแปลงในฐานข้อมูล — นี่คือข้อแตกต่างหลักจากคำขอครั้งเดียวที่ UI ไม่ทราบเกี่ยวกับการเปลี่ยนแปลงหากไม่มีการรีเฟรชด้วยตนเอง
DataSource — คลาสที่รับผิดชอบการทำงานกับแหล่งข้อมูลเฉพาะ RemoteDataSource ใช้ HTTP ไคลเอ็นต์ (URLSession, Retrofit, Ktor) เพื่อรับข้อมูลจาก API LocalDataSource ทำงานกับพื้นที่จัดเก็บในเครื่อง (CoreData, Realm, Room, UserDefaults, DataStore) แต่ละ DataSource มีความรับผิดชอบที่แคบ: RemoteDataSource รู้เฉพาะรูปแบบคำขอ API, LocalDataSource — รู้เฉพาะสคีมาฐานข้อมูล Repository รวมพวกมันเข้าด้วยกัน โดยใช้กลยุทธ์การแคช
| DataSource | แพลตฟอร์ม iOS | แพลตฟอร์ม Android | แหล่งที่มา |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | REST / GraphQL API |
| Local (DB) | CoreData, SwiftData | Room, SQLDelight | SQLite บนอุปกรณ์ |
| Local (แคช) | NSCache, UserDefaults | DataStore, EncryptedSP | ในหน่วยความจำ / ดิสก์ |
| การตั้งค่า | UserDefaults, Keychain | SharedPreferences, EncryptedSP | การตั้งค่า, โทเคน |
กลยุทธ์การแคช ใน Repository: Cache-First (แคชก่อน แล้วโหลดพื้นหลัง), Network-Only (เครือข่ายเท่านั้น สำหรับหน้าจอชำระเงิน), Network-First-With-Cache-Backup (เครือข่ายก่อน สำรองแคชเมื่อเกิดข้อผิดพลาด) การเลือกกลยุทธ์ขึ้นอยู่กับสถานการณ์: รายชื่อประเทศสามารถแคชไว้นาน อัตราแลกเปลี่ยนสกุลเงิน — 15 นาที ยอดคงเหลือในกระเป๋าเงิน — จากเครือข่ายเท่านั้น Repository ใช้กลยุทธ์และเปลี่ยนแปลงโดยไม่ต้องแก้ไข ViewModel หรือ UI
Repository และ Service เป็นรูปแบบที่แตกต่างกันซึ่งมีฟังก์ชันทับซ้อน Repository รับผิดชอบการเข้าถึงข้อมูลและการแคช ส่งคืนโมเดลข้อมูล Service (หรือ Interactor, Use Case) มีตรรกะทางธุรกิจ: การตรวจสอบความถูกต้อง การแปลงข้อมูล การประสานงานการเรียก Repository หลายครั้ง Service สามารถรวม UserRepository, OrderRepository และ NotificationRepository เพื่อดำเนินการตามคำสั่งซื้อ Repository ไม่มีตรรกะทางธุรกิจ — มีเพียง CRUD และการแคช
เมื่อใดควรเลือก Repository — การนำทางข้อมูลกับหลายแหล่ง (API + DB + แคช), สถาปัตยกรรมแบบออฟไลน์ก่อน, ความจำเป็นในการแคชและการสลับแหล่งที่โปร่งใส Repository เป็นสิ่งบังคับใน Clean Architecture และ Google แนะนำสำหรับแอปพลิเคชัน Android ในสถาปัตยกรรม VIPER บน iOS บทบาทของ Repository ดำเนินการโดยชั้น Interactor ซึ่งโต้ตอบกับ Manager หรือ Service สำหรับการเข้าถึงข้อมูล
เมื่อใด Service เพียงพอ — แอปพลิเคชันง่าย ๆ ที่มีแหล่งข้อมูลเดียว หน้าจอแบบอ่านอย่างเดียวโดยไม่มีการเขียน โปรเจกต์ที่ไม่มีโหมดออฟไลน์ ในกรณีเช่นนี้ DataSource ถูกใช้โดยตรงโดย ViewModel หรือ Presenter และ Repository กลายเป็นชั้นที่เกินความจำเป็น อย่างไรก็ตาม การเพิ่ม Repository ในระยะแรกไม่ต้องใช้ความพยายามมากและทำให้การเพิ่มแคชและการทดสอบในอนาคตง่ายขึ้น
คำถามที่พบบ่อย
DataSource คือคลาสที่ทำงานกับแหล่งเดียว (API, DB, แคช) Repository คือคลาสที่จัดการหลาย DataSource และจัดเตรียมอินเทอร์เฟซแบบรวม Repository ตัดสินใจว่าจะใช้ DataSource ใดและประสานงานการแคช DataSource ไม่ทราบเกี่ยวกับการมีอยู่ของแหล่งอื่น ๆ Repository ไม่ทราบรายละเอียดการนำไปใช้ของแต่ละแหล่ง
ใช่ Repository มีประโยชน์ใน SwiftUI สำหรับการแยกข้อมูลออกจาก View ViewModel สมัครสมาชิก Publisher จาก Repository และ Repository จัดการการแคชและการซิงโครไนซ์ ในแอปพลิเคชันง่าย ๆ สามารถใช้ URLSession โดยตรงใน ViewModel แต่สำหรับความสามารถในการทดสอบและการปรับขนาด Repository เป็นที่นิยมมากกว่า Apple ไม่บังคับใช้รูปแบบนี้ แต่มันเข้ากันได้กับ SwiftData และ Network.framework
DataSources จะถูกแทนที่ด้วยวัตถุจำลองผ่าน Dependency Injection การทดสอบสร้าง RemoteDataSource จำลอง (ส่งคืน JSON ที่กำหนดไว้ล่วงหน้า) และ LocalDataSource จำลอง (ตรวจสอบว่าข้อมูลถูกบันทึก) Repository ถูกทดสอบแบบแยกส่วน: ตรวจสอบกลยุทธ์การแคช การจัดการข้อผิดพลาด และลำดับการเรียกที่ถูกต้อง สำหรับการทดสอบแบบบูรณาการ ใช้ TestDispatcher (Kotlin) หรือ MainActor.run (Swift)
เป็นไปได้แต่ไม่แนะนำ หากไม่มีโปรโตคอล จะไม่สามารถเปลี่ยนการนำไปใช้ในการทดสอบและตัวอย่างแสดงได้ ใน Kotlin อินเทอร์เฟซ Repository อนุญาตให้เปลี่ยนการนำไปใช้ผ่าน DI (Dagger, Hilt, Koin) ใน Swift โปรโตคอล Repository เป็นสิ่งจำเป็นสำหรับการทดสอบโค้ด async-await และ Combine ข้อยกเว้นคือโปรเจกต์ง่าย ๆ ที่มีแหล่งข้อมูลเดียวซึ่ง Repository ไม่มีตรรกะการแคช
Offline-first เป็นกลยุทธ์ที่แอปพลิเคชันทำงานโดยไม่มีอินเทอร์เน็ตโดยใช้ข้อมูลในเครื่อง Repository มีบทบาทสำคัญ: ขั้นแรกส่งคืนข้อมูลจาก DataSource ในเครื่อง จากนั้นซิงโครไนซ์กับเซิร์ฟเวอร์ในพื้นหลัง ผู้ใช้เห็นข้อมูลทันที และ Repository อัปเดตข้อมูลหลังจากโหลดจากเครือข่าย Room กับ Flow ให้การอัปเดต UI แบบตอบสนองเมื่อข้อมูลเปลี่ยนแปลงในฐานข้อมูลภายในเครื่อง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม