1. 概述
本文将带你深入理解如何在 Kotlin 项目中应用整洁架构(Clean Architecture)。其核心思想可以用一句话概括:依赖关系只能由外向内,绝不允许反向依赖。
这意味着:
✅ 内层模块(如业务逻辑)不能直接引用外层实现(如数据库、框架)
❌ 外层可以调用内层,但内层绝不能感知外层的存在
这种设计让核心业务逻辑完全独立于外部框架、数据库甚至 UI 技术栈——换言之,你的 Use Case 不应该知道你是用 Spring 还是 Ktor,也不该关心数据存在 MySQL 还是 MongoDB。
💡 踩坑提醒:很多人一开始会把 Repository 接口放在
infrastructure
包里,这是典型的层级倒置!接口属于内层,实现才在外层。
2. 整洁架构的优势
采用 Clean Architecture 带来的收益非常明确,尤其适合中大型项目:
- ✅ 可维护性强:修改某一层不会波及整个系统,避免“改一处,崩一片”
- ✅ 业务逻辑隔离清晰:Use Case 和 Entity 完全脱离框架,新增功能或重构时更有安全感
- ✅ 测试友好:各层高度解耦,单元测试和集成测试可以分层独立进行
- ✅ 技术栈灵活替换:比如从 Ktor 切到 Spring Boot,只需重写适配器,核心逻辑不动
对于长期迭代的项目来说,前期多花点结构设计成本,后期省下的 debug 时间远超投入。
3. 可能遇到的问题
当然,没有银弹。Clean Architecture 也有它的代价:
- ⚠️ 过度设计风险:小项目或 PoC 快速验证场景下,分层反而增加复杂度
- ⚠️ 学习与协作成本高:团队成员需统一理解分层规则,否则容易出现“伪整洁”代码
- ⚠️ 类数量增多:每个 Use Case 一个类、每个接口一个实现,初期看着类很多
📌 建议:中小型项目可先做简化版四层架构(Entity → UseCase → Interface → Adapter),不必追求完全标准。
4. 分层结构详解
典型的 Clean Architecture 分为以下五层,由内到外依次为:
4.1 实体(Entities)
代表领域核心模型,是业务的本质抽象。它不依赖任何框架、数据库 ORM 注解或网络序列化库。
在 Kotlin 中通常使用 data class
表示:
data class User(val id: Long, val username: String, val email: String)
🔍 注意:这个
User
类不应该有任何@Entity
、@Serializable
等注解。那些是外层的事!
4.2 用例(Use Cases)
也叫“交互逻辑”或“应用服务”,封装具体的业务行为。它是连接外部请求与内部实体的桥梁。
关键原则:
- 每个 Use Case 应职责单一
- 通过构造函数注入依赖(通常是接口)
- 不暴露实现细节
示例:创建用户的用例
class CreateUserUseCase(private val userRepository: UserRepository) {
fun execute(username: String, email: String): User {
return userRepository.createUser(username, email)
}
}
💬 小技巧:方法名建议统一为
execute
或invoke
,保持一致性;也可根据语义命名如register()
。
4.3 接口(Interfaces / Protocols)
这里说的“接口”不是 HTTP 接口,而是指内层定义、外层实现的抽象契约,比如 UserRepository
。
它的作用是:
- 隔离核心逻辑与外部依赖
- 支持多实现(内存存储 / DB / Mock)
- 实现依赖反转(DIP)
定义方式如下:
interface UserRepository {
fun createUser(username: String, email: String): User
}
📌 关键点:这个接口必须放在内层包中(如 domain.repository
),而实现类在外层。
4.4 框架层(Frameworks)
这一层负责集成第三方技术栈,比如 Web 框架、数据库驱动等。常见的包括:
- Ktor / Spring Boot 用于构建 HTTP 接口
- Exposed / JPA 处理数据持久化
- Kafka / RabbitMQ 客户端处理消息通信
例如,使用 Ktor 实现一个简单的用户管理路由:
routing {
route("/users") {
get {
call.respond(users)
}
post {
val newUser = call.receive<User>()
users.add(newUser)
call.respond(HttpStatusCode.Created, newUser)
}
get("/{username}") {
val username = call.parameters["username"]
val user = users.find { it.username == username }
if (user != null) {
call.respond(user)
} else {
throw NotFoundException("User not found")
}
}
}
}
⚠️ 注意:这里的 users
是临时内存集合,仅作演示。真实项目应通过 Use Case 调用。
4.5 适配器(Adapters)
适配器是内外层之间的翻译官,分为两类:
输入适配器(Input Adapters)
处理来自外部世界的输入,比如:
- HTTP 请求(Ktor Controller、Spring MVC)
- CLI 命令行调用
- 消息队列监听器
它们的任务是:
- 解析原始输入(JSON、参数)
- 校验数据合法性
- 转换为 Use Case 可接受的格式
- 触发对应 Use Case 执行
典型代表就是 Web 层的路由或 Controller。
输出适配器(Output Adapters)
负责与外部系统交互,常见类型有:
- 数据库访问实现(JPA、MyBatis、Exposed)
- 第三方 API 调用客户端
- 文件系统读写
- 缓存操作(Redis)
下面是一个输出适配器的例子——基于内存的 UserRepository
实现:
class DatabaseUserRepository : UserRepository {
private val users = mutableMapOf<Long, User>()
private var idCounter = 1L
override fun createUser(username: String, email: String): User {
val newUser = User(idCounter++, username, email)
users[newUser.id] = newUser
return newUser
}
}
✅ 正确姿势:
DatabaseUserRepository
实现了内层定义的UserRepository
接口,实现了“外层实现内层契约”。
📌 结构示意:
┌─────────────┐
│ Framework │ ← Ktor, Spring
└─────────────┘
↓
┌─────────────┐
│ Adapter │ ← DatabaseUserRepository
└─────────────┘
↓
┌─────────────┐
│ Interface │ ← UserRepository (interface)
└─────────────┘
↓
┌──────────────────┐
│ Use Case / Entity│ ← CreateUserUseCase, User
└──────────────────┘
5. 总结
本文系统梳理了 Kotlin 项目中实施 Clean Architecture 的关键要素:
- 核心在于依赖方向不可逆
- 分层不是越多越好,关键是职责清晰
- 接口定义在内层,实现在外层
- 通过适配器完成内外数据转换
虽然初期会觉得“绕”,但一旦形成习惯,你会发现代码的可读性、可测性和可扩展性显著提升。特别是在团队协作和长期维护场景下,整洁架构的价值尤为突出。
🎯 最后建议:可以从一个小模块开始试点,逐步推广到整个项目,避免一开始就全面铺开带来的阻力。