immutable-model-updates
Scannednpx machina-cli add skill shimo4228/claude-code-learned-skills/immutable-model-updates --openclawImmutable Model Updates with Factory Methods
ファクトリメソッドを使ったイミュータブルなモデル更新
Extracted / 抽出日: 2026-02-05 Context / コンテキスト: Swift apps requiring predictable state management without mutations ミューテーションなしで予測可能な状態管理が必要なSwiftアプリ
Problem / 課題
Mutating shared state causes bugs:
- Race conditions in concurrent code
- Unexpected side effects
- Difficulty tracking state changes
- Hard to implement undo/redo
共有状態のミューテーションはバグを引き起こす:
- 並行コードでのレースコンディション
- 予期しない副作用
- 状態変更の追跡が困難
- Undo/Redoの実装が困難
Solution / 解決策
Make all model types immutable (using let properties) and provide factory methods that return new instances with updated values.
すべてのモデル型をイミュータブルにし(letプロパティを使用)、更新された値を持つ新しいインスタンスを返すファクトリメソッドを提供。
1. Immutable Struct with Factory Methods / ファクトリメソッドを持つイミュータブル構造体
public struct ProgressRecord: Codable, Sendable, Identifiable, Equatable {
public let questionId: String
public let lastReviewed: Date?
public let intervalDays: Int
public let easeFactor: Double
public let repetitions: Int
public let nextReview: Date?
public var id: String { questionId }
// Private init for internal control
// 内部制御用のプライベートinit
private init(
questionId: String,
lastReviewed: Date?,
intervalDays: Int,
easeFactor: Double,
repetitions: Int,
nextReview: Date?
) {
self.questionId = questionId
self.lastReviewed = lastReviewed
self.intervalDays = intervalDays
self.easeFactor = easeFactor
self.repetitions = repetitions
self.nextReview = nextReview
}
// Factory: Create initial state / 初期状態を作成
public static func initial(questionId: String) -> ProgressRecord {
ProgressRecord(
questionId: questionId,
lastReviewed: nil,
intervalDays: 0,
easeFactor: 2.5,
repetitions: 0,
nextReview: nil
)
}
// Factory: Create updated state / 更新された状態を作成
public func updated(with result: ReviewResult, answeredOn date: Date = Date()) -> ProgressRecord {
ProgressRecord(
questionId: questionId,
lastReviewed: date,
intervalDays: result.interval,
easeFactor: result.easinessFactor,
repetitions: result.repetitions,
nextReview: result.nextReviewDate
)
}
// Factory: Update single field / 単一フィールドを更新
public func withNextReview(_ date: Date?) -> ProgressRecord {
ProgressRecord(
questionId: questionId,
lastReviewed: lastReviewed,
intervalDays: intervalDays,
easeFactor: easeFactor,
repetitions: repetitions,
nextReview: date
)
}
}
2. Usage Pattern / 使用パターン
// Create initial record / 初期レコードを作成
let record = ProgressRecord.initial(questionId: "q-001")
// Update returns NEW record (original unchanged)
// 更新は新しいレコードを返す(元のレコードは変更されない)
let updatedRecord = record.updated(with: reviewResult)
// Chain updates / 更新をチェーン
let finalRecord = record
.updated(with: result1)
.withNextReview(tomorrow)
3. Collection Updates (Immutable) / コレクションの更新(イミュータブル)
extension Array where Element == ProgressRecord {
func updating(_ record: ProgressRecord) -> [ProgressRecord] {
var result = self.filter { $0.questionId != record.questionId }
result.append(record)
return result
}
func removing(questionId: String) -> [ProgressRecord] {
filter { $0.questionId != questionId }
}
}
// Usage / 使用方法
let records = existingRecords.updating(newRecord)
4. ViewModel Integration / ViewModelとの統合
@Observable
@MainActor
public final class QuizViewModel {
public private(set) var records: [ProgressRecord] = []
public func recordAnswer(questionId: String, result: ReviewResult) {
let existingRecord = records.first { $0.questionId == questionId }
?? ProgressRecord.initial(questionId: questionId)
let updatedRecord = existingRecord.updated(with: result)
// Immutable update - creates new array
// イミュータブルな更新 - 新しい配列を作成
records = records.updating(updatedRecord)
}
}
Key Benefits / 主なメリット
- Thread Safety / スレッドセーフ: Immutable values can be shared across actors safely / イミュータブルな値はactor間で安全に共有可能
- Predictability / 予測可能性: No hidden mutations, state changes are explicit / 隠れたミューテーションなし、状態変更は明示的
- Testability / テスト容易性: Easy to create test fixtures, compare expected vs actual / テストフィクスチャの作成が容易、期待値と実際の比較が容易
- Debugging / デバッグ: Can log/diff before and after states / 前後の状態をログ/差分比較可能
- Sendable / Sendable準拠: Immutable structs automatically conform to Sendable / イミュータブルな構造体は自動的にSendableに準拠
Factory Method Types / ファクトリメソッドの種類
| Method / メソッド | Purpose / 目的 | Example / 例 |
|---|---|---|
static func initial(...) | Create default state / デフォルト状態を作成 | ProgressRecord.initial(questionId:) |
func updated(with:) | Major state transition / 主要な状態遷移 | Record after review / レビュー後のレコード |
func with[Field](...) | Single field update / 単一フィールド更新 | withNextReview(_:) |
static func from(...) | Create from external data / 外部データから作成 | from(csvLine:) |
When to Use / 使用すべき場面
- Any model that represents state (user data, progress, settings) / 状態を表すモデル(ユーザーデータ、進捗、設定)
- Concurrent code where state is shared / 状態が共有される並行コード
- When you need change tracking or undo / 変更追跡やUndoが必要な場合
- SwiftUI apps (aligns with declarative UI model) / SwiftUIアプリ(宣言的UIモデルと整合)
Anti-Patterns to Avoid / 避けるべきアンチパターン
// WRONG: Mutable struct / 間違い:ミュータブルな構造体
public struct Record {
public var name: String // Mutation allowed! / ミューテーション可能!
}
// WRONG: Mutating method / 間違い:mutatingメソッド
public mutating func update(name: String) {
self.name = name // Hidden mutation / 隠れたミューテーション
}
// WRONG: Modifying in place / 間違い:その場で変更
records[index].name = "new" // Side effect! / 副作用!
Related Patterns / 関連パターン
- Combine with Swift actors for thread-safe persistence / スレッドセーフな永続化のためにSwift actorと組み合わせる
- Use
Equatableconformance for change detection / 変更検出のためにEquatable準拠を使用 - Consider
Codablefor serialization / シリアライズのためにCodableを検討
Source
git clone https://github.com/shimo4228/claude-code-learned-skills/blob/main/skills/immutable-model-updates/SKILL.mdView on GitHub Overview
Defines immutable Swift model structs using let properties and factory methods to produce updated instances. This approach delivers predictable, thread-safe state changes without mutating existing objects, making undo/redo semantics easier and reducing race conditions.
How This Skill Works
Models are immutable. You declare all properties with let and expose factory methods (like initial and updated(with:)) that return new instances. Updates never mutate the original object; instead, you replace it with the newly derived copy in your state container or collection.
When to Use It
- Build Swift models that must never mutate after creation to guarantee predictable behavior.
- Need thread-safe state updates in concurrent or background tasks without locking.
- Implement undo/redo by maintaining a history of immutable copies.
- Chain multiple updates by deriving new instances instead of mutating fields.
- Maintain collections of records (e.g., progress trackers) by replacing old records with updated copies.
Quick Start
- Step 1: Define your model with let properties and a private init if needed.
- Step 2: Add static initial(...) and instance updated(with:) factories.
- Step 3: Use the returned instance to replace the previous state in your view model or array.
Best Practices
- Use let for all properties to enforce immutability.
- Provide a private init to control instance creation if needed.
- Offer an initial(...) factory for the starting state and updated(with:) for changes.
- Prefer functional updates over in-place mutation to enable easy testing and rollbacks.
- Explain thread-safety and copy semantics in your docs to avoid accidental mutation.
Example Use Cases
- Progress tracking with a per-question ProgressRecord that updates via updated(with:) and optional nextReview.
- Immutable user/settings snapshot objects that replace the previous configuration when a change occurs.
- Undo/redo implementations built by storing successive immutable state copies.
- Collections of records updated by replacing an old item with its updated copy in an array extension.
- SwiftUI ViewModels consuming immutable models, updating state by assigning new copies instead of mutating structs.