Clean Architecture — معماری چندلایهای که توسط رابرت مارتین (Uncle Bob) در سال ۲۰۱۲ ارائه شد، برنامه را به لایههای مستقل تقسیم میکند: Domain (Entities، Use Cases)، Data (Repositories، DataSources) و Presentation (ViewModels، Views). اصل اصلی — Dependency Rule: وابستگیها به سمت داخل هدایت میشوند، لایههای بیرونی به لایههای داخلی وابسته هستند، نه برعکس. Clean Architecture در توسعه موبایل برای پروژههایی با پیچیدگی بالای منطق تجاری استفاده میشود. بیشتر — در کتاب The Clean Architecture.
نکات اصلی
Clean Architecture — الگوی معماری فرمولهشده توسط رابرت مارتین (Uncle Bob) در سال ۲۰۱۲. ایده اصلی — تقسیم برنامه به لایهها با قانون سخت وابستگی: کد داخل یک لایه از کد بیرون اطلاعی ندارد. لایههای بیرونی (UI، فریمورکها، پایگاه داده) — جزئیات پیادهسازی. لایههای داخلی (منطق تجاری، قوانین سازمانی) — جوهر برنامه.
لایههای Clean Architecture در توسعه موبایل: ۱) Domain — Entities (اشیاء تجاری) و Use Cases (موارد استفاده)؛ ۲) Data — RepositoryImpl (پیادهسازی مخازن)، DataSources (شبکه، پایگاه داده، کش)؛ ۳) Presentation — ViewModels، Views (Compose/SwiftUI). Domain — درونیترین لایه بدون وابستگی. Data به Domain وابسته است (اینترفیسهای مخزن را پیادهسازی میکند). Presentation به Domain وابسته است (Use Cases را فراخوانی میکند، در نتیجه مشترک میشود).
| لایه | محتويات | وابستگیها |
|---|---|---|
| Domain | Entities، Use Cases، Repository Interfaces | ندارد (Kotlin/Swift خالص) |
| Data | RepositoryImpl، DataSources (API، DB، Cache) | Domain، Retrofit، Room، Ktor |
| Presentation | ViewModels، Views، Composables | Domain، Jetpack، SwiftUI |
Dependency Rule — تنها قانون سخت Clean Architecture. کد منبع فقط میتواند به لایه داخل خود یا لایه پایینتر (نزدیکتر به مرکز) ارجاع دهد. Presentation Domain را import میکند. Domain Data یا Presentation را import نمیکند. این از طریق اصل معکوسسازی وابستگی (Dependency Inversion Principle) به دست میآید: Domain اینترفیس Repository را تعریف میکند، Data آن را پیادهسازی میکند. Presentation به انتزاع UseCase وابسته است، نه به مخزن مشخص.
Domain — پایدارترین لایه برنامه. Entities — اشیاء تجاری مستقل از فریمورکها: User، Product، Order. Use Cases — کلاسهایی با یک متد invoke (یا operator fun invoke در Kotlin) که یک سناریو را پیادهسازی میکنند: GetUserUseCase، PlaceOrderUseCase، CalculateTotalUseCase. Repository Interfaces — انتزاعات دسترسی به داده که در Domain تعریف و در Data پیادهسازی میشوند. Domain شامل Android SDK، iOS UIKit، Retrofit، Room نیست — فقط Kotlin یا Swift خالص.
// Entity — شیء تجاری (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — انتزاع داده (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — یک سناریو (Domain)
final class GetUserUseCase {
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func execute(id: Int) async throws -> User {
return try await repository.getUser(id: id)
}
}
Use Case — «کلاس با یک متد» — جزم نیست، بلکه توصیه عملی است. وقتی Use Case پیچیدهتر میشود (اعتبارسنجی + لاگگیری + فراخوانی مخزن)، متدهایش بر اساس معنا گروهبندی میشوند: UserUseCase.getUser، UserUseCase.searchUsers، UserUseCase.deleteUser. نکته مهم — Use Case نباید بداند دادهها از کجا میآیند (شبکه، پایگاه داده، کش) و چه کسی آنها را نمایش میدهد (Compose، SwiftUI). در IT Sectr ما برای هر عملیاتی که قانون تجاری، بررسی یا ترکیب داده از دو منبع دارد، Use Case جداگانهای اختصاص میدهیم.
خلوص Domain از طریق نگاشت DTO در مرز لایهها به دست میآید. لایه Data مدلهای JSON (DTO) را دریافت میکند، آنها را به Entity Domain نگاشت میکند. Presentation Entity Domain را دریافت میکند، به ViewModel (DisplayItem) نگاشت میکند. Entity Domain هرگز حاوی حاشیهنویسیهای Retrofit، Room، Codable نیست — این تضمین میکند که لایه هنگام تغییر پایگاه داده از Room به Realm یا جایگزینی Retrofit با Ktor نیازی به تغییر نداشته باشد.
Data Layer — پیادهسازی اینترفیسهای تعریفشده در Domain. شامل RepositoryImpl (کلاسهایی که UserRepository را پیادهسازی میکنند) و DataSources (RemoteDataSource — API، LocalDataSource — پایگاه داده، CacheDataSource — SharedPreferences/NSUserDefaults). لایه Data به Domain (اینترفیسهای مخزن و Entities را import میکند) و به فریمورکها (Retrofit، Room، Ktor، CoreData) وابسته است. RepositoryImpl منبع داده را از Domain پنهان میکند — Use Case نمیداند دادهها از شبکه آمده یا کش.
// DTO — مدل برای شبکه (Data)
data class UserDto(
@SerializedName("id") val id: Int,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("email") val email: String
)
// RepositoryImpl — پیادهسازی (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// تلاش برای دریافت از کش
localDataSource.getUser(id)?.let { return it.toDomain() }
// اگر نیست — از شبکه بارگیری میکنیم
val dto = remoteDataSource.fetchUser(id)
val user = dto.toDomain()
localDataSource.saveUser(user)
return user
}
override suspend fun getUsers(): List<User> {
return remoteDataSource.fetchAllUsers().map { it.toDomain() }
}
}
// Mapper — تبدیل DTO ↔ Domain
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
استراتژی کش کردن در Data Layer: RepositoryImpl ابتدا ذخیره محلی را بررسی میکند، در صورت نبود داده — از شبکه بارگیری کرده و به صورت محلی ذخیره میکند. اگر شبکه در دسترس نباشد — دادههای قدیمی را با علامت isStale برمیگرداند. Use Case در Domain از استراتژی اطلاعی ندارد — User را از طریق Repository.getUser(id) دریافت میکند. تغییر استراتژی (مثلاً ابطال کش هر ۱۵ دقیقه) بر Domain و Presentation تأثیر نمیگذارد.
ماژولار بودن در Android — Kotlin Multiplatform اجازه میدهد Domain به یک ماژول KMP جداگانه بدون وابستگی به Android SDK منتقل شود. Data — ماژول جداگانه با وابستگی به Domain. Presentation — ماژول Android با وابستگی به Domain. وابستگیهای Gradle: domain (pure Kotlin)، data (domain + Retrofit + Room)، app (domain + presentation + Hilt). چنین ماژولاری برای پروژههای بزرگ الزامی است — CI Domain را جداگانه میسازد، تستهای واحد Domain به شبیهساز Android نیاز ندارند.
Presentation Layer — بیرونیترین لایه Clean Architecture. شامل ViewModels (Android) / ObservableObject (iOS) و Views (Compose/SwiftUI). ViewModel Use Case را فراخوانی میکند، نتیجه را دریافت و به حالت UI (State) تبدیل میکند. View در State مشترک شده و نمایش میدهد. Presentation به Domain وابسته است — Use Cases و Entities را import میکند. Presentation Data Layer را import نمیکند — دادهها از طریق Use Case میآیند که در داخل از Repository استفاده میکند.
ViewModel در Clean Architecture شامل منطق تجاری نیست — Use Case را فراخوانی میکند. اگر Use Case User را برگرداند، ViewModel آن را به UserDisplayItem (name، emailFormatted، avatarUrl) — یک مدل صرفاً نمایشی — تبدیل میکند. Use Case از DisplayItem اطلاعی ندارد — Entity برمیگرداند. این تفکیک اجازه میدهد Use Case بدون UI و ViewModel بدون UseCase (از طریق mock) تست شوند. در IT Sectr ما به طور دقیق رعایت میکنیم: Use Case — منطق تجاری، ViewModel — فقط نمایش، View — فقط ارائه.
// Use Case (Domain) — منطق تجاری خالص
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — فقط نمایش
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
val state: StateFlow<UserScreenState> = _state.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
_state.value = UserScreenState.Loading
val user = getUserUseCase(id)
val displayItem = UserDisplayItem(
name = user.name,
email = user.email,
initials = user.name.split(" ").joinToString("") { it.first().toString() }
)
_state.value = UserScreenState.Success(displayItem)
}
}
}
data class UserDisplayItem(
val name: String,
val email: String,
val initials: String
)
sealed interface UserScreenState {
data object Loading : UserScreenState
data class Success(val displayItem: UserDisplayItem) : UserScreenState
data class Error(val message: String) : UserScreenState
}
Navigation در لایه Presentation — بخشی از حلقه بیرونی است. Clean Architecture مکانیسم ناوبری را تجویز نمیکند — میتواند NavController (Compose)، NavigationStack (SwiftUI)، Coordinator (UIKit) یا Router (VIPER) باشد. مهم: تصمیم ناوبری توسط Presentation گرفته میشود، اما ناوبری نباید به Use Case نفوذ کند. Use Case نتیجه را برمیگرداند، ViewModel تصمیم میگیرد به کدام صفحه برود. در Clean Architecture ناوبری جزییاتی است که بدون تغییر Domain قابل تعویض است.
Clean Architecture در Android از طریق ماژولهای Gradle پیادهسازی میشود: domain (pure Kotlin)، data (domain + Retrofit + Room)، presentation (domain + Compose). ساختار پوشهها: domain/user/User.kt، GetUserUseCase.kt، UserRepository.kt؛ data/remote/UserRemoteDataSource.kt، local/UserDao.kt، repository/UserRepositoryImpl.kt؛ presentation/ui/user/UserViewModel.kt، UserScreen.kt. DI (Hilt) لایهها را متصل میکند: UserRepositoryImpl به اینترفیس UserRepository در ماژول domain متصل میشود.
Clean Architecture در iOS از SPM یا گروههای Xcode بدون ماژولهای جداگانه (به دلیل محدودیتهای Xcode) استفاده میکند. Domain — پوشهای با فایلهایی که UIKit یا SwiftUI را import نمیکنند. Data — پوشهای با APIClient، CoreDataStack، RepositoryImpl. Presentation — پوشهای با ViewModels و SwiftUI Views. DI از طریق سازنده یا اسمبلی در App. فراخوانی اصلی — async/await از طریق UseCase.execute() با بررسی MainActor برای بهروزرسانیهای UI.
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
private let apiClient: APIClient
func fetchUser(id: Int) async throws -> UserDTO {
return try await apiClient.get("/users/\(id)")
}
}
// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
func getUser(id: Int) async throws -> User {
if let cached = try await local.getUser(id) {
return cached
}
let dto = try await remote.fetchUser(id)
let user = dto.toDomain()
try await local.saveUser(user)
return user
}
}
// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserScreenState = .loading
private let getUserUseCase: GetUserUseCase
func loadUser(id: Int) {
Task {
state = .loading
if let user = try? await getUserUseCase.execute(id: id) {
state = .success(user)
} else {
state = .error("Failed to load")
}
}
}
}
Clean Architecture در پروژههای IT Sectr — استاندارد ما برای پروژههای از ۳۰ روز. ما از معماری سهلایه با Kotlin Multiplatform برای Android/iOS از سال ۲۰۲۲ استفاده میکنیم. Domain — ماژول مشترک KMP، Data — ماژولهای پلتفرمی (Retrofit در Android، URLSession در iOS)، Presentation — UI بومی. این ۶۰–۸۰٪ کد مشترک منطق تجاری بین iOS و Android میدهد و زمان توسعه را ۳۰–۴۰٪ در مقایسه با دو پیادهسازی جداگانه کاهش میدهد.
سوالات متداول
حداقل سه: Domain، Data، Presentation. برای پروژههای بزرگ Framework (وابستگیهای Android SDK/iOS UIKit) و Device (GPS، دوربین، سنسورها) اضافه میکنند. تعداد لایهها — قانون سخت نیست، بحث راحتی است. مهم رعایت Dependency Rule است: وابستگیها به سمت داخل، به Domain. میتوان با سه لایه شروع کرد و با رشد پروژه لایهها را اضافه کرد.
بله — ۳۰–۵۰٪ در مقایسه با MVVM به دلیل جدا کردن اینترفیسهای مخازن، Use Cases و مپرها. برای برنامه ساده CRUD این اضافی است. Clean Architecture برای پروژههایی با منطق تجاری پیچیده توجیهپذیر است، جایی که قابلیت تست و ایزولاسیون لایهها مهمتر از سرعت توسعه است. برای MVP یا نمونه اولیه از MVVM استفاده کنید — Clean Architecture راهاندازی را کند میکند.
بله، این یک روش رایج است. Use Cases در Domain باقی میمانند و Presentation از چرخه MVI (Intent → Reducer → State) استفاده میکند. Data Layer — یکسان، Domain — یکسان. MVI در Presentation حالت قابل پیشبینی صفحه را میدهد، Clean Architecture — ایزولاسیون منطق تجاری. این ترکیب در پروژههای بزرگ با دهها توسعهدهنده استفاده میشود.
Use Case زمانی نیاز است که عملیات شامل قانون تجاری باشد: اعتبارسنجی، ترکیب داده از دو منبع، محاسبه، لاگگیری، بررسی مجوزهای دسترسی. درخواست ساده getUser(id) بدون منطق اضافی میتواند Repository را مستقیماً از ViewModel فراخوانی کند. اما برای یکنواختی معماری، بسیاری از تیمها برای هر متد عمومی Repository Use Case ایجاد میکنند — این ۵–۱۰٪ کد اضافه میکند اما خواندن را سادهتر میکند.
Domain: تستهای واحد Use Cases با mock Repository — Kotlin/Swift خالص بدون Android SDK. Data: تستهای یکپارچهسازی RepositoryImpl با mock/fake DataSource. Presentation: تستهای ViewModel با mock UseCase. به لطف Dependency Rule هر لایه به صورت ایزوله تست میشود. در IT Sectr پوشش Domain به ۹۵٪، Data — ۷۰–۸۰٪، Presentation — ۶۰–۷۰٪ میرسد.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید