87、Clean Architecture:分层架构、依赖反转、UseCase、数据层与展示层分离

Clean Architecture,说白了就是一套让代码「活得更久」的架构思想。

我最早接触它是在一个维护了三年的老项目里。那项目一开始挺清爽的,半年后业务一多,Activity 里塞了上千行代码,改个需求要翻半天。嗯,那时候我就意识到——架构不是炫技,是保命。

为什么需要分层?

你想想看,一个 App 里,UI 要变、数据源要换、业务逻辑要调整。如果全混在一起,改一处崩三处。

分层的目的很简单:让每一层只关心自己的事

核心原则:

  • 展示层只管 UI 和用户交互
  • 数据层只管数据的获取与存储
  • 领域层(UseCase)只管业务逻辑

我在项目中遇到过一种情况:PM 说「列表页的数据源从本地缓存改成网络请求」。如果没分层,你得改 Activity、改 Adapter、改网络工具类……到处翻。分层之后,只需要换数据层的实现,展示层完全不动。

依赖反转:谁依赖谁?

传统写法是上层依赖下层。Activity 直接 new 一个 Repository,Repository 直接 new 一个 ApiService。这有什么问题?耦合太紧,测试难写,换实现得改源码。

依赖反转告诉你:上层定义接口,下层实现接口

// 领域层定义接口(不依赖任何框架)
interface UserRepository {
    fun getUser(id: String): User
}

// 数据层实现接口(依赖具体框架)
class UserRepositoryImpl(
    private val api: UserApi,
    private val cache: UserCache
) : UserRepository {
    override fun getUser(id: String): User {
        // 先查缓存,再查网络
    }
}

// 展示层通过依赖注入拿到接口,不关心具体实现
class UserViewModel(
    private val repository: UserRepository
) {
    fun loadUser(id: String) {
        viewModelScope.launch {
            val user = repository.getUser(id)
            _uiState.value = UserLoaded(user)
        }
    }
}

说白了,依赖反转就是「我不 care 你怎么实现的,我只 care 你能不能给我想要的东西」。

我的习惯:接口定义放在 domain 层,实现放在 data 层。这样 domain 层零依赖,可以单独做单元测试。

UseCase:业务逻辑的「单元」

很多人问:UseCase 不就是把 Repository 的方法再包一层吗?

不是的。UseCase 代表一个完整的业务场景

class GetUserWithOrdersUseCase(
    private val userRepo: UserRepository,
    private val orderRepo: OrderRepository
) {
    suspend operator fun invoke(userId: String): Result<UserWithOrders> {
        val user = userRepo.getUser(userId) ?: return Result.failure(...)
        val orders = orderRepo.getOrdersByUser(userId)
        return Result.success(UserWithOrders(user, orders))
    }
}

你看,这个 UseCase 组合了两个 Repository 的数据,还做了错误处理。这就是业务逻辑的「原子操作」。

我曾经在一个电商项目里,把「下单」流程拆成了 5 个 UseCase:校验库存、计算价格、生成订单、扣减库存、发送通知。每个 UseCase 都可以单独测试,出了问题直接定位到具体步骤。

注意:不要滥用 UseCase。如果一个 UseCase 里只有一行 repo.getXxx(),那它可能只是「过度封装」。我建议:当业务逻辑需要组合多个数据源、或者有判断/转换逻辑时,才抽成 UseCase。

数据层与展示层分离

展示层只关心两件事:状态事件

数据层只关心三件事:从哪里拿数据存到哪里怎么同步

我习惯用 Repository 模式来做这层隔离:

// 数据层
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {
    
    override suspend fun getUser(id: String): User {
        // 策略:先读缓存,缓存没有就走网络,网络成功就更新缓存
        val cached = localDataSource.getUser(id)
        if (cached != null) return cached
        
        val remote = remoteDataSource.getUser(id)
        localDataSource.saveUser(remote)
        return remote
    }
}

// 展示层
class UserProfileViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            getUserUseCase(id).fold(
                onSuccess = { user -> _uiState.value = UiState.Success(user) },
                onFailure = { e -> _uiState.value = UiState.Error(e.message) }
            )
        }
    }
}

展示层不知道数据是从网络来的还是从数据库来的。它只认 UseCase 返回的结果。这就是分离的意义。

一张图看懂 Clean Architecture

数据层 (Data Layer) 领域层 (Domain Layer) 展示层 (Presentation Layer) ViewModel / UI 状态管理 + 事件 UseCase 业务逻辑单元 Repository 接口 定义契约 Entity / Model 纯数据对象 Remote DataSource Local DataSource Repository 实现 依赖 依赖 依赖

实际项目中的分层建议

层级 包含内容 依赖关系 可测试性
展示层 Activity / Fragment / ViewModel / Compose 依赖领域层接口 ⭐⭐⭐(可 mock UseCase)
领域层 UseCase / Repository 接口 / Entity 零依赖 ⭐⭐⭐⭐⭐(纯 Kotlin 逻辑)
数据层 Repository 实现 / DataSource / DTO / 网络库 / 数据库 依赖领域层接口 ⭐⭐⭐⭐(可 mock 数据源)

避坑指南:我曾经在一个项目里把 DTO(数据传输对象)直接暴露给了展示层。结果后端改了字段名,前端到处报错。后来我强制在 Repository 里做 DTO 到 Entity 的映射,展示层只认 Entity。从此世界清净了。

总结一下

Clean Architecture 不是什么高深理论。它就是一套「让代码好改、好测、好维护」的约定。

  • 展示层只管 UI,不碰数据源
  • 领域层定义业务逻辑,不依赖框架
  • 数据层实现数据获取,不暴露细节
  • 依赖反转让各层解耦,换实现就像换零件

你想想看,如果每个模块都只做一件事,改起来是不是轻松多了?


公众号:蓝海资料掘金营,微信deep3321