系列一:架构思想进阶 | 第2篇 分层架构实战:四层拆分、单向依赖与架构防腐

📱 系列一:架构思想进阶 | 第2篇

分层架构实战:四层拆分、单向依赖与架构防腐

阅读说明

本文为深度技术长文,内容涵盖企业级 Android 项目的物理分层、逻辑解耦、Gradle 模块化及自动化架构守护。文中包含大量可运行代码、Mermaid 流程图及工程化配置。建议结合实际项目阅读,并作为团队架构规范参考。


0. 核心结论(Architectural Thesis)

在开始之前,我们需要明确本篇的核心论点:

  1. 分层不是为了"好看",而是为了"隔离变化"。当数据库从 SQLite 换成 Room,或者网络库从 Volley 换成 OkHttp 时,你的 UI 代码应当无感知。
  2. 单向依赖是铁律。下层绝对不能依赖上层,这是防止代码腐烂成"网状结构"的唯一手段。
  3. 工具大于纪律 。仅靠文档约束是无效的,必须通过 Gradle 模块隔离和 ArchUnit 单元测试,在编译期拦截违规行为。

1. 四层架构:企业级的标准答案

Android 项目发展到中大型规模时,传统的 activity/fragment/utils 包结构会彻底失效。我们必须转向业务导向的四层架构

1.1 架构分层示意图
flowchart TB subgraph Layer4 ["第四层:UI 层 (Presentation Layer)"] direction LR P1[Activity / Fragment] P2[Compose UI] P3[ViewModel] P1 ~~~ P2 ~~~ P3 end subgraph Layer3 ["第三层:业务层 (Domain Layer)"] direction LR D1[Use Cases / Interactors] D2[Domain Models] D3[Repository Interfaces] D1 ~~~ D2 ~~~ D3 end subgraph Layer2 ["第二层:数据层 (Data Layer)"] direction LR DL1[Repository Implementations] DL2[Remote Data Sources] DL3[Local Data Sources] DL1 ~~~ DL2 ~~~ DL3 end subgraph Layer1 ["第一层:基础层 (Infrastructure Layer)"] direction LR I1[Network Clients] I2[Databases] I3[Storage] I4[Utils] I1 ~~~ I2 ~~~ I3 ~~~ I4 end Layer4 -->|调用| Layer3 Layer3 -->|调用| Layer2 Layer2 -->|调用| Layer1
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 基础层禁忌清单
  1. 禁止导入 android.widget.* (除了 Context 用于系统服务)。
  2. 禁止持有 Activity/ViewModel 引用
  3. 禁止使用业务相关的常量 (如 "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?
  1. 复用性LoginUseCase 可以被 App、Widget、Watch 共用。
  2. 可测试性:纯 Kotlin 代码,不依赖 Android,秒级单元测试。
  3. 隔离变化 :后端接口变了,只要 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 地狱,以及企业级代码防腐指南。

相关推荐
weiggle1 小时前
第四篇:布局系统——从 Row、Column 到 Box 的声明式布局思维
android
用户86022504674722 小时前
Now in Android 架构深度解析
android
杊页2 小时前
系列一:架构思想进阶 | 第1篇 Android 架构演进实录:从 MVC 的“万能类”到 MVVM 的数据驱动
android
Database_Cool_2 小时前
AI Agent 混合检索选型:阿里云 AnalyticDB MySQL 向量+全文一站式方案
android·adb
2501_916008892 小时前
全面解析常用Web前端开发工具:编辑器、调试工具、性能分析器与框架
android·前端·ios·小程序·uni-app·编辑器·iphone
zhangphil2 小时前
Kotlin协程Flow及管道中的buffer和bufferCapacity
android·kotlin
恋猫de小郭2 小时前
一个 Linux 调度器优化,让 Android 多耗 20% 的电,传音工程师如何发现问题?
android·前端·ios
Kapaseker2 小时前
一个圆屏逼得我好好学习 Compose MeasurePolicy
android·kotlin
__Witheart__2 小时前
RK Android OTA U盘升级指南
android