- Định nghĩa Clean Architecture và giải thích các nguyên tắc cốt lõi
- Xác định lợi ích của Clean Architecture so với kiến trúc phân lớp truyền thống
Tổng quan Clean Architecture
Tổng quan Clean Architecture - Khóa học Mobile Action Pro
Video bài học sắp lên sóng
Trong lúc chờ, bạn hãy đọc bản text của bài học bên dưới: đầy đủ lý thuyết, code mẫu và checklist thực hành.
Tổng quan khái niệm
Clean Architecture là một triết lý thiết kế phần mềm được giới thiệu bởi Robert C. Martin (Uncle Bob), tổ chức code thành các lớp với các quy tắc phụ thuộc nghiêm ngặt. Mục tiêu chính là tạo ra các hệ thống độc lập với frameworks, UI, databases và các tác nhân bên ngoài.
Trong phát triển Android, Clean Architecture giúp giải quyết các vấn đề phổ biến:
- Tight coupling: Khi ViewModels trực tiếp truy cập databases, việc thay đổi nguồn dữ liệu sẽ phá vỡ mọi thứ
- Untestable code: Khi business logic trộn lẫn với UI code, kiểm thử yêu cầu chạy toàn bộ ứng dụng
- Maintenance nightmares: Khi thay đổi một tính năng làm hỏng các tính năng không liên quan
Codebase kotlin-accelerator-ai triển khai 4-layer Clean Architecture:
- UI Layer: Compose screens và components
- ViewModel Layer: UI state và event handling
- Business Logic Layer: Domain logic, managers, use cases
- Data Layer: Repositories, data sources (Supabase, DataStore)
Tại sao điều này quan trọng
Không có Clean Architecture, một thay đổi đơn giản như "chuyển từ Supabase sang Firebase" có thể yêu cầu chỉnh sửa hàng trăm files. Với Clean Architecture, bạn chỉ sửa đổi data layer implementation—mọi thứ khác vẫn không thay đổi.
Cú pháp & Ví dụ
Traditional Approach (Tightly Coupled)
// ❌ XẤU: ViewModel trực tiếp truy cập Supabase
class GameViewModel(
private val supabase: SupabaseClient
) : ViewModel() {
fun loadQuestion() {
viewModelScope.launch {
val question = supabase.from("questions")
.select()
.decodeSingle<Question>()
_uiState.value = GameUiState(question = question)
}
}
}
Vấn đề:
- ViewModel biết về chi tiết implementation của Supabase
- Không thể test mà không có Supabase instance thật
- Thay đổi data source yêu cầu chỉnh sửa ViewModel
Clean Architecture Approach (Decoupled)
// ✅ TỐT: ViewModel phụ thuộc vào abstraction
class GameViewModel(
private val questionRepository: QuestionRepository // Interface, không phải concrete class
) : ViewModel() {
fun loadQuestion() {
viewModelScope.launch {
val questions = questionRepository.newGameQuestions()
_uiState.value = GameUiState(question = questions.firstOrNull())
}
}
}
Lợi ích:
- ViewModel không biết về data sources
- Có thể mock
QuestionRepositorycho tests - Thay đổi data source không ảnh hưởng ViewModel
Ví dụ Production
File: GameViewModel.kt
class GameViewModel(
reducer: GameStateMachine,
private val overlayQueueManager: GameOverlayQueueManager,
gameTimer: GameTimer,
private val feedbackController: GameFeedbackController,
private val textContentRepository: TextContentRepository,
analyticsManager: AnalyticsManager,
questionRepository: QuestionRepository,
lifelineHelper: LifelineHelper,
userProfileManager: UserProfileManager,
dispatchers: QzDispatchers,
) : DreStoreViewModel<GameState, GameAction, GameSideEffect, GameAsyncOp>(
reducer = reducer,
dispatchContext = dispatchers.mainImmediate,
), IGameViewModel {
private val asyncOpExecutor = GameAsyncOpExecutor(
questionRepository = questionRepository,
lifelineHelper = lifelineHelper,
answerProcessor = AnswerProcessor(),
dispatchers = dispatchers,
)
override fun submitAnswer(answer: String) = dispatch(GameAction.SubmitAnswer(answer))
}
Tại sao Pattern này?
- Reducer-based business logic:
GameStateMachinegiữ state transitions deterministic - Interface-based data access:
QuestionRepository,TextContentRepository,AnalyticsManagervàUserProfileManagerđều là abstractions - Side-effect isolation:
GameAsyncOpExecutorxử lý async I/O; các side-effect handlers xử lý timer, overlay, analytics, stats và navigation - Testability: Reducer có thể test không cần Android, ViewModel có thể inject mocked repositories/services
ViewModel không biết câu hỏi đến từ đâu (mock data hiện tại, Supabase/REST/local database sau này). Nó chỉ biết repository/service contracts và DRE-KT actions.
Bài tập thực hành
Nhiệm vụ
Refactor một ViewModel tightly-coupled để sử dụng các nguyên tắc Clean Architecture.
Yêu cầu
- Tạo interface
UserRepositoryvới methodgetUser(id: String): User? - Tạo
UserViewModelphụ thuộc vào interface (không phải concrete class) - Đảm bảo ViewModel có thể được test với fake repository
Starter Code
// Code tightly-coupled hiện tại
class UserViewModel(
private val database: AppDatabase // Direct dependency on Room
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_user.value = database.userDao().getUser(userId)
}
}
}
Giải pháp
// Bước 1: Định nghĩa repository interface (data layer)
interface UserRepository {
suspend fun getUser(id: String): User?
}
// Bước 2: Implement repository (data layer)
class UserRepositoryImpl(
private val database: AppDatabase
) : UserRepository {
override suspend fun getUser(id: String): User? {
return database.userDao().getUser(id)
}
}
// Bước 3: Refactor ViewModel để phụ thuộc vào abstraction
class UserViewModel(
private val userRepository: UserRepository // Interface, không phải concrete class
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_user.value = userRepository.getUser(userId)
}
}
}
// Bước 4: Tạo fake cho testing
class FakeUserRepository : UserRepository {
private val users = mutableMapOf<String, User>()
fun addUser(user: User) {
users[user.id] = user
}
override suspend fun getUser(id: String): User? = users[id]
}
// Bước 5: Test với fake repository
@Test
fun `loadUser should update user state`() = runTest {
val fakeRepo = FakeUserRepository().apply {
addUser(User(id = "1", name = "Alice"))
}
val viewModel = UserViewModel(fakeRepo)
viewModel.loadUser("1")
advanceUntilIdle()
assertEquals("Alice", viewModel.user.value?.name)
}
Những thay đổi chính:
- ViewModel giờ phụ thuộc vào interface
UserRepository(abstraction) - Truy cập database thực tế được chuyển sang
UserRepositoryImpl(data layer) - Có thể dễ dàng tạo
FakeUserRepositorycho testing - Thay đổi từ Room sang Supabase chỉ ảnh hưởng
UserRepositoryImpl
Những điểm chính
- Clean Architecture tổ chức code thành các lớp với quy tắc phụ thuộc nghiêm ngặt
- Inner layers (business logic, data) không phụ thuộc vào outer layers (UI, frameworks)
- Phụ thuộc vào abstractions (interfaces) thay vì concrete implementations
- Lợi ích: testability, maintainability, flexibility, team collaboration
- kotlin-accelerator-ai sử dụng kiến trúc 4-layer: UI → ViewModel → Business Logic → Data
Tài liệu tham khảo
Tài liệu chính thức
- Clean Architecture (Uncle Bob) - Giải thích concept gốc
- Android Architecture Guide - Khuyến nghị chính thức của Android
- Guide to App Architecture - Separation of concerns
Codebase References
| File | Line | Description |
|---|---|---|
GameViewModel.kt | 15-60 | DRE-KT ViewModel phụ thuộc abstractions |
GameStateMachine.kt | 1-80 | Pure reducer cho game state transitions |
LeaderboardRepository.kt | 5-10 | Repository interface abstraction |