Clean Architecture — 基础、Entities层、Use Cases层和Gateways层

作者: IT Sectr 发布日期: 2026-02-17 阅读时间: 10 分钟

Clean Architecture — 由Robert Martin(Uncle Bob)于2012年提出的多层架构,将应用程序划分为独立的层:Domain(Entities、Use Cases)、Data(Repositories、DataSources)和Presentation(ViewModels、Views)。主要原则 — Dependency Rule:依赖向内指向,外层依赖内层,而非反之。Clean Architecture应用于具有高业务逻辑复杂性的移动开发项目。更多内容 — 见《The Clean Architecture》一书。

要点

  • Clean Architecture — 三层:Domain(业务逻辑)、Data(数据)、Presentation(UI),遵循Dependency Rule
  • Dependency Rule — 依赖向内指向,Domain不知道Data和Presentation
  • Use Cases(Interactors)— 业务逻辑场景,每个Use Case — 一个类一个方法
  • Repository Interface — Domain中的数据抽象,实现在Data层
  • 可测试性 — Domain和Use Cases可以在没有Android SDK和iOS UIKit的情况下进行单元测试

Clean Architecture — 多层架构基础

Clean Architecture — 由Robert Martin(Uncle Bob)于2012年制定的架构模式。核心思想 — 将应用程序划分为具有严格依赖规则的层:层内代码不知道外部代码。外层(UI、框架、数据库)— 实现细节。内层(业务逻辑、企业规则)— 应用程序的本质。

Clean Architecture的层在移动开发中:1)Domain — Entities(业务对象)和Use Cases(用例);2)Data — RepositoryImpl(仓库实现)、DataSources(网络、数据库、缓存);3)Presentation — ViewModels、Views(Compose/SwiftUI)。Domain — 最内层,没有依赖关系。Data依赖Domain(实现仓库接口)。Presentation依赖Domain(调用Use Cases,订阅结果)。

包含依赖
DomainEntities、Use Cases、Repository Interfaces无(纯Kotlin/Swift)
DataRepositoryImpl、DataSources(API、DB、Cache)Domain、Retrofit、Room、Ktor
PresentationViewModels、Views、ComposablesDomain、Jetpack、SwiftUI

Dependency Rule — Clean Architecture唯一严格的规则。源代码只能引用自身内部的层或更下层(更靠近中心)。Presentation导入Domain。Domain不导入Data或Presentation。这是通过依赖反转原则(Dependency Inversion Principle)实现的:Domain定义Repository接口,Data实现它。Presentation依赖UseCase抽象,而非具体仓库。

Domain Layer:Entities、Use Cases和Repository Interfaces

Domain — 应用程序中最稳定的层。Entities — 独立于框架的业务对象:User、Product、Order。Use Cases — 具有一个invoke方法(或Kotlin中的operator fun invoke)的类,实现一个场景:GetUserUseCase、PlaceOrderUseCase、CalculateTotalUseCase。Repository Interfaces — 数据访问抽象,在Domain中定义,在Data中实现。Domain不包含Android SDK、iOS UIKit、Retrofit、Room — 仅纯Kotlin或Swift。

swift
// Entity — 业务对象 (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — 数据抽象 (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — 一个场景 (Domain)
final class GetUserUseCase {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func execute(id: Int) async throws -> User {
        return try await repository.getUser(id: id)
    }
}

Use Case — 「一个方法一个类」 — 不是教条,而是实际建议。当Use Case变得更复杂(验证+日志+仓库调用)时,其方法按意义分组:UserUseCase.getUser、UserUseCase.searchUsers、UserUseCase.deleteUser。重要的是 — Use Case不应知道数据来自哪里(网络、数据库、缓存)以及谁在显示它们(Compose、SwiftUI)。在IT Sectr,我们为每个包含业务规则、验证或组合两个来源数据的操作分配一个Use Case。

Domain的纯净性通过在层边界进行DTO映射来实现。Data层接收JSON模型(DTO),将其映射为Domain Entity。Presentation接收Domain Entity,映射为ViewModel(DisplayItem)。Domain Entity从不包含Retrofit、Room、Codable注解 — 这保证了在从Room切换到Realm数据库或从Retrofit替换为Ktor时无需修改该层。

Data Layer:Repository Implementation和DataSources

Data Layer — 在Domain中定义的接口的实现。包含RepositoryImpl(实现UserRepository的类)和DataSources(RemoteDataSource — API、LocalDataSource — 数据库、CacheDataSource — SharedPreferences/NSUserDefaults)。Data层依赖Domain(导入仓库接口和Entities)和框架(Retrofit、Room、Ktor、CoreData)。RepositoryImpl对Domain隐藏数据源 — Use Case不知道数据来自网络还是缓存。

kotlin
// DTO — 网络模型 (Data)
data class UserDto(
    @SerializedName("id") val id: Int,
    @SerializedName("first_name") val firstName: String,
    @SerializedName("last_name") val lastName: String,
    @SerializedName("email") val email: String
)

// RepositoryImpl — 实现 (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // 尝试从缓存获取
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // 如果没有 — 从网络加载
        val dto = remoteDataSource.fetchUser(id)
        val user = dto.toDomain()
        localDataSource.saveUser(user)
        return user
    }

    override suspend fun getUsers(): List<User> {
        return remoteDataSource.fetchAllUsers().map { it.toDomain() }
    }
}

// Mapper — DTO ↔ Domain转换
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Data Layer中的缓存策略:RepositoryImpl首先检查本地存储,如果没有数据 — 从网络加载并保存本地。如果网络不可用 — 返回带有isStale标记的过期数据。Domain中的Use Case不知道此策略 — 通过Repository.getUser(id)接收User。策略的改变(例如每15分钟使缓存失效)不影响Domain和Presentation。

Android中的模块化 — Kotlin Multiplatform允许将Domain移至独立的KMP模块,不依赖Android SDK。Data — 依赖Domain的独立模块。Presentation — 依赖Domain的Android模块。Gradle依赖:domain(pure Kotlin)、data(domain + Retrofit + Room)、app(domain + presentation + Hilt)。这种模块化对大型项目是强制性的 — CI单独构建Domain,Domain的单元测试不需要Android模拟器。

Presentation Layer:ViewModels和Views

Presentation Layer — Clean Architecture的最外层。包含ViewModels(Android)/ ObservableObject(iOS)和Views(Compose/SwiftUI)。ViewModel调用Use Case,接收结果并将其转换为UI状态(State)。View订阅State并显示。Presentation依赖Domain — 导入Use Cases和Entities。Presentation不导入Data Layer — 数据通过Use Case传入,后者内部使用Repository。

Clean Architecture中的ViewModel不包含业务逻辑 — 调用Use Case。如果Use Case返回User,ViewModel将其转换为UserDisplayItem(name、emailFormatted、avatarUrl)— 纯展示模型。Use Case不知道DisplayItem — 返回Entity。这种分离允许在没有UI的情况下测试Use Case,以及在没有UseCase的情况下(通过mock)测试ViewModel。在IT Sectr,我们严格遵守:Use Case — 业务逻辑,ViewModel — 仅展示,View — 仅显示。

kotlin
// Use Case (Domain) — 纯业务逻辑
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — 仅展示
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {

    private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
    val state: StateFlow<UserScreenState> = _state.asStateFlow()

    fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = UserScreenState.Loading
            val user = getUserUseCase(id)
            val displayItem = UserDisplayItem(
                name = user.name,
                email = user.email,
                initials = user.name.split(" ").joinToString("") { it.first().toString() }
            )
            _state.value = UserScreenState.Success(displayItem)
        }
    }
}

data class UserDisplayItem(
    val name: String,
    val email: String,
    val initials: String
)

sealed interface UserScreenState {
    data object Loading : UserScreenState
    data class Success(val displayItem: UserDisplayItem) : UserScreenState
    data class Error(val message: String) : UserScreenState
}

Navigation在Presentation层 — 也是外环的一部分。Clean Architecture不指定导航机制 — 可以是NavController(Compose)、NavigationStack(SwiftUI)、Coordinator(UIKit)或Router(VIPER)。重要的是:导航决策由Presentation做出,但导航不应渗透到Use Case中。Use Case返回结果,ViewModel决定导航到哪个屏幕。在Clean Architecture中,导航是一个可以在不修改Domain的情况下替换的细节。

Clean Architecture在iOS和Android上:代码示例

Android上的Clean Architecture通过Gradle模块实现:domain(pure Kotlin)、data(domain + Retrofit + Room)、presentation(domain + Compose)。文件夹结构:domain/user/User.kt、GetUserUseCase.kt、UserRepository.kt;data/remote/UserRemoteDataSource.kt、local/UserDao.kt、repository/UserRepositoryImpl.kt;presentation/ui/user/UserViewModel.kt、UserScreen.kt。DI(Hilt)连接各层:UserRepositoryImpl绑定到domain模块中的UserRepository接口。

iOS上的Clean Architecture使用SPM或Xcode组而不使用独立模块(由于Xcode的限制)。Domain — 包含不导入UIKit或SwiftUI文件的文件夹。Data — 包含APIClient、CoreDataStack、RepositoryImpl的文件夹。Presentation — 包含ViewModels和SwiftUI Views的文件夹。DI通过构造函数或App中的assembly进行。主要调用 — 通过UseCase.execute()的async/await,并检查MainActor以进行UI更新。

swift
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
    private let apiClient: APIClient

    func fetchUser(id: Int) async throws -> UserDTO {
        return try await apiClient.get("/users/\(id)")
    }
}

// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
    private let remote: UserRemoteDataSource
    private let local: UserLocalDataSource

    func getUser(id: Int) async throws -> User {
        if let cached = try await local.getUser(id) {
            return cached
        }
        let dto = try await remote.fetchUser(id)
        let user = dto.toDomain()
        try await local.saveUser(user)
        return user
    }
}

// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var state: UserScreenState = .loading
    private let getUserUseCase: GetUserUseCase

    func loadUser(id: Int) {
        Task {
            state = .loading
            if let user = try? await getUserUseCase.execute(id: id) {
                state = .success(user)
            } else {
                state = .error("Failed to load")
            }
        }
    }
}

IT Sectr项目中的Clean Architecture — 我们30天以上项目的标准。自2022年起,我们使用带有Kotlin Multiplatform的三层架构用于Android/iOS。Domain — 共享KMP模块,Data — 平台模块(Android上的Retrofit,iOS上的URLSession),Presentation — 原生UI。这使得iOS和Android之间60–80%的业务逻辑代码共享,与两个独立实现相比,开发时间缩短30–40%。

常见问题

Clean Architecture应该有多少层?

最少三层:Domain、Data、Presentation。对于大型项目,会添加Framework(Android SDK/iOS UIKit依赖)和Device(GPS、相机、传感器)。层的数量 — 不是硬性规则,而是便利性问题。重要的是遵守Dependency Rule:依赖向内指向Domain。可以从三层开始,随着项目增长添加层。

Clean Architecture会增加代码量吗?

是的 — 与MVVM相比增加30–50%,因为分离了仓库接口、Use Cases和映射器。对于简单的CRUD应用程序来说,这过于冗余。Clean Architecture适用于具有复杂业务逻辑的项目,其中可测试性和层隔离比开发速度更重要。对于MVP或原型,请使用MVVM — Clean Architecture会减慢启动速度。

Clean Architecture可以与MVI结合吗?

可以,这是常见做法。Use Cases保留在Domain,Presentation使用MVI循环(Intent → Reducer → State)。Data Layer — 相同,Domain — 相同。Presentation中的MVI提供可预测的屏幕状态,Clean Architecture — 业务逻辑隔离。这种组合用于有数十名开发人员的大型项目。

每个数据请求都需要Use Cases吗?

当操作包含业务规则时才需要Use Case:验证、组合两个来源的数据、计算、日志记录、检查访问权限。没有额外逻辑的简单getUser(id)请求可以直接从ViewModel调用Repository。然而,为了架构的一致性,许多团队为Repository的每个公共方法创建Use Case — 这增加了5–10%的代码,但简化了阅读。

如何测试Clean Architecture?

Domain:使用mock Repository进行Use Cases的单元测试 — 纯Kotlin/Swift,无需Android SDK。Data:使用mock/fake DataSource进行RepositoryImpl的集成测试。Presentation:使用mock UseCase进行ViewModel测试。得益于Dependency Rule,每个层都可以隔离测试。在IT Sectr,Domain的覆盖率达到95%,Data — 70–80%,Presentation — 60–70%。

总结

  • Clean Architecture — 三层(Domain、Data、Presentation),Dependency Rule向内
  • Dependency Rule — Domain不知道Data和Presentation,通过接口隔离
  • Domain — Entities、Use Cases、Repository Interfaces — 纯Kotlin/Swift,无框架
  • Data — RepositoryImpl、DataSources(网络、数据库、缓存)— Domain接口的实现
  • Presentation — ViewModels、Views — 仅显示,业务逻辑在Use Cases中
  • 测试 — Domain的单元测试覆盖率达90–95%
  • KMP — 使用Kotlin Multiplatform的Clean Architecture为iOS + Android提供60–80%的共享代码

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读