站在 Android 的角度,聊聊 Clean Architecture

6 阅读4分钟

Clean Architecture(整洁架构)是由著名软件工程师 Robert C. Martin 提出的一种软件设计理念和架构模式,它通过结构化的方式组织代码,使 Android 应用具备高度可维护性,可测试性和可扩展性。

核心思想

Clean Architecture 的核心原则是依赖规则:源代码依赖只能指向内层,内层不应该知道任何关于外层的东西。

  • 领域层(domain)-> 不依赖任何外层,纯 Kotlin/Java,零 Android 框架依赖,只定义业务规则,UseCase 和 Repository 接口。
  • 数据层(data)-> 依赖领域层,实现领域层定义的 Repository 接口,但领域层不知其实现。
  • 表现层(presentation)-> 依赖领域层,通过 UseCase 调用业务逻辑,不直接依赖数据层。

image.png

分层结构

先来看一下 Android 的项目结构

image.png

领域层 - 内层

职责与定位

  • 领域层是核心,承载了项目真正的业务价值。
  • 定义业务规则和业务模型,包含应用的核心业务逻辑。
  • 只关心要做什么,而不是怎么做。

主要组成

  • 实体(model):表示业务核心对象。
  • 用例(usecase):封装特定业务行为。
  • 仓库接口(repository):定义数据操作的契约,不包含实现。

关键特点

  • 纯 Kotlin/Java 代码,不依赖 Android 框架。
  • 不依赖任何第三方库。
  • 是整个项目中最稳定的一层。
data class Post(
    val id: String,
    val name: String
)
class GetPostsUseCase @Inject constructor(
    private val postRepository: PostRepository
) {
    suspend operator fun invoke(params: HashMap<String, String>): Result<BaseResp<List<Post>>> {
        return try {
            Result.success(postRepository.getPosts(params))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}
interface PostRepository {
    suspend fun getPosts(params: HashMap<String, String>): BaseResp<List<Post>>
}

数据层 - 中间层

职责与定位

  • 负责数据从哪里来,如何获取。
  • 处理数据检索和存储。
  • 实现领域层定义的数据访问接口。

主要组成

  • 数据源(datasource):本地数据源或远程数据源
  • 仓库实现(repository):协调不同数据源,实现领域层定义的 Repository 接口,处理异常,将技术异常转换为业务可理解的结果。

关键特点

  • 可以自由切换数据来源
  • 变化最频繁的一层
  • 可根据需求选择不同的数据存储方案
interface PostService {
    @GET("/xzj/net/api/example/post")
    suspend fun getPosts(@QueryMap params: HashMap<String, String>): BaseResp<List<Post>>
}
class PostRepositoryImpl @Inject constructor(
    private val postService: PostService
) : PostRepository {
    override suspend fun getPosts(params: HashMap<String, String>): BaseResp<List<Post>> {
        return postService.getPosts(params)
    }
}

表现层 - 外层

职责与定位

  • 负责 UI 展示和用户交互。
  • 将数据转换为用户可感知的界面状态。
  • 不包含业务规则,只负责管理 UIState。

主要组成

  • view:视图组件
  • ViewModel:通过 usecase 与 领域层交互
  • state:UI 状态

关键特点

  • 通常使用 MVVM 或 MVI 模式组织代码。
  • 专注于 UI State 管理。
  • 可轻松更换用户界面框架或进行界面优化,而不影响业务逻辑。
data class PostListState(
    val posts: List<Post> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)
@HiltViewModel
class PostListViewModel @Inject constructor(
    private val getPostsUseCase: GetPostsUseCase
) : ViewModel() {

    private val _state = MutableStateFlow(PostListState())
    val state: StateFlow<PostListState> = _state.asStateFlow()

    init {
        loadPosts()
    }

    private fun loadPosts() = viewModelScope.launch {
        _state.update { it.copy(isLoading = true) }
        val params = hashMapOf(
            "param1" to "param1",
            "param2" to "param2"
        )
        getPostsUseCase(params).onSuccess { posts ->
            _state.update { it.copy(posts = posts.data, isLoading = false) }
        }.onFailure { error ->
            _state.update { it.copy(error = error.message, isLoading = false) }
        }
    }
}
@Composable
fun PostListScreen(viewModel: PostListViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsState()

    when {
        state.isLoading -> CircularProgressIndicator()
        state.error != null -> Text("Error: ${state.error}")
        else -> LazyColumn {
            items(state.posts) { post ->
                Text(post.name, fontSize = 20.sp, color = Color.Red)
            }
        }
    }
}

Hilt 通过 @HiltViewModel 和 hiltViewModel() 自动处理 ViewModel 的依赖注入。在 Compose 中使用 hiltViewModel() 获取 ViewModel 实例,确保 ViewModel 及其依赖被正确初始化。

Hilt 依赖注入配置

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Singleton
    @Provides
    fun providePostService(retrofit: Retrofit): PostService {
        return retrofit.create(PostService::class.java)
    }
}


@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton
    @Provides
    fun providePostRepository(
        postService: PostService
    ): PostRepository {
        return PostRepositoryImpl(postService)
    }

    @Provides
    fun provideGetPostsUseCase(
        postRepository: PostRepository
    ): GetPostsUseCase {
        return GetPostsUseCase(postRepository)
    }
}

意义何在

看到这,你们可能要吐槽了,实现一个功能要建这么多层,这么多类,代码量多这么多,不是徒增工作量吗?真的有意义吗?

首先,徒增工作量是客观存在的,比如你实现一个简单的功能 - 获取用户信息,你需要创建领域层,数据层,表现层,还有各种接口与接口实现,对比 “一把梭” 写法(Activity 直接调用方法获取数据),代码量可能要翻三倍以上。

德国著名哲学家黑格尔曾说过:存在即合理。所以,Clean Architecture 既然存在,那肯定是有意义的!它的意义就在于:用短期成本换长期收益。

举个需求变更的例子吧,就拿常见的用户信息来说,一开始只要求显示 user.name,但是几天后,产品突然改主意了,VIP 用户得显示 VIP 和名字。

按照 “一把梭” 的写法,规则散落在各个 Composable 中的。

@Composable
fun UserCard(user: User) {
    Card {
        // 第一处:业务规则直接写死在 UI 里!
        Text(text = if (user.isVip) "VIP: ${user.name}" else user.name) 
    }
}

@Composable
fun UserProfileHeader(user: User) {
    Column {
        // 第二处!规则重复
        Text(text = if (user.isVip) "VIP: ${user.name}" else user.name, style = Title)
    }
}

@Composable
fun SearchItem(user: User) {
    Row {
        // 第三处!改需求时需全局搜索替换
        Text(text = if (user.isVip) "VIP: ${user.name}" else user.name)
    }
}

如果使用 Clean Architecture 的话,只需修改 domain 层即可,其本身就是规则的集中管理。

class GetDisplayNameUseCase { //规则唯一源头!
    operator fun invoke(user: User): String =
        if (user.isVip) "VIP: ${user.name}" else user.name
        // 只改这里!
}

只需修改 UseCase,其余都不用修改,简直不要太优雅。

@HiltViewModel
class UserViewModel @Inject constructor(
    private val getDisplayName: GetDisplayNameUseCase // 依赖注入
) : ViewModel() {
    // ViewModel 只负责调用规则,不包含规则本身。
    fun getDisplayName(user: User) = getDisplayName(user)
}
@Composable
fun UserCard(user: User, viewModel: UserViewModel = hiltViewModel()) {
    Card {
        // Composable 纯粹只关注显示什么,不关心为什么这样显示。
        Text(text = viewModel.getDisplayName(user))
    }
}

@Composable
fun UserProfileHeader(user: User, viewModel: UserViewModel = hiltViewModel()) {
    Text(text = viewModel.getDisplayName(user), style = Title)
}

@Composable
fun SearchItem(user: User, viewModel: UserViewModel = hiltViewModel()) {
    Text(text = viewModel.getDisplayName(user))
}

下次产品再说加什么其他信息的时候,你只需微笑打开 UseCase 即可,而不用翻遍所有 Composable。

但也不是所有项目都该用,而是该用时必须用!对于大型且需持续迭代的项目,架构治理是必然要求,随着代码量增加,改动成本不断上升,所以比较适用于 “中大型”,“团队协作”,“高可靠性要求” 的项目。如果你的项目本身规模不大,也不需要花额外精力治理架构,对于小规模项目,引入 Clean Architecture 可能会增加开发成本,所以不适用。

架构不是炫技,而是为未来的自己和团队减负,我们不能 “为了架构而架构”,若项目注定 “一次性”,请大胆简化,效率优先,若项目需 “活下去、长出来”,就得接受短期成本,投资长期健康。Clean Architecture 的精髓就在于它不承诺 “写得更快”,但承诺 “改得更稳”。