94、整洁架构:依赖规则、用例层、接口适配层、框架层

整洁架构,说白了就是一套让代码「活得更久」的规则。我见过太多项目,刚开始时结构清爽,半年后就成了意大利面条。为什么会这样?因为大家忽略了依赖的方向。

今天咱们就聊聊整洁架构的核心:依赖规则、用例层、接口适配层、框架层。这四层搞明白了,你的Kotlin项目就能经得起时间考验。

依赖规则:箭头只能朝里指

整洁架构最核心的一条规则,就是依赖方向必须从外向内。外层可以依赖内层,内层绝对不能依赖外层。

我习惯把代码画成同心圆:

核心原则:内层定义接口,外层实现接口。内层不知道外层的存在。

举个例子,你的业务逻辑(内层)需要保存数据,它只定义一个接口:

// 内层:纯业务,不依赖任何框架
interface UserRepository {
    fun save(user: User)
    fun findById(id: String): User?
}

外层的数据层去实现它:

// 外层:依赖Room框架
class RoomUserRepository(private val dao: UserDao) : UserRepository {
    override fun save(user: User) {
        dao.insert(user.toEntity())
    }
    
    override fun findById(id: String): User? {
        return dao.findById(id)?.toDomain()
    }
}

你看,内层的UserRepository根本不知道Room的存在。这就是依赖反转。我在项目中遇到过有人把Room的注解直接写在领域层,结果换数据库时改得想哭。

用例层:业务逻辑的「剧本」

用例层是整洁架构的灵魂。它不关心数据怎么存,也不关心界面怎么展示。它只关心一件事:用户想做什么

我习惯把每个用例写成一个类,名字就叫XxxUseCase

class GetUserProfileUseCase(
    private val userRepo: UserRepository,
    private val logger: Logger
) {
    operator fun invoke(userId: String): Result<UserProfile> {
        return try {
            logger.log("Fetching profile for user $userId")
            val user = userRepo.findById(userId) 
                ?: return Result.failure(UserNotFoundException(userId))
            val profile = UserProfile(
                name = user.name,
                email = user.email,
                joinDate = user.createdAt
            )
            Result.success(profile)
        } catch (e: Exception) {
            logger.error("Failed to get profile", e)
            Result.failure(e)
        }
    }
}

这里有几个要点:

  • 用例类只依赖接口,不依赖具体实现
  • 每个用例只做一件事,单一职责
  • 返回Result或自定义的Either,不抛异常

我的习惯:用例类用operator fun invoke,调用方用起来就像在调函数。简洁又直观。

接口适配层:翻译官的角色

这一层负责把外部的数据格式「翻译」成内层能理解的东西。说白了,就是做数据转换。

我把它分成两部分:

  • 输入适配:把网络请求、用户输入转成用例需要的参数
  • 输出适配:把领域模型转成UI需要展示的数据

举个例子,从API拿到的JSON和领域模型往往不一样:

// 接口适配层:DTO转领域模型
data class UserResponse(
    val id: String,
    val full_name: String,
    val email_address: String
)

fun UserResponse.toDomain(): User {
    return User(
        id = id,
        name = full_name,
        email = email_address
    )
}

我曾经见过有人直接把DTO传到用例层,结果API字段一改,业务逻辑也跟着崩。嗯,这就是没做好适配的代价。

框架层:最外层的「工具人」

框架层是最外层,包括Android的Activity、Retrofit、Room、Ktor等等。这一层的代码可以依赖任何框架,但绝对不能向内层泄露框架的细节

我习惯把框架层的代码写得尽量薄:

// 框架层:Activity只做两件事
class UserProfileActivity : AppCompatActivity() {
    
    private val viewModel: UserProfileViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_profile)
        
        // 1. 观察状态
        viewModel.uiState.observe(this) { state ->
            render(state)
        }
        
        // 2. 触发事件
        val userId = intent.getStringExtra("user_id") ?: return
        viewModel.loadProfile(userId)
    }
    
    private fun render(state: UiState<UserProfile>) {
        // 只做UI渲染,不做业务判断
    }
}

Activity里不应该有if (user == null)这种业务逻辑。它只负责把状态变成界面。

避坑指南:我曾经在一个项目里看到Activity直接调用了Room的DAO。结果想加个缓存层,得改Activity、改ViewModel、改Repository……牵一发动全身。记住,框架层的代码要尽量「无脑」,只做转发和渲染。

一张图看懂整洁架构

下面这张SVG图,把四层结构和依赖方向画清楚了。你仔细看看,箭头都是从外指向内的:

框架层 (Framework) Android SDK, Retrofit, Room, Ktor, Hilt 接口适配层 (Interface Adapters) ViewModel, Presenter, DTO转换, 路由 用例层 (Use Cases) GetUserProfileUseCase, LoginUseCase 领域层 (Domain) 实体、接口定义、业务规则 依赖方向:外层 → 内层(依赖反转)

实际项目中的分层建议

说了这么多理论,咱们看看实际项目里怎么分模块。我一般这样组织:

模块名 对应层级 包含内容 依赖
:domain 领域层 实体、接口定义、值对象 无(纯Kotlin)
:usecases 用例层 UseCase类、业务编排 只依赖:domain
:data 接口适配层 Repository实现、DTO、数据源 依赖:domain:usecases
:presentation 接口适配层 ViewModel、UI状态、导航 依赖:usecases
:app 框架层 Activity、Fragment、DI配置 依赖所有下层模块

小技巧:用Gradle的apiimplementation控制依赖可见性。内层模块用api暴露接口,外层模块用implementation隐藏实现细节。

为什么整洁架构值得学?

你想想看,一个项目做两年后,最怕什么?怕改不动。整洁架构的好处就是:

  • 换框架不伤筋骨:从Retrofit换到Ktor,只用改数据层
  • 业务逻辑可测试:用例层不依赖Android,单元测试跑得飞快
  • 新人上手快:每层职责清晰,不用猜代码在干嘛

我接手过一个没有分层的老项目,一个UserManager类写了3000行,里面混着网络请求、数据库操作、UI更新。想加个功能,得先花一周读懂这坨代码。嗯,从那以后我坚决推行分层架构。

整洁架构不是银弹,但它能让你在项目变大时,依然睡得安稳。说白了,就是给未来的自己留条活路。


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