Repository Pattern — الگویی که لایهای از انتزاع بین منطق کسبوکار و منابع داده اضافه میکند. به جای فراخوانی مستقیم API، پایگاه داده یا حافظه پنهان، Repository یک رابط یکپارچه برای دریافت و ذخیره داده فراهم میکند. این کار تستپذیری و جابجایی بین منابع را ساده میکند. جزئیات بیشتر در مستندات Android Data Layer.
نکات اصلی
Repository Pattern — الگوی ساختاری است که منطق کسبوکار را از دسترسی مستقیم به منابع داده جدا میکند. به جای اینکه Activity، UIViewController یا ViewModel مستقیماً Retrofit، URLSession، Room یا CoreData را فراخوانی کنند، به Repository مراجعه میکنند. Repository تصمیم میگیرد داده را از کجا بیاورد: از شبکه، پایگاه داده یا حافظه پنهان، و نتیجه را در قالبی یکپارچه بازمیگرداند. این تحقق اصل مسئولیت واحد است — UI نمیداند داده چگونه و از کجا دریافت شده است.
اجزای Repository شامل رابط (protocol)، پیادهسازی و یک یا چند DataSource است. DataSource — کلاسی که با یک منبع کار میکند: RemoteDataSource API را از طریق HTTP-کلاینت فراخوانی میکند، LocalDataSource در پایگاه داده میخواند و مینویسد. Repository DataSourceها را از طریق سازنده (Dependency Injection) دریافت میکند و انتخاب میکند به کدام منبع مراجعه کند. برای مثال، هنگام درخواست لیست کاربران، Repository ابتدا حافظه پنهان، سپس پایگاه داده، و بعد شبکه را بررسی میکند.
مزایای Repository Pattern: جداسازی تغییرات منابع داده (تغییر API، مهاجرت DB) بر لایه UI تأثیر نمیگذارد؛ تست واحد از طریق جایگزینی Repository یا DataSource؛ ذخیره در حافظه پنهان به صورت شفاف برای UI؛ جابجایی بین حالت آنلاین و آفلاین بدون تغییر منطق صفحه. جامعه Android Repository را به عنوان یک لایه اجباری در Clean Architecture توصیه میکند.
پیادهسازی iOS Repository بر اساس پروتکلهای Swift ساخته میشود. پروتکل Repository متدهای دریافت و ذخیره داده را اعلام میکند. پیادهسازی واقعی از طریق مقداردهنده اولیه تزریق میشود — این امکان جایگزینی پیادهسازی در تستها و preview SwiftUI را فراهم میکند. DataSourceها نیز با پروتکلها اعلام میشوند: 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 با پیادهسازی mock که دادههای از پیش تعیینشده برمیگرداند جایگزین میشود. Async-await کد را بدون بلاکهای تکمیل و دلیگیتها همزمان و خواناتر میکند. برای واکنشگرایی Combine، متدهای Repository به جای async throws AnyPublisher برمیگردانند.
پیادهسازی 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))
}
}
}
Wrapper Result در مثال بالا برای Android استاندارد است: sealed class Result وضعیت بارگذاری (Loading, Success, Error) را به 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 (ابتدا شبکه، در صورت خطا — حافظه پنهان). انتخاب استراتژی به سناریو بستگی دارد: لیست کشورها را میتوان طولانی مدت ذخیره کرد، نرخ ارز — ۱۵ دقیقه، موجودی کیف پول — فقط از شبکه. Repository استراتژی را پیادهسازی میکند و بدون تغییر ViewModel یا UI آن را تغییر میدهد.
Repository و Service — الگوهای متفاوتی با عملکردهای همپوشان. Repository مسئول دسترسی به داده و ذخیره آن در حافظه پنهان است و مدلهای داده را برمیگرداند. Service (یا Interactor, Use Case) شامل منطق کسبوکار است: اعتبارسنجی، تبدیل داده، هماهنگی فراخوانی چندین Repository. Service میتواند UserRepository، OrderRepository و NotificationRepository را برای ثبت سفارش ترکیب کند. Repository حاوی منطق کسبوکار نیست — فقط CRUD و ذخیره در حافظه پنهان.
چه زمانی Repository را انتخاب کنیم — پیمایش داده با منابع متعدد (API + DB + حافظه پنهان)، معماری offline-first، نیاز به ذخیره در حافظه پنهان و جابجایی شفاف بین منابع. 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 سازگار است.
DataSourceها از طریق Dependency Injection با اشیاء mock جایگزین میشوند. تست یک RemoteDataSource mock (که JSON از پیش تعیینشده برمیگرداند) و یک LocalDataSource mock (که بررسی میکند داده ذخیره شده است) ایجاد میکند. Repository به صورت مجزا تست میشود: استراتژی ذخیره در حافظه پنهان، مدیریت خطا و ترتیب صحیح فراخوانیها بررسی میشود. برای تستهای یکپارچهسازی از TestDispatcher (Kotlin) یا MainActor.run (Swift) استفاده میشود.
میتوان، اما توصیه نمیشود. بدون پروتکل نمیتوان پیادهسازی را در تستها و preview جایگزین کرد. در Kotlin، رابط Repository امکان جایگزینی پیادهسازی را از طریق DI (Dagger, Hilt, Koin) فراهم میکند. در Swift، پروتکل Repository برای تست کد async-await و Combine ضروری است. استثنا — پروژههای ساده با یک منبع داده که Repository حاوی منطق ذخیره در حافظه پنهان نیست.
Offline-first — استراتژی که در آن برنامه بدون اینترنت با استفاده از دادههای محلی کار میکند. Repository نقش کلیدی ایفا میکند: ابتدا داده را از DataSource محلی برمیگرداند، سپس در پسزمینه با سرور همگامسازی میکند. کاربر داده را بلافاصله میبیند و Repository پس از بارگیری از شبکه آن را بهروزرسانی میکند. Room با Flow بهروزرسانی واکنشگرای UI را هنگام تغییر داده در پایگاه داده محلی فراهم میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.