欢迎光临
我们一直在努力

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

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

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

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


0. 核心结论(Architectural Thesis)

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

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

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

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

    1.1 架构分层示意图

    #mermaid-svg-iMrBKouIkIZL8QO9{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iMrBKouIkIZL8QO9 .error-icon{fill:#552222;}#mermaid-svg-iMrBKouIkIZL8QO9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iMrBKouIkIZL8QO9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iMrBKouIkIZL8QO9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iMrBKouIkIZL8QO9 .marker.cross{stroke:#333333;}#mermaid-svg-iMrBKouIkIZL8QO9 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iMrBKouIkIZL8QO9 p{margin:0;}#mermaid-svg-iMrBKouIkIZL8QO9 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 .cluster-label text{fill:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 .cluster-label span{color:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 .cluster-label span p{background-color:transparent;}#mermaid-svg-iMrBKouIkIZL8QO9 .label text,#mermaid-svg-iMrBKouIkIZL8QO9 span{fill:#333;color:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 .node rect,#mermaid-svg-iMrBKouIkIZL8QO9 .node circle,#mermaid-svg-iMrBKouIkIZL8QO9 .node ellipse,#mermaid-svg-iMrBKouIkIZL8QO9 .node polygon,#mermaid-svg-iMrBKouIkIZL8QO9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iMrBKouIkIZL8QO9 .rough-node .label text,#mermaid-svg-iMrBKouIkIZL8QO9 .node .label text,#mermaid-svg-iMrBKouIkIZL8QO9 .image-shape .label,#mermaid-svg-iMrBKouIkIZL8QO9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-iMrBKouIkIZL8QO9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iMrBKouIkIZL8QO9 .rough-node .label,#mermaid-svg-iMrBKouIkIZL8QO9 .node .label,#mermaid-svg-iMrBKouIkIZL8QO9 .image-shape .label,#mermaid-svg-iMrBKouIkIZL8QO9 .icon-shape .label{text-align:center;}#mermaid-svg-iMrBKouIkIZL8QO9 .node.clickable{cursor:pointer;}#mermaid-svg-iMrBKouIkIZL8QO9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iMrBKouIkIZL8QO9 .arrowheadPath{fill:#333333;}#mermaid-svg-iMrBKouIkIZL8QO9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iMrBKouIkIZL8QO9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iMrBKouIkIZL8QO9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iMrBKouIkIZL8QO9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iMrBKouIkIZL8QO9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iMrBKouIkIZL8QO9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iMrBKouIkIZL8QO9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iMrBKouIkIZL8QO9 .cluster text{fill:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 .cluster span{color:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iMrBKouIkIZL8QO9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iMrBKouIkIZL8QO9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-iMrBKouIkIZL8QO9 .icon-shape,#mermaid-svg-iMrBKouIkIZL8QO9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iMrBKouIkIZL8QO9 .icon-shape p,#mermaid-svg-iMrBKouIkIZL8QO9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iMrBKouIkIZL8QO9 .icon-shape .label rect,#mermaid-svg-iMrBKouIkIZL8QO9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iMrBKouIkIZL8QO9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iMrBKouIkIZL8QO9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iMrBKouIkIZL8QO9 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    调用

    调用

    调用

    第一层:基础层 (Infrastructure Layer)

    Network Clients

    Databases

    Storage

    Utils

    第二层:数据层 (Data Layer)

    Repository Implementations

    Remote Data Sources

    Local Data Sources

    第三层:业务层 (Domain Layer)

    Use Cases / Interactors

    Domain Models

    Repository Interfaces

    第四层:UI 层 (Presentation Layer)

    Activity / Fragment

    Compose UI

    ViewModel

    1.2 各层职责与代码边界
    层级职责允许的操作禁止的操作
    UI 层 (Presentation) 展示数据,接收用户交互。 观察 ViewModel,更新 UI,导航。 直接操作数据库、网络请求、业务逻辑计算。
    业务层 (Domain) 定义核心业务规则。 组合数据、校验逻辑、定义业务实体。 依赖 Android SDK、直接操作 JSON/SQL。
    数据层 (Data) 负责数据的获取、存储和同步。 调用网络 API、读写数据库、缓存策略。 直接操作 UI 控件、持有 Context。
    基础层 (Infrastructure) 提供通用技术能力。 封装网络、日志、文件 IO、加密。 包含任何业务语义(如“订单”、“用户”)。

    2. 基础层(Infrastructure Layer):地基与禁忌

    基础层是整个应用的基石。它的核心特征是:纯技术、无业务、无 Android 依赖(尽可能)。

    2.1 标准代码结构

    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 代码实战:网络层封装

    错误示范(基础层依赖业务):

    // ❌ 错误:基础层不应该知道 "User" 这个业务概念
    class NetworkClient {
    fun login(username: String, password: String): User {
    // …
    }
    }

    正确示范(基础层只提供能力):

    // ✅ 正确:基础层只提供泛型网络能力
    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 标准代码结构

    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 接口(定义业务契约):

    // 位于 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 实现(组合数据源):

    @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 标准代码结构

    com.example.app.domain
    ├── model
    │ └── User.kt # 纯净的业务模型
    ├── repository
    │ └── UserRepository.kt # 接口(与 Data 层接口同名,但属于业务层)
    └── usecase
    └── LoginUseCase.kt # 用例

    4.2 代码实战:UseCase(用例)

    什么是 UseCase?
    一个 UseCase 只做一件事。它封装了业务规则。

    // 业务模型(无 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 标准代码结构

    com.example.app.presentation
    ├── login
    │ ├── LoginActivity.kt
    │ ├── LoginViewModel.kt
    │ └── LoginUiState.kt
    ├── home
    │ ├── HomeActivity.kt
    │ └── HomeViewModel.kt
    └── base
    └── BaseViewModel.kt

    5.2 代码实战:正统 MVVM

    ViewModel(只依赖 UseCase):

    @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(只观察,不决策):

    @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 模块拆分方案

    // settings.gradle.kts
    include(":app")
    include(":presentation")
    include(":domain")
    include(":data")
    include(":infrastructure")

    6.2 依赖关系配置(单向依赖)

    // 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 中配置资源前缀:

    # gradle.properties
    android.resourcePrefix=login_


    7. 架构守护:用 ArchUnit 拦截违规行为

    这是大厂架构师最后的防线。我们用 ArchUnit 写单元测试,谁敢在 ViewModel 里 import android.widget.TextView,直接编译失败。

    7.1 引入 ArchUnit

    // presentation/build.gradle.kts
    dependencies {
    testImplementation("com.tngtech.archunit:archunit-junit5:1.1.0")
    }

    7.2 编写架构测试

    @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 地狱,以及企业级代码防腐指南。

    赞(0)
    未经允许不得转载:171主机测评 » 系列一:架构思想进阶 | 第2篇 分层架构实战:四层拆分、单向依赖与架构防腐
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址