Dan Tech Academy
Sau bài này bạn sẽ
  • Đị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
Thời lượng25 phút
Độ khóIntermediate

Tổng quan Clean Architecture

Tổng quan Clean Architecture - Khóa học Mobile Action Pro

Video đang cập nhật

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.

Bản quay đang trong khâu hậu kỳ

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:

  1. UI Layer: Compose screens và components
  2. ViewModel Layer: UI state và event handling
  3. Business Logic Layer: Domain logic, managers, use cases
  4. 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.

Clean Architecture so với Kiến trúc Truyền thống

Cú pháp & Ví dụ

Traditional Approach (Tightly Coupled)

kotlin
// ❌ 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)

kotlin
// ✅ 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 QuestionRepository cho tests
  • Thay đổi data source không ảnh hưởng ViewModel

Ví dụ Production

File: GameViewModel.kt

kotlin
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: GameStateMachine giữ state transitions deterministic
  • Interface-based data access: QuestionRepository, TextContentRepository, AnalyticsManagerUserProfileManager đều là abstractions
  • Side-effect isolation: GameAsyncOpExecutor xử 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

  1. Tạo interface UserRepository với method getUser(id: String): User?
  2. Tạo UserViewModel phụ thuộc vào interface (không phải concrete class)
  3. Đảm bảo ViewModel có thể được test với fake repository

Starter Code

kotlin
// 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

kotlin
// 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 FakeUserRepository cho 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

Codebase References

FileLineDescription
GameViewModel.kt15-60DRE-KT ViewModel phụ thuộc abstractions
GameStateMachine.kt1-80Pure reducer cho game state transitions
LeaderboardRepository.kt5-10Repository interface abstraction

Đăng nhập