📱 系列一:架构思想进阶 | 第2篇
分层架构实战:四层拆分、单向依赖与架构防腐
阅读说明
本文为深度技术长文,内容涵盖企业级 Android 项目的物理分层、逻辑解耦、Gradle 模块化及自动化架构守护。文中包含大量可运行代码、Mermaid 流程图及工程化配置。建议结合实际项目阅读,并作为团队架构规范参考。
0. 核心结论(Architectural Thesis)
在开始之前,我们需要明确本篇的核心论点:
- 分层不是为了"好看",而是为了"隔离变化"。当数据库从 SQLite 换成 Room,或者网络库从 Volley 换成 OkHttp 时,你的 UI 代码应当无感知。
- 单向依赖是铁律。下层绝对不能依赖上层,这是防止代码腐烂成"网状结构"的唯一手段。
- 工具大于纪律 。仅靠文档约束是无效的,必须通过 Gradle 模块隔离和 ArchUnit 单元测试,在编译期拦截违规行为。
1. 四层架构:企业级的标准答案
Android 项目发展到中大型规模时,传统的 activity/fragment/utils 包结构会彻底失效。我们必须转向业务导向的四层架构。
1.1 架构分层示意图
1.2 各层职责与代码边界
| 层级 | 职责 | 允许的操作 | 禁止的操作 |
|---|---|---|---|
| UI 层 (Presentation) | 展示数据,接收用户交互。 | 观察 ViewModel,更新 UI,导航。 | 直接操作数据库、网络请求、业务逻辑计算。 |
| 业务层 (Domain) | 定义核心业务规则。 | 组合数据、校验逻辑、定义业务实体。 | 依赖 Android SDK、直接操作 JSON/SQL。 |
| 数据层 (Data) | 负责数据的获取、存储和同步。 | 调用网络 API、读写数据库、缓存策略。 | 直接操作 UI 控件、持有 Context。 |
| 基础层 (Infrastructure) | 提供通用技术能力。 | 封装网络、日志、文件 IO、加密。 | 包含任何业务语义(如"订单"、"用户")。 |
2. 基础层(Infrastructure Layer):地基与禁忌
基础层是整个应用的基石。它的核心特征是:纯技术、无业务、无 Android 依赖(尽可能)。
2.1 标准代码结构
bash
com.example.app.infrastructure
├── network
│ ├── NetworkClient.kt # OkHttp/Retrofit 封装
│ └── ApiService.kt # 接口定义
├── storage
│ ├── LocalStorage.kt # SP/DataStore 封装
│ └── FileManager.kt
├── database
│ ├── AppDatabase.kt # Room Database
│ └── BaseDao.kt
└── utils
├── StringUtils.kt
└── LogUtils.kt
2.2 代码实战:网络层封装
错误示范(基础层依赖业务):
kotlin
// ❌ 错误:基础层不应该知道 "User" 这个业务概念
class NetworkClient {
fun login(username: String, password: String): User {
// ...
}
}
正确示范(基础层只提供能力):
kotlin
// ✅ 正确:基础层只提供泛型网络能力
object NetworkClient {
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.connectTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
// 接口定义
interface ApiService {
@POST("auth/login")
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
}
2.3 基础层禁忌清单
- 禁止导入
android.widget.*(除了Context用于系统服务)。 - 禁止持有 Activity/ViewModel 引用。
- 禁止使用业务相关的常量 (如
"USER_TOKEN_KEY")。
3. 数据层(Data Layer):数据仓库模式
数据层是业务层与基础层之间的桥梁。它采用 Repository 模式,对外屏蔽数据来源(网络、数据库、缓存)。
3.1 标准代码结构
bash
com.example.app.data
├── repository
│ ├── impl
│ │ └── UserRepositoryImpl.kt
│ └── UserRepository.kt # 接口定义(给业务层用)
├── datasource
│ ├── remote
│ │ └── UserRemoteDataSource.kt
│ └── local
│ └── UserLocalDataSource.kt
└── model
├── dto
│ └── UserDto.kt # 网络传输对象
└── entity
└── UserEntity.kt # 数据库实体
3.2 代码实战:Repository 实现
Repository 接口(定义业务契约):
kotlin
// 位于 data 层,但接口属于业务契约
interface UserRepository {
suspend fun login(account: String, password: String): Result<User>
suspend fun getUserInfo(userId: String): Flow<User>
suspend fun logout(): Result<Unit>
}
Repository 实现(组合数据源):
kotlin
@Singleton
class UserRepositoryImpl @Inject constructor(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun login(account: String, password: String): Result<User> {
return runCatching {
// 1. 调用远程接口
val response = remoteDataSource.login(account, password)
// 2. 保存到本地
localDataSource.saveUser(response.toEntity())
// 3. 返回业务模型
response.toDomain()
}
}
override suspend fun getUserInfo(userId: String): Flow<User> {
// 策略:先读缓存,再读网络,再更新缓存
return flow {
val local = localDataSource.getUser(userId)
emit(local.toDomain())
val remote = remoteDataSource.fetchUser(userId)
localDataSource.saveUser(remote.toEntity())
emit(remote.toDomain())
}
}
}
3.3 数据层转换规则
| 数据类型 | 所在层 | 用途 |
|---|---|---|
| DTO (Data Transfer Object) | Data 层 | 网络传输,字段名必须与后端一致。 |
| Entity | Data 层 | 数据库存储,包含 @PrimaryKey。 |
| Domain Model | Domain 层 | 业务核心,纯净 Kotlin 对象,无注解。 |
4. 业务层(Domain Layer):核心大脑
业务层是架构的心脏。它定义了**"App 到底是做什么的"**。很多 Android 项目缺失这一层,导致逻辑散落在各处。
4.1 标准代码结构
bash
com.example.app.domain
├── model
│ └── User.kt # 纯净的业务模型
├── repository
│ └── UserRepository.kt # 接口(与 Data 层接口同名,但属于业务层)
└── usecase
└── LoginUseCase.kt # 用例
4.2 代码实战:UseCase(用例)
什么是 UseCase?
一个 UseCase 只做一件事。它封装了业务规则。
kotlin
// 业务模型(无 Android 依赖)
data class User(
val id: String,
val name: String,
val isVip: Boolean
)
// 用例:登录
class LoginUseCase @Inject constructor(
private val userRepository: UserRepository // 依赖抽象接口
) {
suspend operator fun invoke(account: String, password: String): Result<LoginResult> {
// 1. 业务校验(这是核心)
if (account.isBlank()) {
return Result.failure(IllegalArgumentException("账号不能为空"))
}
if (password.length < 6) {
return Result.failure(IllegalArgumentException("密码长度不足"))
}
// 2. 风控校验(举例)
if (isRiskAccount(account)) {
return Result.failure(SecurityException("账户存在风险"))
}
// 3. 调用仓库
return userRepository.login(account, password)
}
private fun isRiskAccount(account: String): Boolean {
// 风控逻辑
return false
}
}
4.3 为什么必须有一层 Domain?
- 复用性 :
LoginUseCase可以被 App、Widget、Watch 共用。 - 可测试性:纯 Kotlin 代码,不依赖 Android,秒级单元测试。
- 隔离变化 :后端接口变了,只要
UserRepositoryImpl改,业务层LoginUseCase不动。
5. UI 层(Presentation Layer):展示与响应
UI 层是架构的最顶层。它只做两件事:把 State 渲染成 UI ,把用户操作变成 Intent。
5.1 标准代码结构
arduino
com.example.app.presentation
├── login
│ ├── LoginActivity.kt
│ ├── LoginViewModel.kt
│ └── LoginUiState.kt
├── home
│ ├── HomeActivity.kt
│ └── HomeViewModel.kt
└── base
└── BaseViewModel.kt
5.2 代码实战:正统 MVVM
ViewModel(只依赖 UseCase):
kotlin
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginUseCase: LoginUseCase // 依赖业务层
) : ViewModel() {
// 分散的 State(正统 MVVM,非 MVI)
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> = _isLoading
private val _loginResult = SingleLiveEvent<Boolean>()
val loginResult: LiveData<Boolean> = _loginResult
private val _error = SingleLiveEvent<String>()
val error: LiveData<String> = _error
fun login(account: String, password: String) {
_isLoading.value = true
viewModelScope.launch {
loginUseCase(account, password)
.onSuccess {
_isLoading.value = false
_loginResult.value = true
}
.onFailure {
_isLoading.value = false
_error.value = it.message
}
}
}
}
Activity(只观察,不决策):
kotlin
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
// 观察 State
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.isVisible = isLoading
binding.btnLogin.isEnabled = !isLoading
}
viewModel.loginResult.observe(this) { success ->
if (success) {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
}
viewModel.error.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
// 发送事件
binding.btnLogin.setOnClickListener {
viewModel.login(
binding.etAccount.text.toString(),
binding.etPassword.text.toString()
)
}
}
}
6. Gradle 模块化:物理层面的强制隔离
仅仅靠包名约定是不够的。我们必须使用 Gradle Module 来物理隔离。
6.1 模块拆分方案
gradle
// settings.gradle.kts
include(":app")
include(":presentation")
include(":domain")
include(":data")
include(":infrastructure")
6.2 依赖关系配置(单向依赖)
gradle
// app/build.gradle.kts
dependencies {
implementation(project(":presentation"))
}
// presentation/build.gradle.kts
dependencies {
implementation(project(":domain"))
// 注意:presentation 不能依赖 data 或 infrastructure
}
// domain/build.gradle.kts
dependencies {
// 业务层只依赖基础工具(如果有)
implementation(project(":infrastructure"))
}
// data/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation(project(":infrastructure"))
}
// infrastructure/build.gradle.kts
dependencies {
// 无上层依赖
}
6.3 资源隔离
为了防止资源冲突,必须在 gradle.properties 中配置资源前缀:
properties
# gradle.properties
android.resourcePrefix=login_
7. 架构守护:用 ArchUnit 拦截违规行为
这是大厂架构师最后的防线。我们用 ArchUnit 写单元测试,谁敢在 ViewModel 里 import android.widget.TextView,直接编译失败。
7.1 引入 ArchUnit
gradle
// presentation/build.gradle.kts
dependencies {
testImplementation("com.tngtech.archunit:archunit-junit5:1.1.0")
}
7.2 编写架构测试
kotlin
@AnalyzeClasses(packages = ["com.example.app"])
class ArchitectureTest {
@ArchTest
fun `presentation layer should not depend on data layer`(classes: JavaClasses) {
val rule = noClasses()
.that().resideInAPackage("..presentation..")
.should().dependOnClassesThat().resideInAPackage("..data..")
rule.check(classes)
}
@ArchTest
fun `domain layer should not depend on presentation or data layer`(classes: JavaClasses) {
val rule = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("..presentation..", "..data..")
rule.check(classes)
}
@ArchTest
fun `viewmodels should not access android widgets`(classes: JavaClasses) {
val rule = noFields()
.ofTypes("android.widget.Button", "android.widget.TextView")
.should().beDeclaredInClassesThat().haveSimpleNameEndingWith("ViewModel")
rule.check(classes)
}
@ArchTest
fun `use cases should be in domain layer`(classes: JavaClasses) {
val rule = classes()
.that().haveSimpleNameEndingWith("UseCase")
.should().resideInAPackage("..domain.usecase..")
rule.check(classes)
}
}
8. 总结与检查清单
至此,一个具备 物理隔离、逻辑单向、自动守护 的四层架构已经搭建完毕。
请在项目评审时使用以下清单:
- UI 层:ViewModel 是否只依赖 UseCase?是否有业务逻辑?
- 业务层:是否纯 Kotlin?是否定义了 Repository 接口?
- 数据层:Repository 是否实现了业务层接口?是否处理了缓存策略?
- 基础层:是否无业务语义?
- Gradle:是否存在循环依赖?
- ArchUnit:是否所有测试通过?
下一篇预告 :
系列一:架构思想进阶 | 第3篇:SOLID 原则实战 ------ 如何用设计模式干掉 if-else 地狱,以及企业级代码防腐指南。