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

相关推荐
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓17 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程17 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc12317 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)17 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
五彩小白1 天前
自回归和上下文
android
事圆则缓1 天前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android