0

Thực tế SOLID trong Android (có kèm code)

Là lập trình viên Android, chúng ta thường nghe nói về các nguyên lý SOLID, nhưng hãy thực tế một chút — không phải lúc nào cũng rõ cách áp dụng chúng cụ thể vào các dự án của mình. SOLID là tập hợp năm nguyên lý thiết kế giúp phần mềm dễ bảo trì, linh hoạt và dễ mở rộng hơn. Được đặt ra bởi Uncle Bob (Robert C. Martin), những nguyên lý này đặc biệt hữu ích trong Android, nhất là khi làm việc với codebase lớn hoặc muốn cải thiện khả năng kiểm thử và tách biệt trách nhiệm rõ ràng.

Trong bài viết này, chúng ta sẽ khám phá các tình huống thực tế trong phát triển Android cho từng nguyên lý SOLID — kèm theo đoạn code Kotlin và giải thích chi tiết. Dù bạn đang xây dựng UI với Jetpack Compose, tích hợp API, hay cấu trúc ViewModel và UseCase, bạn sẽ có kiến thức thực tế có thể áp dụng ngay.

🟠 1. Single Responsibility Principle - SRP

Một class chỉ nên có một lý do để thay đổi.

Trong phát triển Android, các class thường phình to nhanh chóng — như Activity, Fragment hoặc ViewModel xử lý UI, điều hướng, gọi API, xử lý lỗi và nhiều thứ khác. SRP khuyến khích ta tách các trách nhiệm ra để mỗi class làm tốt một việc duy nhất.

✅ Tình huống 1: Tách logic UI và nghiệp vụ khỏi ViewModel

class LoginUseCase(private val repository: LoginRepository) {
    suspend fun login(username: String, password: String): Result<User> {
        if (username.isEmpty()) return Result.failure(Exception("Username empty"))
        return repository.login(username, password)
    }
}

class LoginViewModel(private val useCase: LoginUseCase) : ViewModel() {
    val uiState = MutableStateFlow<UiState>(UiState.Idle)
    fun login(username: String, password: String) {
        viewModelScope.launch {
            uiState.value = UiState.Loading
            val result = useCase.login(username, password)
            uiState.value = if (result.isSuccess) UiState.Success else UiState.Error
        }
    }
}

Tại sao điều này quan trọng:

Tách logic nghiệp vụ khỏi ViewModel và đưa vào UseCase giúp tách biệt trách nhiệm và dễ kiểm thử hơn. ViewModel chỉ tập trung vào quản lý trạng thái UI, còn UseCase có thể tái sử dụng và kiểm thử độc lập.

✅ Tình huống 2: Tách gọi API khỏi Repository

interface RemoteDataSource {
    suspend fun login(username: String, password: String): Result<User>
}

class LoginRepository(private val remote: RemoteDataSource) {
    suspend fun login(username: String, password: String) = remote.login(username, password)
}

Bạn ủy quyền cho RemoteDataSource xử lý việc gọi API, giữ cho Repository chỉ tập trung vào việc điều phối dữ liệu. Điều này giúp dễ thay thế hoặc mock phần remote khi kiểm thử.

✅ Tình huống 3: Tách chức năng trong Fragment

class ProfileFragment : Fragment() {
    private val viewModel: ProfileViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        setupUi()
        observeViewModel()
    }

    private fun setupUi() { /* setup views, listeners */ }
    private fun observeViewModel() { /* collect flows */ }
}

Thay vì nhồi nhét tất cả vào onViewCreated, bạn chia nhỏ thành các phương thức riêng biệt với một nhiệm vụ cụ thể. Điều này giúp dễ đọc và đảm bảo Fragment tuân thủ SRP — mỗi phương thức chỉ làm một việc.

🟡 2. Open/Closed Principle - OCP

Thành phần phần mềm nên mở để mở rộng, nhưng đóng để sửa đổi.

Bạn nên có thể thêm tính năng mới mà không cần chỉnh sửa mã hiện có. Điều này giảm nguy cơ lỗi và giúp code dễ mở rộng.

✅ Tình huống 1: Mở rộng sealed class cho trạng thái UI

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: String) : UiState()
    data class Error(val message: String) : UiState()
}
data class Empty(val reason: String) : UiState()

Bạn có thể thêm trạng thái mới như Empty mà không cần chỉnh sửa logic hiện có. Sealed class cho phép mở rộng an toàn, có chủ đích.

✅ Tình huống 2: ViewModel Factory tuỳ chỉnh với DI

class MyViewModelFactory @Inject constructor(
    private val useCase: SomeUseCase
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MyViewModel(useCase) as T
    }
}

Bạn có thể inject các UseCase khác nhau vào ViewModel mà không cần thay đổi ViewModel gốc — chỉ cần mở rộng hành vi của factory.

✅ Tình huống 3: Extension cho Modifier trong Compose

fun Modifier.errorBorder(): Modifier = this.border(2.dp, Color.Red)

TextField(
    value = username,
    onValueChange = { username = it },
    modifier = Modifier
        .fillMaxWidth()
        .then(if (hasError) Modifier.errorBorder() else Modifier)
)

Modifier cho phép bạn mở rộng hành vi UI mà không cần thay đổi component. Giúp UI linh hoạt và dễ tái sử dụng hơn.

🟢 3. Liskov Substitution Principle - LSP

Các lớp con có thể thay thế cho lớp cha mà không làm thay đổi hành vi chương trình.

Bất kỳ subclass hoặc implementation nào cũng nên sử dụng được ở nơi class cha được kỳ vọng — mà không làm phát sinh lỗi.

✅ Tình huống 1: Base Fragment với phương thức mẫu

abstract class BaseFragment : Fragment() {
    abstract fun setupObservers()
    abstract fun setupUI()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        setupUI()
        setupObservers()
    }
}

class LoginFragment : BaseFragment() {
    override fun setupUI() { /* Login UI */ }
    override fun setupObservers() { /* ViewModel observers */ }
}

Bạn có thể tạo nhiều màn hình khác nhau dựa trên cấu trúc của BaseFragment mà không cần viết lại code lifecycle. Mỗi màn hình chỉ cần mở rộng hành vi đã có một cách an toàn.

✅ Tình huống 2: RecyclerView với nhiều loại ViewHolder

abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    abstract fun bind(item: ListItem)
}

class TextViewHolder(view: View) : BaseViewHolder(view) {
    override fun bind(item: ListItem) { /* Bind text */ }
}

class ImageViewHolder(view: View) : BaseViewHolder(view) {
    override fun bind(item: ListItem) { /* Bind image */ }
}

Mỗi ViewHolder có thể dùng ở bất kỳ đâu BaseViewHolder được mong đợi. Logic adapter có thể vẫn chung chung, nhưng hành vi thì tuỳ chỉnh được cho từng loại dữ liệu.

✅ Tình huống 3: Thay thế Dispatchers khi kiểm thử

open class CoroutineDispatcherProvider {
    open val io = Dispatchers.IO
    open val main = Dispatchers.Main
}

class TestDispatcherProvider : CoroutineDispatcherProvider() {
    override val io = UnconfinedTestDispatcher()
    override val main = UnconfinedTestDispatcher()
}

Trong kiểm thử, bạn có thể inject subclass mà không cần đổi logic production. Class vẫn hoạt động đúng với bất kỳ subclass nào của CoroutineDispatcherProvider.

🔵 4. Interface Segregation Principle - ISP

Client không nên bị buộc phải phụ thuộc vào các interface mà chúng không sử dụng.

Trong Android, điều này đồng nghĩa với việc giữ cho các interface rõ ràng và tập trung — tránh hợp đồng có quá nhiều method không liên quan.

✅ Tình huống 1: Tách riêng contract của Repository

interface AuthRepository {
    suspend fun login(username: String, password: String): Result<User>
}

interface ProfileRepository {
    suspend fun getProfile(): Result<UserProfile>
}

LoginViewModel không cần biết đến getProfile() — nên interface của nó cũng không có. Interface nhỏ gọn giúp giảm liên kết và dễ kiểm thử hơn.

✅ Tình huống 2: Lắng nghe sự kiện click trong RecyclerView

interface OnImageClickListener {
    fun onImageClick(url: String)
}

interface OnTextClickListener {
    fun onTextClick(text: String)
}

Mỗi adapter chỉ cần implement phần nó cần. Giữ component nhẹ và tránh phải override các phương thức thừa.

✅ Tình huống 3: View contract tách biệt theo tính năng

interface LoginContract {
    fun showLoginForm()
    fun showLoginSuccess()
}

interface SignupContract {
    fun showSignupForm()
    fun showSignupSuccess()
}

Tránh ép các màn hình không liên quan (vd: Login và Signup) phải implement các phương thức không dùng. Interface tập trung = code sạch hơn.

🟣 5.Dependency Inversion Principle - DIP

Phụ thuộc vào abstraction, không phụ thuộc vào implementation.

Đây là nền tảng của các ứng dụng Android có kiến trúc mở rộng tốt như Clean Architecture hoặc MVVM với DI.

✅ Tình huống 1: Inject UseCase vào ViewModel

class LoginViewModel(private val loginUseCase: LoginUseCase) : ViewModel() {
    fun login() = viewModelScope.launch {
        loginUseCase.login()
    }
}

Bạn có thể mock LoginUseCase khi kiểm thử. ViewModel không cần biết implementation cụ thể — nó chỉ gọi abstraction.

✅ Tình huống 2: Trừu tượng hóa nguồn dữ liệu

interface UserDataSource {
    suspend fun fetchUser(): User
}

class RemoteUserDataSource : UserDataSource {
    override suspend fun fetchUser() = api.getUser()
}

class LocalUserDataSource : UserDataSource {
    override suspend fun fetchUser() = db.getUser()
}

Bạn có thể chuyển đổi nguồn dữ liệu lúc runtime — ví dụ, dùng remote khi có mạng, local khi offline — mà không cần đổi phần còn lại của code.

✅ Tình huống 3: Trừu tượng hoá lưu trữ xác thực

interface AuthStorage {
    suspend fun saveToken(token: String)
    suspend fun getToken(): String?
}

class DataStoreAuthStorage(...) : AuthStorage {
    override suspend fun saveToken(token: String) { /* Save to DataStore */ }
    override suspend fun getToken(): String? { /* Load from DataStore */ }
}

Tại sao điều này quan trọng: Dù bạn dùng DataStore, EncryptedSharedPrefs, hay bất cứ thứ gì khác — logic ứng dụng vẫn không đổi. Phần còn lại của app chỉ nói chuyện với abstraction.

✅ Kết luận

Các nguyên lý SOLID không chỉ là lý thuyết suông — mà là những công cụ thực tế giúp bạn cải thiện codebase Android ngay hôm nay.

Tóm lại:

  • SRP: tách rõ trách nhiệm, giúp class tập trung.
  • OCP: thêm tính năng mới mà không cần sửa code cũ.
  • LSP: bảo đảm kế thừa an toàn và đa hình đúng cách.
  • ISP: chia nhỏ interface giúp dễ dùng, dễ kiểm thử.
  • DIP: giảm phụ thuộc cụ thể, tăng linh hoạt và khả năng kiểm thử.

Hãy bắt đầu từ những bước nhỏ. Refactor một ViewModel. Tách một interface lớn thành hai interface nhỏ. Inject một abstraction. Những thay đổi nhỏ hôm nay sẽ dẫn bạn đến một kiến trúc vững chắc về sau 😉.

Nguồn : https://proandroiddev.com/top-3-android-use-cases-for-every-solid-principle-with-code-960eedcdbc3f


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí