はじめに
「設計段階でちゃんとテストできたらいいのに...」
こんな風に思ったことはありませんか?特に状態管理のような複雑な機能を設計するとき、実装前に動作を確認できないのってすごく不安ですよね。今回は、そんなモヤモヤを解決する方法として、LLMを使った設計アプローチを紹介します。具体的には、Webアプリのデプロイ機能にあるロールバック機能を例に、どんな風にLLMを活用できるのか、実践的に見ていきましょう。
LLMと設計を進める
設計プロセスにおいて、LLMとの対話は大きく3つのフェーズで行いました:
Phase 1: 初期設計の探索
最初の対話では、基本的なデータ構造の設計を探りました:
Input to LLM:
rollbackを実装したいので、状態をjsonで管理したい。
1 -> 2 -> rollback -> 1
1 -> 2 -> 3 -> rollback -> 2 -> 4 -> rollback -> 2
のような操作を実現するデータ構造を提案して。
Phase 2: 設計パターンの検討
初期設計を得た後、既存のベストプラクティスを探るため、以下のような対話を行いました:
Input to LLM:
状態管理とロールバックの実装に関して、一般的なデザインパターンはある?
それぞれの利点と欠点を教えて。
Phase 3: 具体的な実装設計
最後に、選択したパターンに基づいて具体的な実装を検討しました:
Input to LLM:
Mementoパターンを使って、具体的なJSONスキーマを設計したい。
状態履歴とメタデータを含める必要がある。
LLMと一緒に設計を進めてみた
Step 1: まずは基本構造を考えよう
最初は、とにかく「これって実現できるの?」というところから始めました。LLMに投げかけた最初の質問は、こんな感じです:
ねぇ、ロールバック機能を作りたいんだけど、状態をJSONで管理するとしたらどんな感じになると思う?
例えば、こんな感じの操作ができるようにしたいんだけど:
1 -> 2 -> rollback -> 1
1 -> 2 -> 3 -> rollback -> 2 -> 4 -> rollback -> 2
これに対してLLMくん、なかなか良い提案をしてくれました:
class StateManager:
def __init__(self):
# 状態の履歴を配列で管理
self.state_history: List[Dict[str, Any]] = []
# 現在の状態を指すインデックス
self.current_index: int = -1
この提案のイイところ:
- 履歴を配列で管理するのは直感的でシンプル
- インデックスで現在位置を管理するのは分かりやすい
- 拡張性もありそう
Step 2: もっと良いやり方を探ってみよう
「でもさ、こういうの、もっと良いやり方があるんじゃない?」
そう思って、LLMに「この手の実装って、何かベストプラクティスとかないの?」って聞いてみました。すると、興味深い提案が返ってきました。
Commandパターン
interface Command {
execute(): void;
undo(): void;
}
このパターンの特徴:
- 各操作を独立したオブジェクトとして管理
- 操作の履歴を詳細に記録できる
- 複雑な操作もサポートできる
- ただし、実装が少し複雑になりがち
Mementoパターン
class Memento {
constructor(private state: any, private timestamp: Date) {}
}
こっちの特徴:
- シンプルで分かりやすい
- 状態のスナップショットを管理しやすい
- 実装が比較的楽
- ただし、細かい操作の制御は難しいかも
正直、どっちにするか迷いました。でも、今回のケースではシンプルさを重視して、Mementoパターンを採用することにしました。
Step 3: 具体的な状態管理を設計してみよう
「じゃあ、具体的にJSONってどんな構造にする?」
この質問に対して、LLMは以下のような構造を提案してきました:
{
"mementos": [
{
"snapshot": {
"value": 1,
"data": "initial state",
"deployInfo": {
"version": "1.0.0",
"timestamp": "2024-11-15T10:00:00",
"deployer": "tanaka"
}
},
"metadata": {
"version": 1,
"timestamp": "2024-11-15T10:00:00",
"description": "初期デプロイ"
}
}
],
"currentIndex": 0
}
これ、なかなか良いですよね:
- スナップショットとメタデータを分けて管理
- デプロイ情報もちゃんと含まれている
- タイムスタンプや説明文で追跡可能性も確保
Step 4: 実際の動きを確認してみよう
設計ができたら、次は実際の動きを確認。以下のような操作シーケンスを考えてみました:
-
Undo操作のケース
// currentIndexを1減らして前の状態に戻る const undo = () => { if (currentIndex > 0) { currentIndex--; return mementos[currentIndex].snapshot; } throw new Error("これ以上戻れません!"); };
-
Redo操作のケース
// currentIndexを1増やして次の状態に進む const redo = () => { if (currentIndex < mementos.length - 1) { currentIndex++; return mementos[currentIndex].snapshot; } throw new Error("これ以上進めません!"); };
-
新しい状態の追加
// 新しい状態を追加する時は、current以降の履歴を消して追加 const addState = (newState) => { mementos.splice(currentIndex + 1); mementos.push({ snapshot: newState, metadata: { version: currentIndex + 2, timestamp: new Date().toISOString() } }); currentIndex++; };
エッジケースの確認のため、以下のようなインタラクティブな対話をLLMと行いました:
Input to LLM:
以下の操作シーケンスでどうなる?
1 -> 2 -> 3 (add)
undo
undo
4を追加
redo
LLMの回答:
各ステップでの状態変化を確認します:
1. 初期状態: [1]
currentIndex: 0
2. 2を追加: [1, 2]
currentIndex: 1
3. 3を追加: [1, 2, 3]
currentIndex: 2
4. undo実行: [1, 2, 3]
currentIndex: 1
現在の値: 2
5. もう一度undo: [1, 2, 3]
currentIndex: 0
現在の値: 1
6. 4を追加: [1, 4]
// ここで重要: currentIndex以降の履歴(2,3)は削除される
currentIndex: 1
7. redo実行: redoできる履歴がないためエラー
// 4の追加時に2,3の履歴が削除されているため
この動的な確認により、以下の重要な点が明確になりました:
- undo/redoの状態管理
- 履歴の分岐処理
- エラー発生条件
さらに、エッジケースについても確認を行いました:
Input to LLM:
以下のようなケースで、どのような問題が発生する可能性がある?
1. 最初の状態に戻ろうとした場合
2. 履歴の最後まで進んだ後の操作
3. ロールバック後に新しい状態を追加
LLMの回答:
各ケースについて以下のような問題点と対策が提示されました:
-
初期状態へのロールバック
// 提案された対策 if (currentIndex <= 0) { throw new StateManagementError('INITIAL_STATE_REACHED'); }
-
最終状態での操作
// 提案された対策 if (currentIndex >= mementos.length - 1) { throw new StateManagementError('FINAL_STATE_REACHED'); }
-
分岐履歴の処理
// 提案された実装 function addStateAfterRollback(newState) { // ロールバック後の履歴を適切に管理 mementos.splice(currentIndex + 1); addNewState(newState); }
Step 5: 仕様書にまとめよう
設計が固まってきたところで、実装のための仕様書を作成しました。
データ構造の定義
// 基本的な状態を表す型
interface State {
value: number;
data: string;
deployInfo: {
version: string;
timestamp: string;
deployer: string;
};
}
// メタデータの型
interface Metadata {
version: number;
timestamp: string;
description: string;
tags?: string[];
}
// スナップショット全体の型
interface Snapshot {
state: State;
metadata: Metadata;
}
// システム全体の型
interface StateManagement {
snapshots: Snapshot[];
currentIndex: number;
maxHistory: number;
}
主要な操作の仕様
-
状態の追加
interface AddStateOptions { description?: string; tags?: string[]; } function addState(state: State, options?: AddStateOptions): void;
-
ロールバック操作
interface RollbackOptions { preserveHistory?: boolean; } function rollback(targetIndex: number, options?: RollbackOptions): void;
-
状態の取得
interface GetStateOptions { includeMetadata?: boolean; } function getCurrentState(options?: GetStateOptions): State;
エラーハンドリング
想定されるエラーケース:
- 無効なインデックスへのロールバック試行
- 状態データの不整合
- メモリ制限の超過
対応方針:
- 各操作の前に必ず状態の整合性をチェック
- エラーは専用の型で定義して明示的に処理
- ロギングによる追跡可能性の確保
この設計アプローチの良かったところ
1. 動的な設計検証ができた!
- LLMと対話しながら設計を進められた
- 実装前に問題点を発見できた
- 設計の改善サイクルが早かった
2. パターンの選択が適切にできた
- 既存のデザインパターンを活用
- プロジェクトに合った実装方法を選択
- 拡張性と保守性のバランスが取れた
3. 問題点の早期発見
- エッジケースを事前に検討できた
- 実装上の課題が明確になった
- 設計の妥当性を確認できた
まとめと今後の展望
得られた成果
-
設計プロセスの改善
- LLMとの対話による設計の検証
- 早期のフィードバック獲得
- 柔軟な設計の改善
-
技術的な成果
- 堅牢な状態管理システムの設計
- 明確な仕様の策定
- エラーケースへの対応方針の確立
-
プロジェクト管理面での効果
- 設計段階でのリスク低減
- チーム内での共通理解の促進
- 実装工数の正確な見積もり
今後の可能性
この経験から、以下のような展望が見えてきました:
-
設計プロセスの進化
- LLMを活用した設計レビュー
- 自動的な設計検証の可能性
- より効率的な設計手法の確立
-
機能の拡張
- より高度な状態管理機能の追加
- パフォーマンス最適化
- セキュリティ強化
-
チーム開発への応用
- 設計ドキュメントの自動生成
- コードレビューでの活用
- 知識共有の促進
最後に、この方法は特に以下のようなケースで効果を発揮すると感じました:
- 複雑な状態管理が必要なプロジェクト
- チーム間での設計の共有が重要なケース
- 早期のフィードバックが求められる開発
みなさんも、ぜひプロジェクトでLLMを活用した設計アプローチを試してみてください!きっと新しい発見があるはずです。