


大きなデスクトップのメディアアプリを、UIを止めずアンドゥを壊さず直す
ファイルやメディアを扱う大きなデスクトップアプリで作業する時に使う。重い処理をUIスレッドの外で動かし、キャンセルやエッジケースを扱い、アンドゥで正しく戻るようにし、実際に動かしたテストだけを報告する。
このAIルールを使うには
## 呼び出しの流れを全部たどり、状態ごとに持ち主を1つ決める
- 編集する前に、対象機能の所有範囲と既存の抽象化を理解し、UI・自動化・アンドゥ・永続化・バックグラウンド処理を通じた呼び出しパス全体を追跡する。
- 些細でない変更では、コードを書く前に、信頼できる情報源(source of truth)・状態の所有者・分離ドメイン・不変条件・失敗時とキャンセル時の挙動を特定する。
- 実装の詳細より先にアーキテクチャを設計し、無関係な層に散らばったパッチよりも、一貫性のある一つの変更を優先する。
- 可変な状態はそれぞれ、権威ある所有者を一つだけ持たせる。派生状態は、明示的な無効化契約を持つキャッシュによって計算・維持する。
- 既存のドメイン操作を再利用する。呼び出し元がプレビュー・ビュー・コントローラー・自動化ツール・テストであることを理由に、二つ目の実装を追加しない。
- コードは、それを所有する機能の中に置く。共有ユーティリティは、複数の独立した機能が使うインフラのためだけに残す。型やファイルの名前は、所有関係が明確になるように付ける。
## コメントは短くし、使われないコードを取り除く
- コメントは最小限にする。理由・不変条件・安全上の制約・回避策が自明でない場合にのみ書き、短く1行に収める。
- コードの動作を実況したり、名前をそのまま言い換えたり、今回のパッチの説明を書いたり、削除の痕跡を残したり、コメントアウトされたコードを残したりしない。
- 説明的なコメントよりも、正確な命名・小さな型・切り出した操作を優先する。
- 計算やビジネスルールは、信頼できる情報源を一つだけ持つ。別のファイルや箇所へコピーしてはならない。
- 作業を終える前に、デッドコード・未使用の状態・使われなくなった互換性のためのパス・一時的な診断コードを削除する。
- プロジェクトがサポートしていないプラットフォーム・バージョン・構成のための互換性コードを追加しない。
## 重い処理をUIスレッドから外し、awaitの後に状態を確かめ直す
- UIスレッドは貴重な資源として扱う。UIと軽い調整用の状態を保持してよいが、ファイル操作・デコード・モデル推論・画像処理・インデックス作成・エクスポート・ブロッキング呼び出し・大きな変換処理を行ってはならない。
- 分離とパフォーマンスは別物である。処理をasyncにしたりタスクでラップしたりしても、それだけでは現在のスレッドから移動しない。実際の実行主体(executor)を確認する。
- スレッドの切り替えは明示的に行う。スレッドを離れる前に必要最小限のイミュータブルな入力をスナップショットし、値を返し、それが依然として最新であることを確認してから適用する。
- 中断ポイントを通過するたびに、共有状態が変化している前提に立つ。結果を確定する前に、同一性・世代・選択状態・前提条件を再検証する。
- 構造化された並行処理を優先する。構造化されていないタスクには、明確な所有者、キャンセルが重要な場合は保持されたハンドル、そして定義された後始末処理が必要である。
- キャンセルは協調的に行う。長いループは意味のある区切りでキャンセルを確認し、部分的な結果や古くなった結果を決して確定しない。
- 並列処理は実際に希少な資源で上限を設ける。処理中の同一リクエストは重複排除する。ゲートの取得と解放は対応するスコープ内で行う。各continuationやコールバックは必ず一度だけ再開する。
- 同期的な待機・セマフォ・ロック・非同期処理のポーリングによってUIスレッドをブロックしてはならない。
## ファイル操作をUIスレッドの外で行い、ファイルを一度に置き換える
- ファイルシステム操作はすべてUIスレッド外で実行する。読み取り・書き込み・存在確認とメタデータ確認・列挙・コピー・移動・置換・削除・ディレクトリ作成が対象である。
- どのボリュームも、低速・取り外し可能・外部から変更される・ネットワーク経由である可能性を前提とする。サイズが小さいことや過去にアクセスが成功したことは、同期アクセスの安全性を保証しない。
- ファイル操作の前に、所有しているモデルデータをスナップショットする。ファイル操作の実行主体から、観測可能な状態を変更してはならない。
- 完成した出力は、本番の対象の外側で用意する。置き換え先で置換内容を準備し、原子的に反映する。実運用への書き込みは直接行わず、一つの調整役(コーディネーター)を経由させる。
- 同じファイルや同じ対象を扱う操作は直列化し、保存・インポート・生成・サムネイル作成・削除・エクスポート・クローズが競合しないようにする。
- 一意な一時パスを使い、成功時・失敗時・キャンセル時のいずれでも後片付けする。置き換え先の削除・置換は、置換内容が完全に準備できるまで行わない。
- ユーザーが要求したファイル操作の失敗は表に出す。無視されたエラー・空の結果・成功したかのような応答の裏に隠さない。
## 速くなったと言う前に測り、よく通る処理を軽く保つ
- レンダリング・再生・入力処理・ビュー更新・インポート・インデックス作成・保存・復元・エクスポートなど、パフォーマンスに敏感なパスを特定し、ホットパスとして扱う。
- 項目ごと・フレームごと・繰り返されるインタラクションのパスの中で、ファイルシステムアクセス・ロギング・シリアライズ・モデルやデコーダーの初期化その他のブロッキングを伴うセットアップを行わない。
- 改善したと言う前に必ず計測する。変更の前後で同じワークロードをプロファイリングまたはベンチマークする。
- 低レベルなチューニングより先に、アルゴリズムの計算量と不要な処理を修正する。ネストしたスキャン・繰り返しのソート・ループ内でのコピーによる変更・中間的なメモリ確保・繰り返されるスレッド切り替えに注意する。
- 大量の変更はバッチ化・統合する。スナップショット・アンドゥ・エクスポート・クローズ、および最新データを返す必要がある読み取りの前には、保留中の状態をフラッシュする。
- データは遅延ロード・遅延ハイドレーションする。コストの高い再利用可能な処理をキャッシュする場合は、明示的なキー・容量・無効化ルール・古い結果への方針がある場合に限る。
- ライフサイクルが許す範囲で、コストの高いオブジェクトは再利用し、関連する設定変更時には無効化する。進捗やメーターのような高頻度の状態が、ビュー全体を無効化しないようにする。保持するメモリ量には上限を設ける。
## 作る前に、エッジケースと失敗の場合を洗い出す
- ハッピーパスだけを実装するのでは不十分である。実装前に、該当する境界値・ライフサイクル・並行処理・失敗のケースを洗い出し、それぞれをどの層が担当するかを決める。
- 該当する場合、空・nil・ゼロ・負数・最大値・オーバーフロー・非有限値・不正な形式・重複・欠損・古くなった値・非対応の入力を検証する。
- 算術演算・インデックスアクセス・数値変換を行う前に検証する。後段のcatchで演算エラーを吸収することに頼ってはならない。暗黙のクランプではなく、丸めとクランプの厳密な仕様を定義する。
- 何もしない操作や繰り返し操作もカバーする。それらは正確に結果を報告しなければならず、状態変更・履歴エントリ・重複した処理・誤解を招く成功報告を生んではならない。
- 開始前・各フェーズの最中・処理完了後でコミット前のそれぞれでキャンセルを考慮する。選択・対象・設定が変更された後の、古い完了結果についても考慮する。
- クローズ・終了・スリープ・復帰・非アクティブ化・デバイスの変更、および処理実行中の「名前を付けて保存」といった後始末のパスを考慮する。
- すべての失敗パスで不変条件を維持する。部分的な成功は、安全に再開可能でありその旨が報告されるか、あるいはロールバックされるかのいずれかである。
## ユーザーの1つの操作を、1回のアンドゥで戻せるようにする
- ユーザーによる操作と自動化による操作は、同じドメインの変更操作と同じ共有履歴を経由させる。
- 一つのまとまったユーザーの意図は、一つのアンドゥ可能な操作を生む。内部のサブステップは、それぞれが独立して意味を持つ場合を除き、別々の履歴エントリとして公開しない。
- 履歴グループを開く前に、引数と前提条件を検証する。失敗・キャンセル・拒否・変更なしの操作は、空の履歴エントリを作ってはならない。
- ネストした処理は、プラットフォームや他のサブシステムが所有するグループを閉じることなく、外側の操作へと統合される。
- アンドゥは、丸め誤差の蓄積・派生状態のズレ・孤立した関係・古い選択状態を残すことなく、正確な状態を復元する。
- 変更が履歴に関わる場合は、ユーザーによる編集・自動化による編集・プラットフォームのイベントのグループ化・同時リクエストの間の入り交じりをテストする。
## 自動化ツールを、ユーザーのやりたい事から設計する
- ツールは、内部のAPI・ストレージ操作・サービスの境界ではなく、ユーザーの意図から設計する。スキーマを決める前に、結果・成功基準・警告・失敗・キャンセル・冪等性・アンドゥの意味を定義する。
- 一つのツールは一つのまとまった操作を行う。1回の呼び出しで、通常は一つの原子的で理解しやすく、アンドゥ可能な一連の処理が完了する。無関係なモードを持つ幅広いツールを作らない。システム自体が安全に行える処理のために、呼び出し元に低レベルなツールの連結を強いない。
- パラメータは、ユーザー向けのドメイン概念で表現し、ストレージの構造や実装の詳細は隠す。自動化には安定したエンティティ識別子を使い、位置によるインデックスやラベルは、永続的な識別子ではなく文脈情報としてのみ扱う。
- すべての引数を信頼できないものとして扱う。正確な型・有限な数値・明示的な範囲・有効な識別子を要求する。状態を変更する前にリクエスト全体を解決・検証し、複数エンティティにまたがる変更は原子的に適用する。
- UIと同じドメイン操作を再利用し、配置・リンク付け・検証のロジックを重複させない。
- 何が変更されたか・安定した識別子・明示的な「変更なし」状態・警告・対処可能なエラーを記述した構造化された受領情報を返す。結果が調整されたり達成されなかったりした場合に、成功したかのような応答を返してはならない。契約として約束され報告される場合を除き、暗黙のクランプ・対象の変更・フォールバックを行ってはならない。
- 長時間実行されるツールは、呼び出し元が確認できる永続的なジョブまたは最終結果を公開する。機械向けの契約は、ローカライズされた文言とは分離し、安定させておく。説明文には、そのツールがいつ・なぜ・どのように他のツールと関わるかを記す。
## UIの状態はUIスレッドに置き、プラットフォーム本来の動きを守る
- 観測可能なUIの状態はUIスレッド上に保持し、バックグラウンドの結果はイミュータブルな値として境界を越えさせる。
- 監視の範囲は、その値を必要とする最小のビューに限定する。進捗・ホバー・再生状態のような高頻度の状態が、無関係なビューツリーを無効化してはならない。
- レンダリング本体から、持続する副作用を開始してはならない。明示的なキャンセルと後始末を備えた、ライフサイクルを意識したタスクやコントローラーを使う。レンダリング本体は高速で副作用のない状態を保つ。
- フォーカス・キーボード操作・Escapeキー・メニュー・ウィンドウの復元・アンドゥ・ドラッグ状態・クローズ時の確認について、プラットフォーム本来の挙動を維持する。
- デリゲートや完了ハンドラーの契約は、成功・失敗・キャンセル・対象の欠落のいずれの経路でも、必ず一度だけ完了させる。
- APIが保証していない限り、フレームワークや完了コールバックがUIスレッド上で呼ばれると仮定しない。
## メディアを止まらずに処理し、時間やフレームの値を正確に保つ
- 外部アセットには非同期でのプロパティ読み込みを使う。呼び出し元のスレッドをブロックしうる、非推奨の同期アクセサを呼び出さない。
- 正確な時間・フレーム・単位を表す整数といった、値本来の領域はできるだけ長く保つ。浮動小数点への変換は、UIや外部フォーマットとの境界で明示的に行う場合に限る。
- タイミング・スケール・変換・色・アルファ・チャンネルレイアウト・メタデータといった元データの意味を定義し、機能が明示的に変更しない限り維持する。
- ライフサイクルが許す範囲で、リーダー・レンダーコンテキスト・パイプラインは再利用する。無効化が必要な場合を除き、連続的なインタラクションの最中に再構築しない。
- 同時実行するデコーダー・リーダー・エクスポート・推論・抽出の数には上限を設ける。デコード・レンダリング・エクスポートの各ループにキャンセルを伝播させ、基盤となる処理がキャンセル不能な場合はチャンクの間で確認する。
- トラックの欠落・特殊なコンテナ形式・長さがゼロまたは不定・回転やアルファチャンネルを含む入力・非標準のレート・キャンセルの各ケースでテストする。
## 見た目の値を直接書かず、デザイントークンを使う
- スタイルはすべて共有のデザイントークンシステムを通して適用する。余白・サイズ・フォントの太さ・角丸の半径・枠線の太さ・不透明度・色・シャドウ・アニメーションの長さをハードコードしてはならない。
- 必要なトークンが存在しない場合は、値をハードコードするのではなく、まずトークンシステムに追加する。
- リソースの参照は、パッケージ化されたアプリケーション・ローカル実行・テストのいずれでも機能しなければならない。機能ごとに独自の探索処理を書かず、共有のリソース参照の抽象化を使う。バンドルの構成はそれぞれ、検証すべき別個の設定として扱う。
- デバッグ用フラグを使って根本的に異なるリソースパスや挙動を選択しない。その違いが意図的でテスト済みである場合を除く。
- バンドルするリソースを追加する際は、パッケージマニフェスト・ビルドスクリプト・テストを更新し、最終的にパッケージ化された構成を検証する。フィーチャーフラグ付きの任意機能のコードは、フラグの有無どちらでもビルドできる状態を保つ。
## どの操作も結果が見える形で終わらせ、役に立つログを残す
- ユーザーが開始した操作、自動化が開始した操作はいずれも、観測可能な成功・失敗・拒否・キャンセルのいずれかの状態に到達する。
- 失敗が結果を左右する場合、インタラクションの境界でエラーを無視してはならない。UIの状態・ツールのエラー・ジョブの失敗・対処可能なログのいずれかに変換する。
- 投げっぱなし(fire-and-forget)の処理は、明示的にベストエフォートである場合にのみ許される。必須の永続化処理を担ってはならず、その失敗は安全に無視できるか、記録されるかのいずれかでなければならない。
- 失敗をログに残す際は、操作内容・安定した識別子・ライフサイクルのどの段階かを含める。機密情報・認証情報・生のユーザー入力をログに書き込んではならない。
- キャンセル・検証エラー・リソースの利用不可・フレームワークの障害・内部の不変条件違反を区別できるだけの診断情報を残す。
- アサーションは、プログラマーが保証すべき不変条件にのみ使う。回復可能なユーザー入力・外部ファイル・ライフサイクルの競合・自動化リクエストには使わない。
## 小さく、毎回同じ結果になり、並列で動くテストを書く
- 単体テストは簡潔で、振る舞いに焦点を当てる。一つのテストは、それを明確にするために必要なセットアップのみを使い、一つの不変条件またはリグレッションを検証する。説明的な名前を使い、手順を実況するコメントは書かない。
- 境界値のマトリクスにはパラメータ化テストを使い、複数のテストスイートにまたがってセットアップを重複させるのではなく、共有のフィクスチャービルダーを使う。
- その振る舞いを証明できる最小限の安定した継ぎ目(seam)を通してテストする。非公開の実装詳細や偶発的な呼び出し回数への結合は避ける。
- すべてのバグ修正には、現実的な場合、元の不具合で失敗するリグレッションテストを付ける。検証・何もしない操作・部分的な失敗・キャンセル・古い完了結果・後片付けといった、失敗系のパスもカバーする。
- 並行処理のテストは、ゲート・continuation・注入されたフックを使って決定論的に制御する。sleepやタイミングの運、スケジューリングの順序に依存しない。
- テストは並列実行される。可変なグローバル状態・固定の一時的な名前・ポート・ディレクトリを共有しない。一意な一時ディレクトリを使い、後片付けする。実際の認証情報・外部サービス・実際のユーザーファイルは単体テストから排除する。
## 正しさから先にレビューし、実際に動かしたテストを正確に伝える
- スタイルより先に正確性をレビューする。変更された振る舞いを、呼び出し元・共有状態・バックグラウンド処理・永続化・アンドゥ・UI・自動化の各接点を通して追跡する。
- データ損失・破損・クラッシュ・ハング・競合・セキュリティ上の露出・誤った結果・壊れたアンドゥ・誤解を招く成功報告を引き起こす指摘を優先し、その次にパフォーマンス・保守性・仕上がりの質を扱う。
- UIスレッドのブロッキング、中断後に再検証なしで状態を確定するといった並行処理上のミス、所有者のいないタスク、キャンセルの欠落、無制限なファンアウト、対応の取れていないゲート、二重に再開されたコールバックに加え、重複したドメインロジック・安全でないキャッシュ・検証されていない境界値の演算がないか確認する。レビューの範囲は、変更箇所とそれが直接影響する範囲に限定する。
- 主張は検証する。変更に適したビルドとテストを実行し、並行処理に関わる変更ではキャンセルとライフサイクルのパスを実際に動かし、パフォーマンスの変化は代表的なワークロードのもとで計測する。
- 何を実行し何を実行しなかったかを正確に報告する。一部しか完了していないのに、ビルド・テスト・実行時検証をすべて行ったと主張しない。
- コミットは、狭く正確なカテゴリのプレフィックスを付け、無関係な整理を含めず、焦点を絞る。変更内容は、編集した順序ではなく、概要・アプローチ・主要な設計判断・テストの根拠によって説明する。
