系列一:架构思想进阶 | 第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 地狱,以及企业级代码防腐指南。

相关推荐
海水冷却32 分钟前
Android 群组聊天怎么实现:基于 ZIM 的完整落地指南(附可运行 Demo)
android
三少爷的鞋3 小时前
现代 Android 架构指南:不要再主动设计 Callback 接口
android
TimeFine9 小时前
AI 编码工作流的设计
android·ai编程
骑着蜗牛撵大象32710 小时前
C++20 协程 × 屏障:手写一个 co_await 版 Barrier,让等待不再钉死线程
android·c++20·协程·并发编程·co_await·屏障同步
山东孙燕姿11 小时前
设备台账重构:从一张表到三层模型
android·java·网络·低代码·重构·rxjava
蒸鱼Yuzheng12 小时前
移动端 CPU 调度分析:线程亲和性、大小核迁移与卡顿
android·perfetto·性能分析·线程优化·cpu调度
booboosui12 小时前
安卓唤醒词:12 维 MFCC 加 DTW,阈值 1.8
android
传奇开心果12 小时前
Compose Multiplatform 共享边界实战:架构契约、互操作同步与可回退设计
android·kotlin
事圆则缓13 小时前
从开机到首帧:Android 系统与应用启动流程源码分析
android
AI吃大瓜13 小时前
指针压力表读数4:Android实现指针压力表读数(可实时检测)
android·指针读数·指针压力表·压力表·压力表读数