Clean Architecture — রবার্ট মার্টিন (আঙ্কেল বব) 2012 সালে প্রস্তাবিত একটি মাল্টি-লেয়ার আর্কিটেকচার, যা অ্যাপ্লিকেশনকে স্বাধীন স্তরে বিভক্ত করে: Domain (Entities, Use Cases), Data (Repositories, DataSources) এবং Presentation (ViewModels, Views)। মূল নীতি হল ডিপেন্ডেন্সি রুল: নির্ভরতাগুলি ভিতরের দিকে নির্দেশ করে, বাইরের স্তরগুলি ভিতরের স্তরের উপর নির্ভর করে, কিন্তু তার উল্টোটা নয়। Clean Architecture মোবাইল ডেভেলপমেন্টে উচ্চ ব্যবসায়িক লজিক জটিলতা সম্পন্ন প্রকল্পের জন্য ব্যবহৃত হয়। আরও বিস্তারিত জানতে বইটি দেখুন The Clean Architecture।
মূল পয়েন্ট
Clean Architecture — রবার্ট মার্টিন (আঙ্কেল বব) 2012 সালে প্রণীত একটি আর্কিটেকচারাল প্যাটার্ন। মূল ধারণা হল কঠোর নির্ভরতা নিয়ম সহ অ্যাপ্লিকেশনকে স্তরে বিভক্ত করা: স্তরের ভিতরের কোড বাইরের কোড সম্পর্কে জানে না। বাইরের স্তরগুলি (UI, ফ্রেমওয়ার্ক, DB) বাস্তবায়নের বিবরণ। ভিতরের স্তরগুলি (ব্যবসায়িক লজিক, এন্টারপ্রাইজ নিয়ম) অ্যাপ্লিকেশনের সারমর্ম।
Clean Architecture-এর স্তরগুলি মোবাইল ডেভেলপমেন্টে: 1) Domain — Entities (ব্যবসায়িক অবজেক্ট) এবং Use Cases (ব্যবহারের পরিস্থিতি); 2) Data — RepositoryImpl (রিপোজিটরি বাস্তবায়ন), DataSources (নেটওয়ার্ক, DB, ক্যাশ); 3) 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, ক্যাশ) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
ডিপেন্ডেন্সি রুল — Clean Architecture-এর একমাত্র কঠোর নিয়ম। সোর্স কোড শুধুমাত্র নিজের ভিতরের স্তর বা নিচের স্তরকে (কেন্দ্রের কাছাকাছি) রেফারেন্স করতে পারে। Presentation Domain ইম্পোর্ট করে। Domain Data বা Presentation ইম্পোর্ট করে না। এটি ডিপেন্ডেন্সি ইনভার্সন প্রিন্সিপলের মাধ্যমে অর্জিত হয়: Domain Repository ইন্টারফেস সংজ্ঞায়িত করে, Data তা বাস্তবায়ন করে। Presentation UseCase অ্যাবস্ট্রাকশনের উপর নির্ভর করে, কোনো নির্দিষ্ট রিপোজিটরির উপর নয়।
Domain — অ্যাপ্লিকেশনের সবচেয়ে স্থিতিশীল স্তর। Entities হল ফ্রেমওয়ার্ক-স্বাধীন ব্যবসায়িক অবজেক্ট: User, Product, Order। Use Cases হল একটি একক invoke মেথড (বা Kotlin-এ operator fun invoke) বিশিষ্ট ক্লাস, যা একটি পরিস্থিতি বাস্তবায়ন করে: 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-এর জানা উচিত নয় যে ডেটা কোথা থেকে আসে (নেটওয়ার্ক, DB, ক্যাশ) বা কে তা প্রদর্শন করে (Compose, SwiftUI)। IT Sectr-এ আমরা প্রতিটি অপারেশনের জন্য একটি Use Case বরাদ্দ করি যাতে ব্যবসায়িক নিয়ম, ভ্যালিডেশন বা দুটি উৎস থেকে ডেটা কম্বিনেশন থাকে।
Domain-এর বিশুদ্ধতা স্তর সীমানায় DTO ম্যাপিংয়ের মাধ্যমে অর্জিত হয়। Data স্তর JSON মডেল (DTO) গ্রহণ করে, সেগুলিকে Domain Entity-তে ম্যাপ করে। Presentation Domain Entity গ্রহণ করে, ViewModel (DisplayItem)-এ ম্যাপ করে। Domain Entity-তে কখনও Retrofit, Room, বা Codable অ্যানোটেশন থাকে না — এটি নিশ্চিত করে যে DB Room থেকে Realm-এ বা Retrofit Ktor-এ পরিবর্তন করলে স্তরটি পরিবর্তন করতে হবে না।
Data Layer — Domain-এ সংজ্ঞায়িত ইন্টারফেসের বাস্তবায়ন। এতে RepositoryImpl (UserRepository বাস্তবায়নকারী ক্লাস) এবং DataSources (RemoteDataSource — API, LocalDataSource — DB, CacheDataSource — SharedPreferences/NSUserDefaults) অন্তর্ভুক্ত। Data স্তর Domain-এর উপর (রিপোজিটরি ইন্টারফেস এবং Entities ইম্পোর্ট করে) এবং ফ্রেমওয়ার্কের (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 ফ্ল্যাগ সহ পুরানো ডেটা ফেরত দেয়। Domain-এ Use Case কৌশল সম্পর্কে জানে না — এটি Repository.getUser(id) এর মাধ্যমে User পায়। কৌশল পরিবর্তন করা (যেমন, প্রতি 15 মিনিটে ক্যাশ ইনভ্যালিডেশন) Domain এবং Presentation-কে প্রভাবিত করে না।
Android-এ মডুলারিটি — Kotlin Multiplatform Android SDK নির্ভরতা ছাড়া Domain-কে আলাদা KMP মডিউলে বের করার অনুমতি দেয়। Data হল Domain-এর উপর নির্ভরতা সহ একটি আলাদা মডিউল। Presentation হল Domain-এর উপর নির্ভরতা সহ একটি Android মডিউল। Gradle নির্ভরতা: domain (বিশুদ্ধ 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 স্টেটে রূপান্তর করে। View স্টেটে সাবস্ক্রাইব করে এবং প্রদর্শন করে। Presentation Domain-এর উপর নির্ভর করে — Use Cases এবং Entities ইম্পোর্ট করে। Presentation Data Layer ইম্পোর্ট করে না — ডেটা Use Case-এর মাধ্যমে আসে, যা অভ্যন্তরীণভাবে Repository ব্যবহার করে।
Clean Architecture-এ ViewModel-এ কোনো ব্যবসায়িক লজিক নেই — এটি Use Case কল করে। যদি Use Case User ফেরত দেয়, ViewModel তা UserDisplayItem (name, emailFormatted, avatarUrl)-এ রূপান্তর করে — একটি বিশুদ্ধ প্রেজেন্টেশন মডেল। Use Case DisplayItem সম্পর্কে জানে না — এটি Entity ফেরত দেয়। এই বিভাজন UI ছাড়া Use Case এবং UseCase ছাড়া ViewModel (মকের মাধ্যমে) পরীক্ষা করার অনুমতি দেয়। 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
}
নেভিগেশন Presentation স্তরেও বাইরের রিংয়ের অংশ। Clean Architecture কোনো নেভিগেশন মেকানিজম নির্ধারণ করে না — এটি NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) বা Router (VIPER) হতে পারে। গুরুত্বপূর্ণ বিষয় হল নেভিগেশন সিদ্ধান্ত Presentation নেয়, কিন্তু নেভিগেশন Use Case-এ প্রবেশ করা উচিত নয়। Use Case ফলাফল ফেরত দেয়, ViewModel সিদ্ধান্ত নেয় কোন স্ক্রিনে যেতে হবে। Clean Architecture-এ, নেভিগেশন একটি বিবরণ যা Domain পরিবর্তন না করেই প্রতিস্থাপন করা যেতে পারে।
Android-এ Clean Architecture Gradle মডিউলের মাধ্যমে বাস্তবায়িত হয়: domain (বিশুদ্ধ 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 domain মডিউলে UserRepository ইন্টারফেসের সাথে আবদ্ধ হয়।
iOS-এ Clean Architecture আলাদা মডিউল ছাড়া SPM বা Xcode গ্রুপ ব্যবহার করে (Xcode সীমাবদ্ধতার কারণে)। Domain — একটি ফোল্ডার যার ফাইলগুলি UIKit বা SwiftUI ইম্পোর্ট করে না। Data — APIClient, CoreDataStack, RepositoryImpl সহ একটি ফোল্ডার। Presentation — ViewModels এবং SwiftUI Views সহ একটি ফোল্ডার। কনস্ট্রাক্টর বা অ্যাপে অ্যাসেম্বলির মাধ্যমে DI। প্রধান কল হল UI আপডেটের জন্য MainActor চেক সহ UseCase.execute() এর মাধ্যমে async/await।
// 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")
}
}
}
}
IT Sectr প্রকল্পে Clean Architecture — 30 দিন থেকে প্রকল্পের জন্য আমাদের মান। আমরা 2022 সাল থেকে Android/iOS-এর জন্য Kotlin Multiplatform সহ তিন-স্তরের আর্কিটেকচার ব্যবহার করছি। Domain — একটি শেয়ার্ড KMP মডিউল, Data — প্ল্যাটফর্ম মডিউল (Android-এ Retrofit, iOS-এ URLSession), Presentation — নেটিভ UI। এটি iOS এবং Android-এর মধ্যে 60–80% শেয়ার্ড বিজনেস লজিক কোড দেয়, যা দুটি আলাদা বাস্তবায়নের তুলনায় ডেভেলপমেন্ট সময় 30–40% কমায়।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
সর্বনিম্ন তিনটি: Domain, Data, Presentation। বড় প্রকল্পের জন্য, Framework (Android SDK/iOS UIKit নির্ভরতা) এবং Device (GPS, ক্যামেরা, সেন্সর) যোগ করা হয়। স্তরের সংখ্যা কোনো কঠোর নিয়ম নয় বরং সুবিধার বিষয়। মূল বিষয় হল ডিপেন্ডেন্সি রুল অনুসরণ করা: নির্ভরতাগুলি ভিতরের দিকে, Domain-এর দিকে নির্দেশ করে। আপনি তিনটি দিয়ে শুরু করতে পারেন এবং প্রকল্প বাড়ার সাথে সাথে স্তর যোগ করতে পারেন।
হ্যাঁ — MVVM-এর তুলনায় 30–50% পর্যন্ত রিপোজিটরি ইন্টারফেস, Use Cases এবং ম্যাপার উত্তোলনের কারণে। একটি সাধারণ CRUD অ্যাপ্লিকেশনের জন্য এটি অপ্রয়োজনীয়। Clean Architecture জটিল ব্যবসায়িক লজিক সম্পন্ন প্রকল্পের জন্য ন্যায্য যেখানে পরীক্ষাযোগ্যতা এবং স্তর বিচ্ছিন্নতা ডেভেলপমেন্ট গতির চেয়ে গুরুত্বপূর্ণ। MVP বা প্রোটোটাইপের জন্য MVVM ব্যবহার করুন — Clean Architecture শুরু ধীর করবে।
হ্যাঁ, এটি সাধারণ অভ্যাস। Use Cases Domain-এ থাকে, যখন Presentation MVI চক্র (Intent → Reducer → State) ব্যবহার করে। Data Layer একই থাকে, Domain একই থাকে। Presentation-এ MVI পূর্বানুমানযোগ্য স্ক্রিন স্টেট দেয়, Clean Architecture ব্যবসায়িক লজিক বিচ্ছিন্নতা দেয়। এই সংমিশ্রণ ডজনখানেক ডেভেলপার বিশিষ্ট বড় প্রকল্পে ব্যবহৃত হয়।
Use Case প্রয়োজন যখন অপারেশনে ব্যবসায়িক নিয়ম জড়িত: ভ্যালিডেশন, দুটি উৎস থেকে ডেটা কম্বিনেশন, গণনা, লগিং, অ্যাক্সেস পারমিশন চেক। অতিরিক্ত লজিক ছাড়া একটি সাধারণ getUser(id) অনুরোধ সরাসরি ViewModel থেকে Repository কল করতে পারে। তবে, আর্কিটেকচারাল সামঞ্জস্যের জন্য, অনেক টিম Repository-এর প্রতিটি পাবলিক মেথডের জন্য Use Case তৈরি করে — এটি 5–10% কোড যোগ করে কিন্তু পড়া সহজ করে।
Domain: মক Repository সহ Use Cases-এর ইউনিট টেস্ট — Android SDK ছাড়া বিশুদ্ধ Kotlin/Swift। Data: মক/ফেক DataSource সহ RepositoryImpl-এর ইন্টিগ্রেশন টেস্ট। Presentation: মক UseCase সহ ViewModel টেস্ট। ডিপেন্ডেন্সি রুলের কারণে, প্রতিটি স্তর আলাদাভাবে পরীক্ষিত হয়। IT Sectr-এ, Domain কভারেজ 95%, Data — 70–80%, Presentation — 60–70% পর্যন্ত পৌঁছায়।
সারাংশ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন