clean-architecture-ios

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clean Architecture iOS — Expert Decisions

iOS平台Clean Architecture——权威决策指南

Expert decision frameworks for Clean Architecture choices. Claude knows the layers — this skill provides judgment calls for boundary decisions and pragmatic trade-offs.

针对Clean Architecture选型的权威决策框架。Claude精通各层设计——本技能为边界决策和实用性权衡提供判断依据。

Decision Trees

决策树

When Clean Architecture Is Worth It

何时值得采用Clean Architecture

Is this a side project or prototype?
├─ YES → Skip Clean Architecture (YAGNI)
│  └─ Simple MVVM with services is fine
└─ NO → How many data sources?
   ├─ 1 (just API) → Lightweight Clean Architecture
   │  └─ Skip local data source, repository = API wrapper
   └─ Multiple (API + cache + local DB)
      └─ How long will codebase live?
         ├─ < 1 year → Consider simpler approach
         └─ > 1 year → Full Clean Architecture
            └─ Team size > 2? → Strongly recommended
Clean Architecture wins: Apps with complex business logic, multiple data sources, long maintenance lifetime, or teams > 3 developers.
Clean Architecture is overkill: Prototypes, simple apps with single API, short-lived projects, solo developers who know the whole codebase.
这是副业项目或原型吗?
├─ 是 → 跳过Clean Architecture(YAGNI原则)
│  └─ 简单的MVVM+服务架构即可满足需求
└─ 否 → 有多少种数据源?
   ├─ 1种(仅API) → 轻量级Clean Architecture
   │  └─ 跳过本地数据源,Repository直接作为API的封装层
   └─ 多种(API + 缓存 + 本地数据库)
      └─ 代码库的预期生命周期是多久?
         ├─ < 1年 → 考虑更简单的架构方案
         └─ > 1年 → 完整Clean Architecture
            └─ 团队规模>2人? → 强烈推荐采用
Clean Architecture适用场景:业务逻辑复杂、多数据源、长期维护、团队规模超过3人的应用。
Clean Architecture属于过度设计的场景:原型项目、仅单API的简单应用、短期项目、熟悉整个代码库的独立开发者。

Where Does This Code Belong?

代码归属判断

Does it know about UIKit/SwiftUI?
├─ YES → Presentation Layer
│  └─ Views, ViewModels, Coordinators
└─ NO → Does it know about network/database specifics?
   ├─ YES → Data Layer
   │  └─ Repositories (impl), DataSources, DTOs, Mappers
   └─ NO → Is it a business rule or core model?
      ├─ YES → Domain Layer
      │  └─ Entities, UseCases, Repository protocols
      └─ NO → Reconsider if it's needed
代码是否依赖UIKit/SwiftUI?
├─ 是 → 表现层(Presentation Layer)
│  └─ 视图、ViewModel、Coordinator
└─ 否 → 代码是否涉及网络/数据库细节?
   ├─ 是 → 数据层(Data Layer)
   │  └─ Repository(实现类)、DataSource、DTO、Mapper
   └─ 否 → 是业务规则或核心模型吗?
      ├─ 是 → 领域层(Domain Layer)
      │  └─ Entity、UseCase、Repository协议
      └─ 否 → 重新评估该代码是否有存在必要

UseCase Granularity

UseCase粒度设计

Is this operation a single business action?
├─ YES → One UseCase per operation
│  Example: CreateOrderUseCase, GetUserUseCase
└─ NO → Does it combine multiple actions?
   ├─ YES → Can actions be reused independently?
   │  ├─ YES → Separate UseCases, compose in ViewModel
   │  └─ NO → Single UseCase with clear naming
   └─ NO → Is it just CRUD?
      ├─ YES → Consider skipping UseCase
      │  └─ ViewModel → Repository directly is OK for simple CRUD
      └─ NO → Review the operation's purpose
The trap: Creating UseCases for every operation. If it's just
repository.get(id:)
pass-through, skip the UseCase.

该操作是单一业务动作吗?
├─ 是 → 每个操作对应一个UseCase
│  示例:CreateOrderUseCase、GetUserUseCase
└─ 否 → 是否包含多个动作的组合?
   ├─ 是 → 这些动作是否可独立复用?
   │  ├─ 是 → 拆分为独立UseCase,在ViewModel中组合使用
   │  └─ 否 → 单个UseCase,命名需清晰明确
   └─ 否 → 是否仅为CRUD操作?
      ├─ 是 → 考虑跳过UseCase
      │  └─ 简单CRUD场景下,ViewModel可直接调用Repository
      └─ 否 → 重新审视该操作的目的
常见陷阱:为每个操作都创建UseCase。如果只是简单的
repository.get(id:)
透传调用,无需使用UseCase。

NEVER Do

绝对禁忌

Dependency Rule Violations

依赖规则违规

NEVER import outer layers in inner layers:
swift
// ❌ Domain importing Data layer
// Domain/UseCases/GetUserUseCase.swift
import Alamofire  // Data layer framework!
import CoreData   // Data layer framework!

// ❌ Domain importing Presentation layer
import SwiftUI    // Presentation framework!

// ✅ Domain has NO framework imports (except Foundation)
import Foundation
NEVER let Domain know about DTOs:
swift
// ❌ Repository protocol returns DTO
protocol UserRepositoryProtocol {
    func getUser(id: String) async throws -> UserDTO  // Data layer type!
}

// ✅ Repository protocol returns Entity
protocol UserRepositoryProtocol {
    func getUser(id: String) async throws -> User  // Domain type
}
NEVER put business logic in Repository:
swift
// ❌ Business validation in Repository
final class UserRepository: UserRepositoryProtocol {
    func updateUser(_ user: User) async throws -> User {
        // Business rule leaked into Data layer!
        guard user.email.contains("@") else {
            throw ValidationError.invalidEmail
        }
        return try await remoteDataSource.update(user)
    }
}

// ✅ Business logic in UseCase
final class UpdateUserUseCase {
    func execute(user: User) async throws -> User {
        guard user.email.contains("@") else {
            throw DomainError.validation("Invalid email")
        }
        return try await repository.updateUser(user)
    }
}
绝对禁止在内层中导入外层框架:
swift
// ❌ 领域层导入数据层框架
// Domain/UseCases/GetUserUseCase.swift
import Alamofire  // 数据层框架!
import CoreData   // 数据层框架!

// ❌ 领域层导入表现层框架
import SwiftUI    // 表现层框架!

// ✅ 领域层仅导入Foundation(无其他框架依赖)
import Foundation
绝对禁止领域层知晓DTO的存在:
swift
// ❌ Repository协议返回DTO
protocol UserRepositoryProtocol {
    func getUser(id: String) async throws -> UserDTO  // 数据层类型!
}

// ✅ Repository协议返回Entity
protocol UserRepositoryProtocol {
    func getUser(id: String) async throws -> User  // 领域层类型
}
绝对禁止在Repository中编写业务逻辑:
swift
// ❌ Repository中包含业务验证逻辑
final class UserRepository: UserRepositoryProtocol {
    func updateUser(_ user: User) async throws -> User {
        // 业务规则泄露到数据层!
        guard user.email.contains("@") else {
            throw ValidationError.invalidEmail
        }
        return try await remoteDataSource.update(user)
    }
}

// ✅ 业务逻辑应放在UseCase中
final class UpdateUserUseCase {
    func execute(user: User) async throws -> User {
        guard user.email.contains("@") else {
            throw DomainError.validation("Invalid email")
        }
        return try await repository.updateUser(user)
    }
}

Entity Anti-Patterns

Entity反模式

NEVER add framework dependencies to Entities:
swift
// ❌ Entity with Codable for JSON
struct User: Codable {  // Codable couples to serialization format
    let id: String
    let createdAt: Date  // Will have JSON parsing issues
}

// ✅ Pure Entity, DTOs handle serialization
struct User: Identifiable, Equatable {
    let id: String
    let createdAt: Date
}

// Data layer handles Codable
struct UserDTO: Codable {
    let id: String
    let created_at: String  // API format
}
NEVER put computed properties that need external data in Entities:
swift
// ❌ Entity needs external service
struct Order {
    let items: [OrderItem]

    var totalWithTax: Decimal {
        // Where does tax rate come from? External dependency!
        total * TaxService.currentRate
    }
}

// ✅ Calculation in UseCase
final class CalculateOrderTotalUseCase {
    private let taxService: TaxServiceProtocol

    func execute(order: Order) -> Decimal {
        order.total * taxService.currentRate
    }
}
绝对禁止为Entity添加框架依赖:
swift
// ❌ Entity实现Codable用于JSON序列化
struct User: Codable {  // Codable会与序列化格式耦合
    let id: String
    let createdAt: Date  // 会出现JSON解析问题
}

// ✅ 纯Entity,由DTO处理序列化逻辑
struct User: Identifiable, Equatable {
    let id: String
    let createdAt: Date
}

// 数据层处理Codable实现
struct UserDTO: Codable {
    let id: String
    let created_at: String  // API返回格式
}
绝对禁止在Entity中添加需要外部数据的计算属性:
swift
// ❌ Entity依赖外部服务
struct Order {
    let items: [OrderItem]

    var totalWithTax: Decimal {
        // 税率从哪里获取?外部依赖!
        total * TaxService.currentRate
    }
}

// ✅ 计算逻辑放在UseCase中
final class CalculateOrderTotalUseCase {
    private let taxService: TaxServiceProtocol

    func execute(order: Order) -> Decimal {
        order.total * taxService.currentRate
    }
}

Mapper Anti-Patterns

Mapper反模式

NEVER put Mappers in Domain layer:
swift
// ❌ Domain knows about mapping
// Domain/Mappers/UserMapper.swift  — WRONG LOCATION!

// ✅ Mappers live in Data layer
// Data/Mappers/UserMapper.swift
NEVER map in Repository if domain logic is needed:
swift
// ❌ Silent default in mapper
enum ProductMapper {
    static func toDomain(_ dto: ProductDTO) -> Product {
        Product(
            currency: Product.Currency(rawValue: dto.currency) ?? .usd  // Silent default!
        )
    }
}

// ✅ Throw on invalid data, let UseCase handle
enum ProductMapper {
    static func toDomain(_ dto: ProductDTO) throws -> Product {
        guard let currency = Product.Currency(rawValue: dto.currency) else {
            throw MappingError.invalidCurrency(dto.currency)
        }
        return Product(currency: currency)
    }
}

绝对禁止将Mapper放在领域层:
swift
// ❌ 领域层包含映射逻辑
// Domain/Mappers/UserMapper.swift  — 错误位置!

// ✅ Mapper应放在数据层
// Data/Mappers/UserMapper.swift
绝对禁止如果涉及领域逻辑仍在Repository中进行映射:
swift
// ❌ 映射中使用静默默认值
enum ProductMapper {
    static func toDomain(_ dto: ProductDTO) -> Product {
        Product(
            currency: Product.Currency(rawValue: dto.currency) ?? .usd  // 静默默认值!
        )
    }
}

// ✅ 无效数据时抛出异常,由UseCase处理
enum ProductMapper {
    static func toDomain(_ dto: ProductDTO) throws -> Product {
        guard let currency = Product.Currency(rawValue: dto.currency) else {
            throw MappingError.invalidCurrency(dto.currency)
        }
        return Product(currency: currency)
    }
}

Pragmatic Patterns

实用模式

When to Skip the UseCase

何时可跳过UseCase

swift
// ✅ Simple CRUD — ViewModel → Repository is fine
@MainActor
final class UserListViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    func loadUsers() async {
        // Direct repository call for simple fetch
        users = try? await repository.getUsers()
    }
}

// ✅ UseCase needed — business logic involved
final class PlaceOrderUseCase {
    func execute(cart: Cart) async throws -> Order {
        // Validate stock
        // Calculate totals
        // Apply discounts
        // Create order
        // Notify inventory
        // Return order
    }
}
Rule: No business logic? Skip UseCase. Any validation, transformation, or orchestration? Create UseCase.
swift
// ✅ 简单CRUD场景 — ViewModel直接调用Repository即可
@MainActor
final class UserListViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    func loadUsers() async {
        // 简单获取操作直接调用Repository
        users = try? await repository.getUsers()
    }
}

// ✅ 涉及业务逻辑时必须使用UseCase
final class PlaceOrderUseCase {
    func execute(cart: Cart) async throws -> Order {
        // 校验库存
        // 计算总价
        // 应用折扣
        // 创建订单
        // 通知库存系统
        // 返回订单
    }
}
规则:无业务逻辑?跳过UseCase。涉及任何校验、转换或编排逻辑?创建UseCase。

Repository Caching Strategy

Repository缓存策略

swift
final class UserRepository: UserRepositoryProtocol {
    func getUser(id: String) async throws -> User {
        // Strategy 1: Cache-first (offline-capable)
        if let cached = try? await localDataSource.getUser(id: id) {
            // Return cached, refresh in background
            Task { try? await refreshUser(id: id) }
            return UserMapper.toDomain(cached)
        }

        // Strategy 2: Network-first (always fresh)
        let dto = try await remoteDataSource.fetchUser(id: id)
        try? await localDataSource.save(dto)  // Cache for offline
        return UserMapper.toDomain(dto)
    }
}
swift
final class UserRepository: UserRepositoryProtocol {
    func getUser(id: String) async throws -> User {
        // 策略1:缓存优先(支持离线)
        if let cached = try? await localDataSource.getUser(id: id) {
            // 返回缓存数据,后台异步刷新
            Task { try? await refreshUser(id: id) }
            return UserMapper.toDomain(cached)
        }

        // 策略2:网络优先(始终获取最新数据)
        let dto = try await remoteDataSource.fetchUser(id: id)
        try? await localDataSource.save(dto)  // 缓存以支持离线
        return UserMapper.toDomain(dto)
    }
}

Minimal DI Container

极简依赖注入容器

swift
// For small-medium apps, simple factory is enough
@MainActor
final class Container {
    static let shared = Container()

    // Lazy initialization — created on first use
    lazy var networkClient = NetworkClient()
    lazy var userRepository: UserRepositoryProtocol = UserRepository(
        remote: UserRemoteDataSource(client: networkClient),
        local: UserLocalDataSource()
    )

    // Factory methods for UseCases
    func makeGetUserUseCase() -> GetUserUseCaseProtocol {
        GetUserUseCase(repository: userRepository)
    }

    // Factory methods for ViewModels
    func makeUserProfileViewModel() -> UserProfileViewModel {
        UserProfileViewModel(getUser: makeGetUserUseCase())
    }
}

swift
// 中小型应用,简单工厂模式足够
@MainActor
final class Container {
    static let shared = Container()

    // 懒加载 — 首次使用时创建
    lazy var networkClient = NetworkClient()
    lazy var userRepository: UserRepositoryProtocol = UserRepository(
        remote: UserRemoteDataSource(client: networkClient),
        local: UserLocalDataSource()
    )

    // UseCase工厂方法
    func makeGetUserUseCase() -> GetUserUseCaseProtocol {
        GetUserUseCase(repository: userRepository)
    }

    // ViewModel工厂方法
    func makeUserProfileViewModel() -> UserProfileViewModel {
        UserProfileViewModel(getUser: makeGetUserUseCase())
    }
}

Layer Reference

层级参考

Dependency Direction

依赖方向

Presentation → Domain ← Data

✅ Presentation depends on Domain (imports UseCases, Entities)
✅ Data depends on Domain (implements Repository protocols)
❌ Domain depends on nothing (no imports from other layers)
Presentation → Domain ← Data

✅ 表现层依赖领域层(导入UseCases、Entities)
✅ 数据层依赖领域层(实现Repository协议)
❌ 领域层无任何依赖(不导入其他层级的内容)

What Goes Where

内容归属

LayerContainsDoes NOT Contain
DomainEntities, UseCases, Repository protocols, Domain errorsUIKit, SwiftUI, Codable DTOs, Network code
DataRepository impl, DataSources, DTOs, Mappers, NetworkUI code, Business rules, UseCases
PresentationViews, ViewModels, Coordinators, UI componentsNetwork code, Database code, DTOs
层级包含内容不包含内容
DomainEntities、UseCases、Repository协议、领域错误UIKit、SwiftUI、Codable DTOs、网络代码
DataRepository实现、DataSource、DTOs、Mappers、网络相关UI代码、业务逻辑、UseCases
Presentation视图、ViewModels、Coordinators、UI组件网络代码、数据库代码、DTOs

Protocol Placement

协议位置

ProtocolLives InImplemented By
UserRepositoryProtocol
DomainData (UserRepository)
UserRemoteDataSourceProtocol
DataData (UserRemoteDataSource)
GetUserUseCaseProtocol
DomainDomain (GetUserUseCase)

协议所属层级实现方
UserRepositoryProtocol
DomainData(UserRepository)
UserRemoteDataSourceProtocol
DataData(UserRemoteDataSource)
GetUserUseCaseProtocol
DomainDomain(GetUserUseCase)

Testing Strategy

测试策略

What to Test Where

各层级测试重点

LayerTest FocusMock
Domain (UseCases)Business logic, validation, orchestrationRepository protocols
Data (Repositories)Coordination, caching, error mappingDataSource protocols
Presentation (ViewModels)State changes, user actionsUseCase protocols
swift
// UseCase test — mock Repository
func test_createOrder_validatesStock() async throws {
    mockProductRepo.stubbedProduct = Product(inStock: false)

    await XCTAssertThrowsError(
        try await sut.execute(items: [item])
    ) { error in
        XCTAssertEqual(error as? DomainError, .businessRule("Out of stock"))
    }
}

// ViewModel test — mock UseCase
func test_loadUser_updatesState() async {
    mockGetUserUseCase.stubbedUser = User(name: "John")

    await sut.loadUser(id: "123")

    XCTAssertEqual(sut.user?.name, "John")
    XCTAssertFalse(sut.isLoading)
}

层级测试重点模拟对象(Mock)
Domain(UseCases)业务逻辑、校验、编排Repository协议
Data(Repositories)协调逻辑、缓存、错误映射DataSource协议
Presentation(ViewModels)状态变化、用户交互UseCase协议
swift
// UseCase测试 — 模拟Repository
func test_createOrder_validatesStock() async throws {
    mockProductRepo.stubbedProduct = Product(inStock: false)

    await XCTAssertThrowsError(
        try await sut.execute(items: [item])
    ) { error in
        XCTAssertEqual(error as? DomainError, .businessRule("Out of stock"))
    }
}

// ViewModel测试 — 模拟UseCase
func test_loadUser_updatesState() async {
    mockGetUserUseCase.stubbedUser = User(name: "John")

    await sut.loadUser(id: "123")

    XCTAssertEqual(sut.user?.name, "John")
    XCTAssertFalse(sut.isLoading)
}

Quick Reference

快速参考

Clean Architecture Checklist

Clean Architecture检查清单

  • Domain layer has zero framework imports (except Foundation)
  • Entities are pure structs with no Codable
  • Repository protocols live in Domain
  • Repository implementations live in Data
  • DTOs and Mappers live in Data
  • UseCases contain business logic, not pass-through
  • ViewModels depend on UseCase protocols, not concrete classes
  • No circular dependencies between layers
  • Domain层无任何框架导入(仅Foundation除外)
  • Entities为纯结构体,未实现Codable
  • Repository协议位于Domain层
  • Repository实现类位于Data层
  • DTOs和Mappers位于Data层
  • UseCases包含业务逻辑,而非简单透传
  • ViewModels依赖UseCase协议,而非具体实现类
  • 各层级之间无循环依赖

Red Flags

危险信号

SmellProblemFix
import UIKit
in Domain
Layer violationMove to Presentation
UseCase just calls
repo.get()
Unnecessary abstractionViewModel → Repo directly
DTO in DomainLayer violationKeep DTOs in Data
Business logic in RepositoryWrong layerMove to UseCase
ViewModel imports NetworkClientSkipped layersUse Repository
代码异味问题修复方案
Domain层中
import UIKit
层级违规移至Presentation层
UseCase仅调用
repo.get()
不必要的抽象ViewModel直接调用Repository
Domain层中出现DTO层级违规将DTO限制在Data层
Repository中包含业务逻辑层级错误移至UseCase
ViewModel导入NetworkClient跳过了中间层级通过Repository进行调用