Get the FREE Ultimate OpenClaw Setup Guide →

immutable-model-updates

Scanned
npx machina-cli add skill shimo4228/claude-code-learned-skills/immutable-model-updates --openclaw
Files (1)
SKILL.md
7.9 KB

Immutable 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 / 主なメリット

  1. Thread Safety / スレッドセーフ: Immutable values can be shared across actors safely / イミュータブルな値はactor間で安全に共有可能
  2. Predictability / 予測可能性: No hidden mutations, state changes are explicit / 隠れたミューテーションなし、状態変更は明示的
  3. Testability / テスト容易性: Easy to create test fixtures, compare expected vs actual / テストフィクスチャの作成が容易、期待値と実際の比較が容易
  4. Debugging / デバッグ: Can log/diff before and after states / 前後の状態をログ/差分比較可能
  5. 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 Equatable conformance for change detection / 変更検出のためにEquatable準拠を使用
  • Consider Codable for 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

  1. Step 1: Define your model with let properties and a private init if needed.
  2. Step 2: Add static initial(...) and instance updated(with:) factories.
  3. 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.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers