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图,把四层结构和依赖方向画清楚了。你仔细看看,箭头都是从外指向内的:
实际项目中的分层建议
说了这么多理论,咱们看看实际项目里怎么分模块。我一般这样组织:
| 模块名 | 对应层级 | 包含内容 | 依赖 |
|---|---|---|---|
:domain |
领域层 | 实体、接口定义、值对象 | 无(纯Kotlin) |
:usecases |
用例层 | UseCase类、业务编排 | 只依赖:domain |
:data |
接口适配层 | Repository实现、DTO、数据源 | 依赖:domain、:usecases |
:presentation |
接口适配层 | ViewModel、UI状态、导航 | 依赖:usecases |
:app |
框架层 | Activity、Fragment、DI配置 | 依赖所有下层模块 |
小技巧:用Gradle的api和implementation控制依赖可见性。内层模块用api暴露接口,外层模块用implementation隐藏实现细节。
为什么整洁架构值得学?
你想想看,一个项目做两年后,最怕什么?怕改不动。整洁架构的好处就是:
- 换框架不伤筋骨:从Retrofit换到Ktor,只用改数据层
- 业务逻辑可测试:用例层不依赖Android,单元测试跑得飞快
- 新人上手快:每层职责清晰,不用猜代码在干嘛
我接手过一个没有分层的老项目,一个UserManager类写了3000行,里面混着网络请求、数据库操作、UI更新。想加个功能,得先花一周读懂这坨代码。嗯,从那以后我坚决推行分层架构。
整洁架构不是银弹,但它能让你在项目变大时,依然睡得安稳。说白了,就是给未来的自己留条活路。
公众号:蓝海资料掘金营,微信deep3321