Repository Pattern — một mẫu thêm lớp trừu tượng giữa logic nghiệp vụ và các nguồn dữ liệu. Thay vì gọi trực tiếp API, cơ sở dữ liệu hoặc bộ nhớ đệm, Repository cung cấp một giao diện thống nhất để lấy và lưu trữ dữ liệu. Điều này đơn giản hóa việc kiểm thử và chuyển đổi giữa các nguồn. Đọc thêm trong tài liệu Android Data Layer.
Những điểm chính
Repository Pattern là một mẫu cấu trúc cô lập logic nghiệp vụ khỏi truy cập trực tiếp vào các nguồn dữ liệu. Thay vì Activity, UIViewController hoặc ViewModel gọi trực tiếp Retrofit, URLSession, Room hoặc CoreData, chúng giao tiếp với Repository. Repository quyết định lấy dữ liệu từ đâu — từ mạng, cơ sở dữ liệu hay bộ nhớ đệm — và trả về kết quả ở định dạng thống nhất. Điều này thực hiện nguyên tắc trách nhiệm đơn lẻ — UI không biết dữ liệu được lấy như thế nào hay từ đâu.
Các thành phần của Repository bao gồm một giao diện (protocol), một triển khai và một hoặc nhiều DataSource. DataSource là một lớp làm việc với một nguồn duy nhất: RemoteDataSource gọi API qua HTTP client, LocalDataSource đọc và ghi vào cơ sở dữ liệu. Repository nhận DataSources qua hàm khởi tạo (Dependency Injection) và quyết định sử dụng nguồn nào. Ví dụ, khi yêu cầu danh sách người dùng, Repository đầu tiên kiểm tra bộ nhớ đệm, sau đó cơ sở dữ liệu, sau đó mạng.
Lợi ích của Repository Pattern: sự cô lập thay đổi nguồn dữ liệu (thay đổi API, di chuyển DB) không ảnh hưởng đến lớp UI; kiểm thử đơn vị qua việc thay thế Repository hoặc DataSource; lưu trữ đệm trong suốt với UI; chuyển đổi giữa chế độ trực tuyến và ngoại tuyến mà không thay đổi logic màn hình. Cộng đồng Android khuyến nghị Repository như một lớp bắt buộc trong Clean Architecture.
Triển khai iOS của Repository được xây dựng trên các giao thức Swift. Giao thức Repository khai báo các phương thức để lấy và lưu trữ dữ liệu. Triển khai thực tế được tiêm qua hàm khởi tạo — điều này cho phép thay thế triển khai trong kiểm thử và bản xem trước SwiftUI. DataSources cũng được khai báo dưới dạng giao thức: Protocol RemoteDataSource, Protocol LocalDataSource. ViewModel hoặc Interactor không biết về một triển khai cụ thể — chỉ biết về giao thức 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 trong iOS cho Repository thường được cấu hình qua factory hoặc DI container (Swinject, Factory). Trong kiểm thử, giao thức UserRepository được thay thế bằng triển khai giả trả về dữ liệu được xác định trước. Async-await làm cho mã đồng bộ và dễ đọc mà không cần closures và delegates. Để phản ứng với Combine, các phương thức Repository trả về AnyPublisher thay vì async throws.
Triển khai Android của Repository sử dụng rộng rãi Kotlin Coroutines và Flow cho các hoạt động bất đồng bộ. Google khuyến nghị Repository trong hướng dẫn kiến trúc Android chính thức (Android Architecture Components). Repository chấp nhận RemoteDataSource (Retrofit) và LocalDataSource (Room) qua hàm khởi tạo, và ViewModel đăng ký Flow từ Repository. Repository quản lý chiến lược dữ liệu: bộ nhớ đệm trước, mạng trước, hoặc luôn mạng với ghi vào bộ nhớ đệm.
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))
}
}
}
Wrapper Result trong ví dụ trên là tiêu chuẩn cho Android: một lớp sealed Result thông báo cho ViewModel về trạng thái tải (Đang tải, Thành công, Lỗi). ViewModel đăng ký qua collect và cập nhật StateFlow hoặc LiveData. Repository với Flow tự động thông báo cho UI về các thay đổi trong cơ sở dữ liệu — đây là điểm khác biệt chính so với các yêu cầu một lần nơi UI không biết về thay đổi nếu không làm mới thủ công.
DataSource — các lớp chịu trách nhiệm làm việc với một nguồn dữ liệu cụ thể. RemoteDataSource sử dụng HTTP client (URLSession, Retrofit, Ktor) để lấy dữ liệu từ API. LocalDataSource làm việc với bộ nhớ cục bộ (CoreData, Realm, Room, UserDefaults, DataStore). Mỗi DataSource có trách nhiệm hẹp: RemoteDataSource chỉ biết về định dạng yêu cầu API, LocalDataSource — về lược đồ cơ sở dữ liệu. Repository kết hợp chúng, triển khai chiến lược lưu trữ đệm.
| DataSource | Nền tảng iOS | Nền tảng Android | Nguồn |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | API REST / GraphQL |
| Local (DB) | CoreData, SwiftData | Room, SQLDelight | SQLite trên thiết bị |
| Local (bộ nhớ đệm) | NSCache, UserDefaults | DataStore, EncryptedSP | Trong bộ nhớ / đĩa |
| Tùy chọn | UserDefaults, Keychain | SharedPreferences, EncryptedSP | Cài đặt, token |
Chiến lược lưu trữ đệm trong Repository: Cache-First (bộ nhớ đệm trước, sau đó tải nền), Network-Only (chỉ mạng, cho màn hình thanh toán), Network-First-With-Cache-Backup (mạng trước, dự phòng bộ nhớ đệm khi lỗi). Lựa chọn chiến lược phụ thuộc vào kịch bản: danh sách quốc gia có thể được lưu đệm lâu, tỷ giá hối đoái — trong 15 phút, số dư ví — chỉ từ mạng. Repository triển khai chiến lược và thay đổi nó mà không sửa đổi ViewModel hoặc UI.
Repository và Service là các mẫu khác nhau với chức năng chồng chéo. Repository chịu trách nhiệm truy cập dữ liệu và lưu trữ đệm, trả về các mô hình dữ liệu. Service (hoặc Interactor, Use Case) chứa logic nghiệp vụ: xác thực, chuyển đổi dữ liệu, điều phối các cuộc gọi đến nhiều Repository. Service có thể kết hợp UserRepository, OrderRepository và NotificationRepository để xử lý đơn hàng. Repository không chứa logic nghiệp vụ — chỉ CRUD và lưu trữ đệm.
Khi nào chọn Repository — điều hướng dữ liệu với nhiều nguồn (API + DB + bộ nhớ đệm), kiến trúc offline-first, nhu cầu lưu trữ đệm và chuyển đổi nguồn trong suốt. Repository là bắt buộc trong Clean Architecture và được Google khuyến nghị cho các ứng dụng Android. Trong kiến trúc VIPER trên iOS, vai trò của Repository được thực hiện bởi lớp Interactor, tương tác với Manager hoặc Service để truy cập dữ liệu.
Khi nào Service đủ — ứng dụng đơn giản với một nguồn dữ liệu duy nhất, màn hình chỉ đọc không ghi, dự án không có chế độ ngoại tuyến. Trong những trường hợp này, DataSource được sử dụng trực tiếp bởi ViewModel hoặc Presenter, và Repository trở thành một lớp dư thừa. Tuy nhiên, thêm Repository ở giai đoạn đầu không đòi hỏi nhiều công sức và đơn giản hóa việc thêm bộ nhớ đệm và kiểm thử trong tương lai.
Câu hỏi thường gặp
DataSource là một lớp làm việc với một nguồn duy nhất (API, DB, bộ nhớ đệm). Repository là một lớp quản lý nhiều DataSource và cung cấp một giao diện thống nhất. Repository quyết định sử dụng DataSource nào và điều phối lưu trữ đệm. DataSource không biết về sự tồn tại của các nguồn khác; Repository không biết chi tiết triển khai của từng nguồn.
Có, Repository hữu ích trong SwiftUI để tách dữ liệu khỏi View. ViewModel đăng ký Publisher từ Repository, và Repository quản lý lưu trữ đệm và đồng bộ hóa. Trong ứng dụng đơn giản, có thể sử dụng URLSession trực tiếp trong ViewModel, nhưng để kiểm thử và mở rộng, Repository được ưu tiên. Apple không áp đặt mẫu này, nhưng nó tương thích với SwiftData và Network.framework.
DataSource được thay thế bằng các đối tượng giả qua Dependency Injection. Kiểm thử tạo RemoteDataSource giả (trả về JSON được xác định trước) và LocalDataSource giả (xác minh dữ liệu được lưu). Repository được kiểm thử độc lập: chiến lược lưu trữ đệm, xử lý lỗi và thứ tự gọi đúng được xác minh. Cho kiểm thử tích hợp, sử dụng TestDispatcher (Kotlin) hoặc MainActor.run (Swift).
Có thể nhưng không được khuyến nghị. Không có giao thức, không thể thay thế triển khai trong kiểm thử và bản xem trước. Trong Kotlin, giao diện Repository cho phép thay thế triển khai qua DI (Dagger, Hilt, Koin). Trong Swift, giao thức Repository là bắt buộc để kiểm thử mã async-await và Combine. Ngoại lệ là các dự án đơn giản với một nguồn dữ liệu duy nhất nơi Repository không có logic lưu trữ đệm.
Offline-first là một chiến lược nơi ứng dụng hoạt động không có internet sử dụng dữ liệu cục bộ. Repository đóng vai trò chính: đầu tiên trả về dữ liệu từ DataSource cục bộ, sau đó đồng bộ hóa với máy chủ trong nền. Người dùng thấy dữ liệu ngay lập tức, và Repository cập nhật dữ liệu sau khi tải từ mạng. Room với Flow cung cấp cập nhật UI phản ứng khi dữ liệu thay đổi trong cơ sở dữ liệu cục bộ.
Tổng kết
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.
Đọc thêm