model-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Model Patterns — Expert Decisions

模型模式——专家级决策

Expert decision frameworks for model design choices. Claude knows Codable syntax — this skill provides judgment calls for when to separate DTOs, validation strategies, and immutability trade-offs.

模型设计选择的专家级决策框架。Claude熟悉Codable语法——本技能提供关于何时进行DTO分层、验证策略选择以及不可变性权衡的判断依据。

Decision Trees

决策树

DTO vs Single Model

DTO vs 单一模型

Does API response match your domain needs?
├─ YES (1:1 mapping)
│  └─ Is API contract stable?
│     ├─ YES → Single Codable model is fine
│     └─ NO → DTO protects against API changes
├─ NO (needs transformation)
│  └─ DTO + Domain model
│     DTO: matches API exactly
│     Domain: matches app needs
└─ Multiple APIs for same domain concept?
   └─ Separate DTOs per API
      Single domain model aggregates
The trap: DTO for everything. If your API matches your domain and is stable, a single Codable struct is simpler. Add DTO layer when it solves a real problem.
API响应是否与你的领域需求匹配?
├─ 是(1:1映射)
│  └─ API契约是否稳定?
│     ├─ 是 → 单一Codable模型即可
│     └─ 否 → DTO可抵御API变更
├─ 否(需要转换)
│  └─ DTO + 领域模型
│     DTO:与API完全匹配
│     领域模型:符合应用需求
└─ 同一领域概念对应多个API?
   └─ 为每个API单独设置DTO
      单一领域模型聚合数据
误区:给所有场景都加DTO。如果你的API与领域需求匹配且稳定,单一Codable struct会更简单。仅当能解决实际问题时再添加DTO层。

Validation Strategy Selection

验证策略选择

When should validation happen?
├─ External data (API, user input)
│  └─ Validate at boundary (init or factory)
│     Fail fast with clear errors
├─ Internal data (already validated)
│  └─ Trust it (no re-validation)
│     Validation at boundary is sufficient
└─ Critical invariants (money, permissions)
   └─ Type-level enforcement
      Email type, not String
      Money type, not Double
验证应在何时进行?
├─ 外部数据(API、用户输入)
│  └─ 在边界处验证(初始化或工厂方法中)
│     快速失败并返回清晰错误
├─ 内部数据(已验证过)
│  └─ 信任数据(无需重复验证)
│     边界处的验证已足够
└─ 关键不变量(金额、权限)
   └─ 类型级别的强制约束
      使用Email类型而非String
      使用Money类型而非Double

Struct vs Class Decision

Struct vs Class 决策

What are your requirements?
├─ Simple data container
│  └─ Struct (value semantics)
│     Passed by copy, immutable by default
├─ Shared mutable state needed?
│  └─ Really? Reconsider design
│     └─ If truly needed → Class with @Observable
├─ Identity matters (same instance)?
│  └─ Class (reference semantics)
│     But consider if ID equality suffices
└─ Inheritance needed?
   └─ Class (but prefer composition)
你的需求是什么?
├─ 简单数据容器
│  └─ Struct(值语义)
│     按值传递,默认不可变
├─ 需要共享可变状态?
│  └─ 真的需要吗?重新考量设计
│     └─ 若确实需要 → 使用带@Observable的Class
├─ 身份标识很重要(需同一实例)?
│  └─ Class(引用语义)
│     但可考虑是否用ID相等性替代
└─ 需要继承?
   └─ Class(但优先使用组合)

Custom Decoder Complexity

自定义解码器复杂度

How much custom decoding?
├─ Just key mapping (snake_case → camelCase)
│  └─ Use keyDecodingStrategy
│     decoder.keyDecodingStrategy = .convertFromSnakeCase
├─ Few fields need transformation
│  └─ Custom init(from decoder:)
│     Transform specific fields only
├─ Complex nested structure flattening
│  └─ Custom init(from decoder:) with nested containers
│     Or intermediate DTO + mapping
└─ Polymorphic decoding (type field determines struct)
   └─ Type-discriminated enum with associated values
      Or AnyDecodable wrapper

需要多少自定义解码逻辑?
├─ 仅需键映射(snake_case → camelCase)
│  └─ 使用keyDecodingStrategy
│     decoder.keyDecodingStrategy = .convertFromSnakeCase
├─ 少数字段需要转换
│  └─ 自定义init(from decoder:)
│     仅转换特定字段
├─ 复杂嵌套结构扁平化
│  └─ 带嵌套容器的自定义init(from decoder:)
│     或中间DTO + 映射
└─ 多态解码(类型字段决定struct)
   └─ 带关联值的类型判别枚举
      或AnyDecodable包装器

NEVER Do

绝对不要做的事

DTO Design

DTO设计

NEVER make DTOs mutable:
swift
// ❌ DTO can be modified after decoding
struct UserDTO: Codable {
    var id: String
    var name: String  // var allows mutation
}

// ✅ DTOs are immutable snapshots of API response
struct UserDTO: Codable {
    let id: String
    let name: String  // let enforces immutability
}
NEVER add business logic to DTOs:
swift
// ❌ DTO has behavior
struct UserDTO: Codable {
    let id: String
    let firstName: String
    let lastName: String

    func sendWelcomeEmail() { ... }  // Business logic in DTO!

    var isAdmin: Bool {
        roles.contains("admin")  // Business rule in DTO
    }
}

// ✅ DTO is pure data; logic in domain model or service
struct UserDTO: Codable {
    let id: String
    let firstName: String
    let lastName: String
}

struct User {
    let id: String
    let fullName: String
    let isAdmin: Bool

    init(from dto: UserDTO, roles: [String]) {
        // Mapping and business logic here
    }
}
NEVER expose DTOs to UI layer:
swift
// ❌ View depends on API contract
struct UserView: View {
    let user: UserDTO  // If API changes, UI breaks

    var body: some View {
        Text(user.first_name)  // Snake_case in UI!
    }
}

// ✅ View uses domain model
struct UserView: View {
    let user: User  // Stable domain model

    var body: some View {
        Text(user.fullName)  // Clean API
    }
}
绝对不要让DTO可变:
swift
// ❌ DTO解码后可被修改
struct UserDTO: Codable {
    var id: String
    var name: String  // var允许修改
}

// ✅ DTO是API响应的不可变快照
struct UserDTO: Codable {
    let id: String
    let name: String  // let强制不可变
}
绝对不要在DTO中添加业务逻辑:
swift
// ❌ DTO包含行为
struct UserDTO: Codable {
    let id: String
    let firstName: String
    let lastName: String

    func sendWelcomeEmail() { ... }  // 业务逻辑放在DTO中!

    var isAdmin: Bool {
        roles.contains("admin")  // 业务规则放在DTO中
    }
}

// ✅ DTO是纯数据;逻辑放在领域模型或服务中
struct UserDTO: Codable {
    let id: String
    let firstName: String
    let lastName: String
}

struct User {
    let id: String
    let fullName: String
    let isAdmin: Bool

    init(from dto: UserDTO, roles: [String]) {
        // 映射和业务逻辑放在这里
    }
}
绝对不要将DTO暴露给UI层:
swift
// ❌ 视图依赖API契约
struct UserView: View {
    let user: UserDTO  // API变更时,UI会崩溃

    var body: some View {
        Text(user.first_name)  // UI中出现蛇形命名!
    }
}

// ✅ 视图使用领域模型
struct UserView: View {
    let user: User  // 稳定的领域模型

    var body: some View {
        Text(user.fullName)  // 清晰的API
    }
}

Codable Implementation

Codable实现

NEVER force-unwrap in custom decoders:
swift
// ❌ Crashes on unexpected data
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let urlString = try container.decode(String.self, forKey: .imageURL)
    imageURL = URL(string: urlString)!  // Crashes if invalid URL!
}

// ✅ Handle invalid data gracefully
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let urlString = try container.decode(String.self, forKey: .imageURL)
    guard let url = URL(string: urlString) else {
        throw DecodingError.dataCorrupted(
            .init(codingPath: [CodingKeys.imageURL],
                  debugDescription: "Invalid URL: \(urlString)")
        )
    }
    imageURL = url
}
NEVER silently default invalid data:
swift
// ❌ Hides data problems
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    // Silently uses 0 for invalid price — masks bugs!
    price = (try? container.decode(Double.self, forKey: .price)) ?? 0.0
}

// ✅ Fail or default with logging
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    do {
        price = try container.decode(Double.self, forKey: .price)
    } catch {
        Logger.api.warning("Invalid price, defaulting to 0: \(error)")
        price = 0.0  // Intentional default, logged
    }
}
NEVER use String for typed values:
swift
// ❌ No type safety
struct User: Codable {
    let email: String  // Any string allowed
    let status: String  // "active", "inactive"... or typo?
}

// ✅ Type-safe wrappers
struct Email {
    let value: String
    init?(_ value: String) {
        guard value.contains("@") else { return nil }
        self.value = value
    }
}

enum UserStatus: String, Codable {
    case active, inactive, suspended
}

struct User {
    let email: Email
    let status: UserStatus
}
绝对不要在自定义解码器中强制解包:
swift
// ❌ 遇到异常数据会崩溃
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let urlString = try container.decode(String.self, forKey: .imageURL)
    imageURL = URL(string: urlString)!  // URL无效时会崩溃!
}

// ✅ 优雅处理无效数据
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let urlString = try container.decode(String.self, forKey: .imageURL)
    guard let url = URL(string: urlString) else {
        throw DecodingError.dataCorrupted(
            .init(codingPath: [CodingKeys.imageURL],
                  debugDescription: "无效URL: \(urlString)")
        )
    }
    imageURL = url
}
绝对不要默默默认无效数据:
swift
// ❌ 隐藏数据问题
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    // 价格无效时默默使用0——掩盖bug!
    price = (try? container.decode(Double.self, forKey: .price)) ?? 0.0
}

// ✅ 失败或默认时记录日志
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    do {
        price = try container.decode(Double.self, forKey: .price)
    } catch {
        Logger.api.warning("价格无效,默认设为0: \(error)")
        price = 0.0  // 有意设置默认值并记录日志
    }
}
绝对不要用String表示类型化的值:
swift
// ❌ 无类型安全
struct User: Codable {
    let email: String  // 允许任意字符串
    let status: String  // "active", "inactive"... 或拼写错误?
}

// ✅ 类型安全包装器
struct Email {
    let value: String
    init?(_ value: String) {
        guard value.contains("@") else { return nil }
        self.value = value
    }
}

enum UserStatus: String, Codable {
    case active, inactive, suspended
}

struct User {
    let email: Email
    let status: UserStatus
}

Validation

验证

NEVER validate in multiple places:
swift
// ❌ Validation scattered
func saveUser(_ user: User) {
    guard user.email.contains("@") else { return }  // Duplicate!
    // ...
}

func displayUser(_ user: User) {
    guard user.email.contains("@") else { return }  // Duplicate!
    // ...
}

// ✅ Validate once at creation
struct User {
    let email: Email  // Email type guarantees validity

    init(email: String) throws {
        guard let validEmail = Email(email) else {
            throw ValidationError.invalidEmail
        }
        self.email = validEmail
        // All downstream code trusts email is valid
    }
}
NEVER throw generic errors from validation:
swift
// ❌ Caller can't determine what's wrong
init(name: String, email: String, age: Int) throws {
    guard !name.isEmpty else { throw NSError(domain: "error", code: -1) }
    guard email.contains("@") else { throw NSError(domain: "error", code: -1) }
    // Same error for different problems!
}

// ✅ Specific validation errors
enum ValidationError: LocalizedError {
    case emptyName
    case invalidEmail(String)
    case ageOutOfRange(Int)

    var errorDescription: String? {
        switch self {
        case .emptyName: return "Name cannot be empty"
        case .invalidEmail(let email): return "Invalid email: \(email)"
        case .ageOutOfRange(let age): return "Age \(age) is out of valid range"
        }
    }
}

绝对不要在多个地方重复验证:
swift
// ❌ 验证逻辑分散
func saveUser(_ user: User) {
    guard user.email.contains("@") else { return }  // 重复!
    // ...
}

func displayUser(_ user: User) {
    guard user.email.contains("@") else { return }  // 重复!
    // ...
}

// ✅ 在创建时一次性验证
struct User {
    let email: Email  // Email类型保证有效性

    init(email: String) throws {
        guard let validEmail = Email(email) else {
            throw ValidationError.invalidEmail
        }
        self.email = validEmail
        // 所有下游代码都信任邮箱是有效的
    }
}
绝对不要在验证时抛出通用错误:
swift
// ❌ 调用者无法确定具体问题
init(name: String, email: String, age: Int) throws {
    guard !name.isEmpty else { throw NSError(domain: "error", code: -1) }
    guard email.contains("@") else { throw NSError(domain: "error", code: -1) }
    // 不同问题返回相同错误!
}

// ✅ 特定的验证错误
enum ValidationError: LocalizedError {
    case emptyName
    case invalidEmail(String)
    case ageOutOfRange(Int)

    var errorDescription: String? {
        switch self {
        case .emptyName: return "名称不能为空"
        case .invalidEmail(let email): return "无效邮箱: \(email)"
        case .ageOutOfRange(let age): return "年龄\(age)超出有效范围"
        }
    }
}

Essential Patterns

核心模式

DTO with Domain Mapping

DTO与领域模型映射

swift
// DTO: Exact API contract
struct UserDTO: Codable {
    let id: String
    let first_name: String
    let last_name: String
    let email: String
    let avatar_url: String?
    let created_at: String
    let is_verified: Bool
}

// Domain: App's representation
struct User: Identifiable {
    let id: String
    let fullName: String
    let email: Email
    let avatarURL: URL?
    let createdAt: Date
    let isVerified: Bool

    var initials: String {
        fullName.split(separator: " ")
            .compactMap { $0.first }
            .map(String.init)
            .joined()
    }
}

// Mapping extension
extension User {
    init(from dto: UserDTO) throws {
        self.id = dto.id
        self.fullName = "\(dto.first_name) \(dto.last_name)"

        guard let email = Email(dto.email) else {
            throw MappingError.invalidEmail(dto.email)
        }
        self.email = email

        self.avatarURL = dto.avatar_url.flatMap(URL.init)
        self.createdAt = ISO8601DateFormatter().date(from: dto.created_at) ?? Date()
        self.isVerified = dto.is_verified
    }
}
swift
// DTO:与API契约完全匹配
struct UserDTO: Codable {
    let id: String
    let first_name: String
    let last_name: String
    let email: String
    let avatar_url: String?
    let created_at: String
    let is_verified: Bool
}

// 领域模型:应用的内部表示
struct User: Identifiable {
    let id: String
    let fullName: String
    let email: Email
    let avatarURL: URL?
    let createdAt: Date
    let isVerified: Bool

    var initials: String {
        fullName.split(separator: " ")
            .compactMap { $0.first }
            .map(String.init)
            .joined()
    }
}

// 映射扩展
extension User {
    init(from dto: UserDTO) throws {
        self.id = dto.id
        self.fullName = "\(dto.first_name) \(dto.last_name)"

        guard let email = Email(dto.email) else {
            throw MappingError.invalidEmail(dto.email)
        }
        self.email = email

        self.avatarURL = dto.avatar_url.flatMap(URL.init)
        self.createdAt = ISO8601DateFormatter().date(from: dto.created_at) ?? Date()
        self.isVerified = dto.is_verified
    }
}

Type-Safe Wrapper Pattern

类型安全包装器模式

swift
struct Email: Codable, Hashable {
    let value: String

    init?(_ value: String) {
        let regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
        guard value.wholeMatch(of: regex) != nil else { return nil }
        self.value = value
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawValue = try container.decode(String.self)
        guard let email = Email(rawValue) else {
            throw DecodingError.dataCorrupted(
                .init(codingPath: container.codingPath,
                      debugDescription: "Invalid email: \(rawValue)")
            )
        }
        self = email
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}

// Usage: compiler enforces email validity
func sendEmail(to: Email) { ... }  // Can't pass arbitrary String
swift
struct Email: Codable, Hashable {
    let value: String

    init?(_ value: String) {
        let regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
        guard value.wholeMatch(of: regex) != nil else { return nil }
        self.value = value
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawValue = try container.decode(String.self)
        guard let email = Email(rawValue) else {
            throw DecodingError.dataCorrupted(
                .init(codingPath: container.codingPath,
                      debugDescription: "无效邮箱: \(rawValue)")
            )
        }
        self = email
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}

// 使用方式:编译器强制保证邮箱有效性
func sendEmail(to: Email) { ... }  // 无法传入任意String

Polymorphic Decoding

多态解码

swift
enum MediaItem: Codable {
    case image(ImageMedia)
    case video(VideoMedia)
    case document(DocumentMedia)

    private enum CodingKeys: String, CodingKey {
        case type
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)

        switch type {
        case "image":
            self = .image(try ImageMedia(from: decoder))
        case "video":
            self = .video(try VideoMedia(from: decoder))
        case "document":
            self = .document(try DocumentMedia(from: decoder))
        default:
            throw DecodingError.dataCorrupted(
                .init(codingPath: [CodingKeys.type],
                      debugDescription: "Unknown media type: \(type)")
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        switch self {
        case .image(let media): try media.encode(to: encoder)
        case .video(let media): try media.encode(to: encoder)
        case .document(let media): try media.encode(to: encoder)
        }
    }
}

swift
enum MediaItem: Codable {
    case image(ImageMedia)
    case video(VideoMedia)
    case document(DocumentMedia)

    private enum CodingKeys: String, CodingKey {
        case type
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)

        switch type {
        case "image":
            self = .image(try ImageMedia(from: decoder))
        case "video":
            self = .video(try VideoMedia(from: decoder))
        case "document":
            self = .document(try DocumentMedia(from: decoder))
        default:
            throw DecodingError.dataCorrupted(
                .init(codingPath: [CodingKeys.type],
                      debugDescription: "未知媒体类型: \(type)")
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        switch self {
        case .image(let media): try media.encode(to: encoder)
        case .video(let media): try media.encode(to: encoder)
        case .document(let media): try media.encode(to: encoder)
        }
    }
}

Quick Reference

快速参考

When to Use DTO Separation

何时使用DTO分层

ScenarioUse DTO?
API matches domain exactlyNo
API likely to changeYes
Need transformation (flatten, combine)Yes
Multiple APIs for same conceptYes
Single stable internal APINo
场景是否使用DTO?
API与领域需求完全匹配
API可能变更
需要转换(扁平化、合并)
同一概念对应多个API
单一稳定内部API

Validation Strategy by Layer

各层验证策略

LayerValidation Type
API boundary (DTO init)Structure validity
Domain model initBusiness rules
Type wrappersFormat enforcement
UIAlready validated
层级验证类型
API边界(DTO初始化)结构有效性
领域模型初始化业务规则
类型包装器格式强制约束
UI层已验证无需重复

Red Flags

危险信号

SmellProblemFix
var
in DTO
Mutable snapshotUse
let
Business logic in DTOWrong layerMove to domain model
DTO in ViewCouplingMap to domain model
Force-unwrap in decoderCrash riskThrow or optional
String for typed valuesNo safetyType wrappers
Same validation in multiple placesDRY violationValidate at creation
Generic validation errorsPoor UXSpecific error cases
代码异味问题修复方案
DTO中使用
var
可变快照使用
let
DTO中包含业务逻辑分层错误移至领域模型
UI层使用DTO耦合性高映射到领域模型
解码器中强制解包崩溃风险抛出错误或使用可选类型
用String表示类型化值无类型安全使用类型包装器
多处重复验证违反DRY原则在创建时一次性验证
通用验证错误体验差使用特定错误类型

Decoder Strategy Selection

解码器策略选择

NeedSolution
snake_case → camelCase
keyDecodingStrategy
Custom date format
dateDecodingStrategy
Single field transformationCustom
init(from:)
Nested structure flatteningNested containers
Type-discriminated unionEnum with associated values
需求解决方案
snake_case → camelCase
keyDecodingStrategy
自定义日期格式
dateDecodingStrategy
单字段转换自定义
init(from:)
嵌套结构扁平化嵌套容器
类型判别联合类型带关联值的枚举