clean-architecture-ios
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClean 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 recommendedClean 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 purposeThe trap: Creating UseCases for every operation. If it's just pass-through, skip the UseCase.
repository.get(id:)该操作是单一业务动作吗?
├─ 是 → 每个操作对应一个UseCase
│ 示例:CreateOrderUseCase、GetUserUseCase
│
└─ 否 → 是否包含多个动作的组合?
├─ 是 → 这些动作是否可独立复用?
│ ├─ 是 → 拆分为独立UseCase,在ViewModel中组合使用
│ └─ 否 → 单个UseCase,命名需清晰明确
│
└─ 否 → 是否仅为CRUD操作?
├─ 是 → 考虑跳过UseCase
│ └─ 简单CRUD场景下,ViewModel可直接调用Repository
│
└─ 否 → 重新审视该操作的目的常见陷阱:为每个操作都创建UseCase。如果只是简单的透传调用,无需使用UseCase。
repository.get(id:)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 FoundationNEVER 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.swiftNEVER 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
内容归属
| Layer | Contains | Does NOT Contain |
|---|---|---|
| Domain | Entities, UseCases, Repository protocols, Domain errors | UIKit, SwiftUI, Codable DTOs, Network code |
| Data | Repository impl, DataSources, DTOs, Mappers, Network | UI code, Business rules, UseCases |
| Presentation | Views, ViewModels, Coordinators, UI components | Network code, Database code, DTOs |
| 层级 | 包含内容 | 不包含内容 |
|---|---|---|
| Domain | Entities、UseCases、Repository协议、领域错误 | UIKit、SwiftUI、Codable DTOs、网络代码 |
| Data | Repository实现、DataSource、DTOs、Mappers、网络相关 | UI代码、业务逻辑、UseCases |
| Presentation | 视图、ViewModels、Coordinators、UI组件 | 网络代码、数据库代码、DTOs |
Protocol Placement
协议位置
| Protocol | Lives In | Implemented By |
|---|---|---|
| Domain | Data (UserRepository) |
| Data | Data (UserRemoteDataSource) |
| Domain | Domain (GetUserUseCase) |
| 协议 | 所属层级 | 实现方 |
|---|---|---|
| Domain | Data(UserRepository) |
| Data | Data(UserRemoteDataSource) |
| Domain | Domain(GetUserUseCase) |
Testing Strategy
测试策略
What to Test Where
各层级测试重点
| Layer | Test Focus | Mock |
|---|---|---|
| Domain (UseCases) | Business logic, validation, orchestration | Repository protocols |
| Data (Repositories) | Coordination, caching, error mapping | DataSource protocols |
| Presentation (ViewModels) | State changes, user actions | UseCase 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
危险信号
| Smell | Problem | Fix |
|---|---|---|
| Layer violation | Move to Presentation |
UseCase just calls | Unnecessary abstraction | ViewModel → Repo directly |
| DTO in Domain | Layer violation | Keep DTOs in Data |
| Business logic in Repository | Wrong layer | Move to UseCase |
| ViewModel imports NetworkClient | Skipped layers | Use Repository |
| 代码异味 | 问题 | 修复方案 |
|---|---|---|
Domain层中 | 层级违规 | 移至Presentation层 |
UseCase仅调用 | 不必要的抽象 | ViewModel直接调用Repository |
| Domain层中出现DTO | 层级违规 | 将DTO限制在Data层 |
| Repository中包含业务逻辑 | 层级错误 | 移至UseCase |
| ViewModel导入NetworkClient | 跳过了中间层级 | 通过Repository进行调用 |