model-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseModel 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 aggregatesThe 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类型而非DoubleStruct 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 Stringswift
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) { ... } // 无法传入任意StringPolymorphic 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分层
| Scenario | Use DTO? |
|---|---|
| API matches domain exactly | No |
| API likely to change | Yes |
| Need transformation (flatten, combine) | Yes |
| Multiple APIs for same concept | Yes |
| Single stable internal API | No |
| 场景 | 是否使用DTO? |
|---|---|
| API与领域需求完全匹配 | 否 |
| API可能变更 | 是 |
| 需要转换(扁平化、合并) | 是 |
| 同一概念对应多个API | 是 |
| 单一稳定内部API | 否 |
Validation Strategy by Layer
各层验证策略
| Layer | Validation Type |
|---|---|
| API boundary (DTO init) | Structure validity |
| Domain model init | Business rules |
| Type wrappers | Format enforcement |
| UI | Already validated |
| 层级 | 验证类型 |
|---|---|
| API边界(DTO初始化) | 结构有效性 |
| 领域模型初始化 | 业务规则 |
| 类型包装器 | 格式强制约束 |
| UI层 | 已验证无需重复 |
Red Flags
危险信号
| Smell | Problem | Fix |
|---|---|---|
| Mutable snapshot | Use |
| Business logic in DTO | Wrong layer | Move to domain model |
| DTO in View | Coupling | Map to domain model |
| Force-unwrap in decoder | Crash risk | Throw or optional |
| String for typed values | No safety | Type wrappers |
| Same validation in multiple places | DRY violation | Validate at creation |
| Generic validation errors | Poor UX | Specific error cases |
| 代码异味 | 问题 | 修复方案 |
|---|---|---|
DTO中使用 | 可变快照 | 使用 |
| DTO中包含业务逻辑 | 分层错误 | 移至领域模型 |
| UI层使用DTO | 耦合性高 | 映射到领域模型 |
| 解码器中强制解包 | 崩溃风险 | 抛出错误或使用可选类型 |
| 用String表示类型化值 | 无类型安全 | 使用类型包装器 |
| 多处重复验证 | 违反DRY原则 | 在创建时一次性验证 |
| 通用验证错误 | 体验差 | 使用特定错误类型 |
Decoder Strategy Selection
解码器策略选择
| Need | Solution |
|---|---|
| snake_case → camelCase | |
| Custom date format | |
| Single field transformation | Custom |
| Nested structure flattening | Nested containers |
| Type-discriminated union | Enum with associated values |
| 需求 | 解决方案 |
|---|---|
| snake_case → camelCase | |
| 自定义日期格式 | |
| 单字段转换 | 自定义 |
| 嵌套结构扁平化 | 嵌套容器 |
| 类型判别联合类型 | 带关联值的枚举 |