breadboarding について
ワークフローの説明を、UI とコードのアフォーダンスおよび関係性のマップに変換する手法です。既存システムの把握と設計の具体化に対応し、既存部分と新規部分を組み合わせて扱えます。
主な機能
- 01ワークフローを UI とコードの表に整理
- 02アフォーダンス間の配線を記録
- 03プレイス、モード、サブプレイスをモデル化
- 04既存システムと設計変更を組み合わせ
- 05Mermaid による任意の可視化
活用シーン
- 01既存ワークフローの動作経路の把握
- 02提案設計の仕組みの具体化
- 03フロントエンドとバックエンドにまたがる変更の整理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rjs/ shaping-skills --skill breadboardingインストール後、Claude Code で/breadboardingを入力して読み込まれたか確認します。
詳細
ブレッドボーディング
ブレッドボーディングは、ワークフローの説明を、アフォーダンスとその関係性を網羅したマップに変換します。出力は常に、番号付きの UI アフォーダンスとコードアフォーダンス、およびそれらの「出力先」と「戻り先」の関係を示す一連のテーブルです。テーブルが正確な情報源です。Mermaid の図は、人間向けの任意の視覚化です。
ユースケース
ブレッドボーディングには、2 つの用途があります。
1. 既存システムのマッピング
既存システムが具体的にどのように動作するのかを理解できていません。ある動作がどのように起きるのか、またはなぜ起きないのかを説明しようとしており、そのワークフローを理解する必要があります。
入力:
- 分析対象のコードリポジトリ
- ワークフローの説明(常に、UI を通じて、または呼び出し元として、効果を発生させようとするオペレーターの視点で記述)
出力:
- UI アフォーダンステーブル
- コードアフォーダンステーブル
- (任意)Mermaid による視覚化
注: ワークフローが複数のアプリケーション(フロントエンド+バックエンド)にまたがる場合は、全体像を示すブレッドボードを 1 つ作成します。各システムが分かるように場所をラベル付けしてください。
2. 形作られたパーツからの設計
形作りによって、パーツ(メカニズム)の組み合わせとして新しいシステムをスケッチしました。具体的なメカニズムを詳しく示し、それらのパーツがシステムとしてどのように相互作用するかを示す必要があります。
入力:
- パーツの一覧(形作りで定義したメカニズム)
- パーツが達成する要件/成果(R)
- 既存システム(任意)—新しいパーツが既存コードと相互運用する必要がある場合
出力:
- UI アフォーダンステーブル
- コードアフォーダンステーブル
- (任意)Mermaid による視覚化
混在するケース
既存のまま維持する必要があるシステムに、形作りで定義した新しい要素や変更を加えるケースはよくあります。この場合、既存のアフォーダンスと新しいアフォーダンスをまとめてブレッドボードにし、それらのつながりを示します。
3. ホワイトボードのブレッドボードの読み方
手描きまたはホワイトボード上のブレッドボードでは、テーブルではなく、視覚的に積み重ねる形式を使用します。同じ概念(Place、アフォーダンス、ワイヤリング)が適用されますが、レイアウトの規則は異なります。
視覚的な規則:
| 要素 | 表示方法 |
|---|---|
| Place | 縦に積み重ねたブロックの最上部にある色付きブロック(多くの場合、ピンク/紫) |
| Place 内のアフォーダンス | Place のブロックの下に積み重ねられたブロック。スタック内の縦位置によって包含関係を示します |
| コードアフォーダンス | 通常、Place のスタック内ではなく、スタックの間に配置されます |
| Place ローダー | Place のブロックの左上に配置されたコードアフォーダンス。その Place を表示するために必要なデータ/入力を示します |
| Wires Out | ブロック間の実線の矢印 |
| Returns To | ブロック間の破線の矢印 |
| 条件分岐 | スタック内にインデントされたブロック。多くの場合、別の色(例:緑)で、if/else 分岐を示します |
| Place 参照 | スタック内の Place 名の前に付く _(_PlaceName の規則と同じ) |
| 不確実/暫定 | アフォーダンス名の前に付く ? または ~、あるいは破線の枠。アフォーダンスが仮説段階であることを示します |
| 包含ボックス | 複数のスタックを囲む大きな境界。システムまたは責任範囲ごとにアフォーダンスをまとめます(例:「HireEZ」ボックス) |
| 注記/注釈 | 要素の近くにある自由形式のテキスト。背景情報、未解決の質問、または根拠を示します |
ホワイトボードのブレッドボードの読み方:
- Place を特定する — 各スタックの最上部にある色付きのヘッダーブロックを見つけます
- 各スタックを上から下へ読む — Place の下に積み重ねられたものはすべて、その Place に属します
- ローダーを見つける — Place のブロックの左上にあるコードアフォーダンスは、表示に必要なデータを示します
- ワイヤリングをたどる — スタック間の矢印をたどり、制御フロー(実線)とデータフロー(破線)を確認します
- 条件分岐に注意する — 色の異なるインデントされたブロックは、Place 内の分岐ロジックを示します
- 包含ボックスを確認する — 大きな境界は、システムまたは責任範囲の境界を示します
- 仮説段階の項目に印を付ける —
?や~が先頭に付いた項目は不確実であり、シェイピングの過程で残らない可能性があります
テーブルへの変換: ホワイトボードのブレッドボードを標準のアフォーダンステーブルに変換する際は、各スタックをその Place に対応付け、アフォーダンスを上から下へ列挙し、矢印を Wires Out/Returns To の関係として記録します。ローダーは、データを渡す UI アフォーダンスを Returns To の接続先とするコードアフォーダンスになります。
コア概念
Place
Place とは、相互作用の境界付きコンテキストです。Place にいる間は、次のようになります。
- 利用できるアフォーダンスのセットが定められています
- その境界の外にあるアフォーダンスとはやり取りできません
- その場を離れるには、何らかの操作が必要です
Place は技術的なものではなく、知覚的なものです。 URL やコンポーネントについてではなく、ユーザーが現在のコンテキストとして体験するものを指します。Place は、今できることという観点での「現在いる場所」です。
ブロッキングテスト
あるものが別の Place かどうかを判断する最も簡単なテストは、**背後にあるものを操作できますか?**という問いです。
| 回答 | 意味 |
|---|---|
| いいえ | 別の Place にいます |
| はい | 同じ Place で、ローカルな状態が変化しています |
例
| UI 要素 | ブロッキングするか | Place か | 理由 |
|---|---|---|---|
| モーダル | はい | はい | 背後のページを操作できません |
| 確認ポップオーバー | はい | はい | 戻る前に応答する必要があります(モーダルの限界事例) |
| 編集モード(画面全体が変化) | はい | はい | すべてのアフォーダンスが変化します |
| チェックボックスで追加フィールドが表示される | いいえ | いいえ | 周囲は変化しません |
| ドロップダウンメニュー | いいえ | いいえ | ほかの場所をクリックして閉じられ、操作を妨げません |
| ツールチップ | いいえ | いいえ | 情報を表示するもので、操作を妨げません |
ローカル状態とプレイスナビゲーション
コントロールが状態を変更したとき、自問してください:すべてが変わったのか、それとも周囲はそのままで一部だけが変わったのか?
| 種類 | 何が起こるか | モデル化の方法 |
|---|---|---|
| ローカル状態 | UI の一部が変化、周囲は変化なし | 同じプレイス、条件付き N → 依存する U |
| プレイスナビゲーション | 画面全体が変わる、またはブロッキングオーバーレイ | 異なるプレイス |
モードベースのプレイス
モード(たとえば「編集モード」)が画面全体を変える場合 — ボタンがすべて変わり、あらゆるアフォーダンスが変わる — は、別々のプレイスとしてモデル化します:
PLACE: CMS Page (Read Mode)
PLACE: CMS Page (Edit Mode)
それらを切り替える状態フラグ(例:editMode$)はナビゲーションの仕組みであり、データストアではありません。どちらのプレイスにも S として含めないでください。
あらゆるコントロールに対する 3 つの質問
どんな UI アフォーダンスに対しても、自問してください:
- これを見るためにどこから来たのか?
- 今どこにいるのか?
- これに働きかけたらどこへ行くのか?
#3 の答えが「すべてが変わる」または「応答するまで背後にあるものと対話できない」なら、それは別のプレイスへのナビゲーションです。
ラベリングの慣例
| パターン | 用途 |
|---|---|
PLACE: ページ名 | 標準のページ/ルート |
PLACE: ページ名 (モード) | ページのモードベースのバリアント |
PLACE: モーダル名 | モーダルダイアログ |
PLACE: バックエンド | API/データベースの境界 |
複数のシステムにまたがる場合は、システム名を付けてラベル付けします:PLACE: チェックアウトページ (フロントエンド)、PLACE: 決済 API (バックエンド)。
プレイス ID
プレイスはデータモデルにおける第一級の要素です。各プレイスには ID が付与されます:
| # | プレイス | 説明 |
|---|---|---|
| P1 | CMS ページ(閲覧モード) | 閲覧専用の状態 |
| P2 | CMS ページ(編集モード) | ページレベルのコントロールを備えた編集状態 |
| P2.1 | widget-grid (letters) | サブプレイス:P2 内のレター編集ウィジェット |
| P3 | レターフォームモーダル | レターを追加/編集するためのフォーム |
| P4 | バックエンド | API リゾルバーとデータベース |
プレイス ID によって以下が可能になります:
- 明示的なナビゲーションの配線 — 内部のアフォーダンスではなく
→ P2に配線する - 包含関係の追跡 — 各アフォーダンスがどのプレイスに属するかを宣言する
- 一貫した Mermaid サブグラフ — サブグラフ ID がプレイス ID と一致する
プレイス参照
ネストされたプレイスが内部アフォーダンスを多く持ち、親を煩雑にする場合、それを切り離すことができます:
- 親プレイスに、アンダースコア接頭辞を使った参照ノードを置きます:
_letter-browser - 内部をすべて含む完全なプレイスを別途定義します
- 参照からプレイスへ配線します:
_letter-browser --> letter-browser
参照はUI アフォーダンスです — 親のコンテキストにおいて「このウィジェット/コンポーネントがここにレンダリングされる」ことを表します。
flowchart TB
subgraph P1["P1: CMS Page (Read Mode)"]
U1["U1: Edit button"]
U_LB["_letter-browser"]
end
subgraph letterBrowser["letter-browser"]
U10["U10: Search input"]
U11["U11: Letter list"]
N40["N40: performSearch()"]
end
U_LB --> letterBrowser
アフォーダンステーブルでは、参照を UI アフォーダンスとして記載します:
| # | アフォーダンス | コントロール | 配線先 |
|---|---|---|---|
| U1 | 編集ボタン | click | → N1 |
| _letter-browser | ウィジェット参照 | — | → P3 |
プレイス参照は、区別しやすいように破線の境界線でスタイル設定します:
classDef placeRef fill:#ffb6c1,stroke:#d87093,stroke-width:2px,stroke-dasharray:5 5
class U_LB placeRef
プレイスとしてのモード
コンポーネントに明確に異なるモード(閲覧 vs 編集、表示 vs 編集、折りたたみ vs 展開)がある場合、それらを別々のプレイスとしてモデル化します — ユーザーにとって異なる知覚状態だからです。
あるモードが別のモードのすべてを含み、さらに追加がある場合、拡張されたプレイス内にプレイス参照を表示してこれを示します:
P3: letter-browser (Read) — base state
P4: letter-browser (Edit) — contains _letter-browser (Read) + new affordances
参照は構成を示します:「P3 のすべてがここに現れ、さらにこれらの追加がある」。
flowchart TB
subgraph P3["P3: letter-browser (Read)"]
U10["U10: Search input"]
U11["U11: Letter list"]
end
subgraph P4["P4: letter-browser (Edit)"]
U_P3["_letter-browser (Read)"]
U3["U3: Add button"]
U4["U4: Edit button"]
end
U_P3 --> P3
P4 のアフォーダンステーブルでは、参照は継承を示します:
| # | アフォーダンス | コントロール | 配線先 | 備考 |
|---|---|---|---|---|
| _letter-browser (Read) | P3 のすべてを継承 | — | → P3 | |
| U3 | 追加ボタン | click | → N3 | NEW |
| U4 | 編集ボタン | click | → N4 | NEW |
サブプレイス
サブプレイスとは、プレイスの定義されたサブセットです。つまり、関連するアフォーダンスをグループ化する、プレイス内のまとまった領域です。サブプレイスは次のような場合に使用します:
- 1 つのプレイスに複数の異なるウィジェットやセクションが含まれる場合
- より大きなプレイスの一部分を詳細化する場合
- スコープ内とスコープ外を区別して示したい場合
表記: 階層的な ID を使用します — P2 のサブプレイスには P2.1、P2.2 などと付けます。
| # | Place | Description |
|---|-------|-------------|
| P2 | Dashboard | Main dashboard page |
| P2.1 | Sales widget | Subplace: sales metrics |
| P2.2 | Activity feed | Subplace: recent activity |
アフォーダンスの表では、包含関係を示すためにサブプレイスの ID を使用します:
| U3 | P2.1 | sales-widget | "Refresh" button | click | → N4 | — |
| U7 | P2.2 | activity-feed | activity list | render | — | — |
Mermaid の場合: サブプレイスのサブグラフを親の内側にネストします。同じ背景色を使用してください(区別できる塗りつぶしは使わない)— サブプレイスは親の一部であり、独立したプレイスではありません:
flowchart TB
subgraph P2["P2: Dashboard"]
subgraph P2_1["P2.1: Sales widget"]
U3["U3: Refresh button"]
end
subgraph P2_2["P2.2: Activity feed"]
U7["U7: activity list"]
end
otherContent[["... other dashboard content ..."]]
end
スコープ外コンテンツのプレースホルダー: あるサブプレイスを詳細化するときは、ページ上にさらに内容があることを示すために、兄弟となるプレースホルダーを追加します:
otherContent[["... other page content ..."]]
これは読者に「ここでは P2.1 を詳しく見ていますが、P2 にはまだ詳細化していない内容がほかにもあります」と伝えています。
包含と配線
これらはデータモデルにおける 2 つの異なる関係です:
| 関係 | 意味 | 記述する場所 |
|---|---|---|
| 包含(Containment) | アフォーダンスがプレイスに属する / プレイス内に存在する | Place 列(集合の所属) |
| 配線(Wiring) | アフォーダンスが何かをトリガーする / 呼び出す | Wires Out 列(制御フロー) |
包含は集合の所属です: U1 ∈ P1 は U1 がプレイス P1 の要素であることを意味します。すべてのアフォーダンスは正確に 1 つのプレイスに属します。
配線は制御フローです: U1 → N1 は U1 が N1 をトリガーすることを意味します。アフォーダンスは何にでも配線できます — 他のアフォーダンスでもプレイスでも構いません。
Place 列が答えるのは「このアフォーダンスはどこに存在するか?」です。 Wires Out 列が答えるのは「このアフォーダンスは何をトリガーするか?」です。
ナビゲーションの配線
アフォーダンスがナビゲーションを引き起こす場合(ユーザーがどこかへ「移動する」場合)、その内部のアフォーダンスではなく、プレイス自体に配線します:
✅ N1 Wires Out: → P2 (navigate to Edit Mode)
❌ N1 Wires Out: → U3 (wiring to affordance inside P2)
これにより、ナビゲーションが表の中で明示されます。プレイスが移動先であり、その内部の具体的なアフォーダンスは、そこに到達すると利用可能になります。
Mermaid では、これは次のようになります:
N1 --> P2
サブグラフの ID はプレイスの ID と一致するため、配線はプレイスの境界に接続されます。
アフォーダンス
操作の対象となるものです:
- UI アフォーダンス(U): 入力欄、ボタン、表示される要素、スクロール領域
- コードアフォーダンス(N): メソッド、サブスクリプション、データストア、フレームワークの機構
配線
アフォーダンス同士がどのように接続されるか:
Wires Out — アフォーダンスがトリガーまたは呼び出すもの(制御フロー):
- 呼び出し配線: あるアフォーダンスが別のアフォーダンスを呼び出す
- 書き込み配線: コードがデータストアに書き込む
- ナビゲーション配線: 別のプレイスへルーティングする
Returns To — アフォーダンスの出力がどこへ流れるか(データフロー):
- 戻り配線: 関数が呼び出し元に値を返す
- 読み取り配線: データストアが別のアフォーダンスによって読み取られる
この分離により、データフローが明確になります。Wires Out は制御フロー(何が何をトリガーするか)を示します。Returns To はデータフロー(出力がどこへ行くか)を示します。
出力: アフォーダンスの表
表こそが真実です。すべてのブレッドボードは次の表を生成します:
Places 表
| # | Place | Description |
|---|---|---|
| P1 | Search Page | Main search interface |
| P2 | Detail Page | Individual result view |
UI アフォーダンス表
| # | Place | Component | Affordance | Control | Wires Out | Returns To |
|---|---|---|---|---|---|---|
| U1 | P1 | search-detail | search input | type | → N1 | — |
| U2 | P1 | search-detail | loading spinner | render | — | — |
| U3 | P1 | search-detail | results list | render | — | — |
| U4 | P1 | search-detail | result row | click | → P2 | — |
コードのアフォーダンス表
| # | 場所 | コンポーネント | アフォーダンス | 制御 | 接続先 | 戻り先 |
|---|---|---|---|---|---|---|
| N1 | P1 | search-detail | activeQuery.next() | 呼び出し | → N2 | — |
| N2 | P1 | search-detail | activeQuery の購読 | 監視 | → N3 | — |
| N3 | P1 | search-detail | performSearch() | 呼び出し | → N4, → N5, → N6 | — |
| N4 | P1 | search.service | searchOneCategory() | 呼び出し | → N7 | → N3 |
| N5 | P1 | search-detail | loading | 書き込み | ストア | → U2 |
| N6 | P1 | search-detail | results | 書き込み | ストア | → U3 |
| N7 | P1 | typesense.service | rawSearch() | 呼び出し | — | → N4 |
データストア表
| # | 場所 | ストア | 説明 |
|---|---|---|---|
| S1 | P1 | results | 検索結果の配列 |
| S2 | P1 | loading | 読み込み状態を示すブール値 |
列の定義
| 列 | 説明 |
|---|---|
| # | 一意の ID(場所は P1、P2...、UI は U1、U2...、コードは N1、N2...、ストアは S1、S2...) |
| 場所 | このアフォーダンスが属する場所(包含関係) |
| コンポーネント | このコンポーネント/サービスを所有するもの |
| アフォーダンス | 操作できる具体的なもの |
| 制御 | トリガーとなるイベント:クリック、入力、呼び出し、監視、書き込み、レンダリング |
| 接続先 | これによってトリガーされるもの:→ N4、→ P2(ナビゲーションを含む制御フロー) |
| 戻り先 | 出力の流れ先:→ N3 または → U2, U3(データフロー) |
手順
既存システムをマッピングする場合
完全な実践例については、以下の 例 A を参照してください。
ステップ 1:分析するフローを特定する
具体的なユーザージャーニーを選びます。何かを実行しようとするオペレーターの視点で、必ず表現してください。
- 「/search にアクセスし、クエリを入力し、さらに読み込むためにスクロールして、結果をクリックする」
- 「カードトークンを指定して支払い API を呼び出し、請求が作成されることを期待する」
ステップ 2:関係するすべての場所を列挙する
ジャーニーをたどり、ユーザーが訪れる場所、またはシステム境界を越える箇所をそれぞれ特定します。
ステップ 3:コードをたどってコンポーネントを見つける
エントリーポイント(ルート、API エンドポイント)からコードをたどり、そのフローで関係するすべてのコンポーネントを見つけます。
ステップ 4:各コンポーネントのアフォーダンスを列挙する
コードを読み、以下を特定します。
- UI:ユーザーに何が表示され、何を操作できますか?
- コード:どのメソッド、購読、ストアが関係していますか?
ステップ 5:抽象概念ではなく、実際のものに名前を付ける
「DATABASE」と書いたら、いったん止めてください。実際のメソッドは何でしょうか?(userRepo.save())。すべてのアフォーダンス名は、コード内で実際に確認できるものでなければなりません。
ステップ 6:制御列を埋める
各アフォーダンスをトリガーするものは何ですか?(クリック、入力、呼び出し、監視、書き込み、レンダリング)
ステップ 7:接続先を埋める
各アフォーダンスがトリガーするものは何ですか?コードを読み、このメソッドが何を呼び出すのか、このボタンのハンドラーが何を実行するのかを確認します。
ステップ 8:戻り先を埋める
各アフォーダンスの出力はどこに流れますか?
- 値を返す関数 → 戻り値を受け取る呼び出し元を列挙します
- データストア → そのストアから読み取るアフォーダンスを列挙します
- 意味のある出力がない場合 →
—を使用します
ステップ 9:データストアをアフォーダンスとして追加する
コードがプロパティに書き込み、そのプロパティが後で別のアフォーダンスによって読み取られる場合、そのプロパティを制御タイプ write のコードアフォーダンスとして追加します。
ステップ 10:フレームワークの仕組みをアフォーダンスとして追加する
コードと UI のレンダリングをつなぐ cdr.detectChanges() などを含めます。これにより、状態の変更が実際に UI に反映される仕組みを示せます。
ステップ 11:コードと照合して確認する
コードをもう一度読みます。すべてのアフォーダンスが実在し、接続関係が実態と一致していることを確認します。
形状からパーツを設計する
スライスを含む完全な作業例については、以下の例 Bを参照してください。
ステップ 1:形状から各パーツを列挙する
形状設計で特定した仕組み/パーツをそれぞれ書き出します。
ステップ 2:パーツをアフォーダンスに変換する
各パーツについて、次の点を特定します。
- このパーツにはどのような UI アフォーダンスが必要ですか?
- このパーツを実装する Code アフォーダンスは何ですか?
ステップ 3:すべての U に対応する N があることを確認する
各 UI アフォーダンスについて、確認します。どの Code アフォーダンスがデータを提供するか、またはその描画を制御しますか?該当するものがなければ、不足している N を追加します。
ステップ 4:場所を既存または新規に分類する
各 UI アフォーダンスについて、次のどちらに属するかを判断します。
- 変更する既存の場所
- 作成する新しい場所
ステップ 5:アフォーダンスを接続する
各アフォーダンスの Wires Out と Returns To を記入します。想定する動作をたどり、何が何を呼び出し、何がどこに返されるかを確認します。
ステップ 6:既存システムに接続する(該当する場合)
既存のコードベースがある場合:
- 新しいアフォーダンスが接続する必要のある既存のアフォーダンスを特定します
- それらの既存のアフォーダンスをテーブルに追加します
- 新しいアフォーダンスをそれらに接続します
ステップ 7:漏れがないことを確認する
- すべての U に、データを供給する N があること
- すべての N に、Wires Out または Returns To(あるいは両方)があること
- ハンドラー → Wires Out があること
- クエリ → Returns To があること
- データストア → Returns To があること
ステップ 8:ユーザーに見える出力を U として扱う
ユーザーに表示されるもの(メールや通知など)はすべて UI アフォーダンスであり、それに接続する N が必要です。
重要な原則
記憶に頼らず、必ずデータを確認する
フローを逆方向にたどるときは、覚えている経路をたどらないでください。Wires Out 列を確認し、対象に接続するすべてのアフォーダンスを探します。
テーブルに記入するときは、各行を順を追って読みます。知っているつもりのことに頼らないでください。
テーブルが信頼できる唯一の情報源です。記憶は当てになりません。
マッピング時は、すべてのアフォーダンス名が実在する必要がある
既存のコードをマッピングするときに、抽象化を決して作り出さないでください。すべての名前は、コードベース内の実在するものを指している必要があります。
仕組みはアフォーダンスではない
アフォーダンスとは、システム内で意味のある識別性を持ち、作用を加えられるものです。アフォーダンスのように見えても、実際には単なる実装上の仕組みであるものがいくつかあります。
| 種類 | 例 | アフォーダンスではない理由 |
|---|---|---|
| 視覚的なコンテナー | modal-frame wrapper | wrapper に作用することはできません。単なる Place の境界です |
| 内部変換 | letterDataTransform() | 呼び出し元の実装詳細であり、独立して作用できるものではありません |
| ナビゲーションの仕組み | modalService.open() | Place に移動するための「方法」にすぎません。遷移先に直接接続します |
これらは、最初の草稿では必ずしも明白ではありません。 アフォーダンスのテーブルを確認するときは、各 Code アフォーダンスを再確認し、次のように問いかけてください。
「これは本当にアフォーダンスですか。それとも、何かが起こる仕組みを詳しく説明しているだけですか?」
単なる「方法」であれば、それは省略し、遷移先または結果に直接接続します。
例:
❌ N8 --> N22 --> P3 (N22 is modalService.open — just mechanism)
✅ N8 --> P3 (handler navigates to modal)
❌ N6 --> N20 --> S2 (N20 is data transform — internal to N6)
✅ N6 --> S2 (callback writes to store)
❌ U7: modal-frame (wrapper — just the boundary of P3)
✅ U8: Save button (actionable)
ハンドラーは P3 に遷移します。コールバックはストアに書き込みます。モーダルが P3 です。仕組みは暗黙的です。
2 つのフロー:ナビゲーションとデータ
ブレッドボードでは、2 つの異なるフローを捉えます。
| フロー | 追跡する内容 | 接続 |
|---|---|---|
| ナビゲーション | Place から Place への移動 | Wires Out → Places |
| データ | 状態がどのように表示に反映されるか | Returns To → Us |
この 2 つは直交しています。ナビゲーションがあってもデータが変わらない場合や、ナビゲーションがなくてもデータが変わる場合があります。
ブレッドボードを確認するときは、両方のフローをたどります。
- ナビゲーションフロー: Place から Place へ続くユーザーの操作の流れをたどれますか?
- データフロー: データを表示するすべての U について、そのデータの取得元をたどれますか?
データを表示するすべての U にはデータソースが必要です
データを表示する UI アフォーダンスには、データストア(S)か、データを返すコードアフォーダンス(N)のいずれかが必要です。
❌ U6: letter list (no incoming wire — where does the data come from?)
✅ S1 -.-> U6 (store feeds the display)
✅ N4 -.-> U6 (query result feeds the display)
表示する U にデータソースの配線が接続されていない場合、次のいずれかです。
- ブレッドボードにソースがありません
- その U は実在しません
ナビゲーションに集中していると、これは見落としがちです。必ず「この U はデータを表示しています。そのデータはどこから来るのでしょうか?」と確認してください。
すべての N は接続する必要があります
コードアフォーダンスに Wires Out も Returns To もない場合、何かが間違っています。
- ハンドラー → Wires Out が必要です(呼び出す対象や書き込む先)
- クエリ → Returns To が必要です(戻り値を受け取る先)
- データストア → Returns To が必要です(読み取るアフォーダンス)
副作用にはストアが必要です
どこにも配線されていないように見える N は疑わしいものです。システムの境界外に副作用(ブラウザーの URL、localStorage、外部 API、分析機能)がある場合は、その外部状態を表すストアノードを追加してください。
❌ N41: updateUrl() (wires to... nothing?)
✅ N41: updateUrl() → S15 (wires to Browser URL store)
これにより、データフローが明確になります。ストアには、外部状態がどのように戻ってくるかを示すリターンワイヤーを接続することもできます。
flowchart TB
N42["N42: performSearch()"] --> N41["N41: updateUrl()"]
N41 --> S15["S15: Browser URL (?q=)"]
S15 -.->|back button / init| N40["N40: activeQuery$"]
モデル化する一般的な外部ストア:
Browser URL— クエリパラメーター、ハッシュフラグメントlocalStorage/sessionStorage— 永続化されるクライアント状態Clipboard— コピー/貼り付け操作Browser History— ナビゲーション状態
制御フローとデータフローを分ける
Wires Out = 制御フロー(何が何をトリガーするか) Returns To = データフロー(出力の送信先)
この分離によって、システムの動作が明確になります。
ナビゲーションはループではなく、インラインで示す
ルーティングは、すべてのページが使う汎用的な仕組みです。すべてのナビゲーションを中央の Router アフォーダンス経由で描くのではなく、発生する箇所に Router navigate() をインラインで示し、遷移先の Place に直接配線します。
ストアは書き込み元ではなく、動作を可能にする場所に配置する
データストアは、そのデータが何らかの効果を可能にするために消費されるPlace に属します。生成される場所ではありません。ほかの Place から書き込むことは、その Place の状態に「手を伸ばす」ことになります。
ストアの配置場所を決めるには:
- 読み取り/書き込みの関係をたどる — 誰が書き込みますか? 誰が読み取りますか?
- 配置場所は読み取り側で決まります — 動作を可能にする場所が配置先です
- 読み取る Place が 1 つだけの場合、ストアはその Place の中に配置します
例:changedPosts 配列は、Modal がユーザーによる変更の確定時に書き込みますが、PAGE_SAVE ハンドラーがユーザーによる Save のクリック時に読み取ります。このストアは PAGE_SAVE ハンドラーとともに配置します。そこで永続化処理を可能にするためです。
本当に共有される場合にのみ、共有領域に分離する
ストアを独立した DATA STORES セクションに置く前に、実際に複数の Place から読み取られていることを確認してください。1 つの Place での動作のみを可能にする場合は、その Place の中に配置します。
ストアは読み取るサブコンポーネントの中に配置する
Place 内では、動作を可能にするサブコンポーネントにストアを配置します。特定のハンドラーがストアを読み取る場合は、Place のレベルに浮かせるのではなく、そのハンドラーのコンポーネント内に配置します。
バックエンドは Place です
データベースやリゾルバーは、どこにも属さないインフラではありません。独自のアフォーダンスを持つ Place です。データベーステーブル(S)は、それらを読み書きするリゾルバー(N)とともに、バックエンド Place の中に配置します。
パーツと関係性のカタログ
このセクションでは、ブレッドボードに表示できるすべての要素を網羅的に説明します。
要素
| 要素 | ID パターン | 説明 | 該当する条件 |
|---|---|---|---|
| 場所 | P1, P2, P3... | インタラクションが行われる境界付きのコンテキスト | ブロッキングテスト:背後にあるものとはインタラクションできない |
| サブ場所 | P2.1, P2.2... | 場所内に定義された部分集合 | より大きな場所内で、関連するアフォーダンスをまとめる |
| 場所の参照 | _PlaceName | 切り離された場所を指す UI アフォーダンス | 複雑な入れ子の場所が別途定義されている |
| UI アフォーダンス | U1, U2, U3... | ユーザーが見たり、操作したりできるもの | 入力欄、ボタン、表示領域、スクロール領域 |
| コードアフォーダンス | N1, N2, N3... | コード内で操作対象にできるもの | メソッド、サブスクリプション、ハンドラー、フレームワークの仕組み |
| データストア | S1, S2, S3... | 保持され、読み書きされる状態 | データを保持するプロパティ、配列、オブザーバブル |
| チャンク | — | 折りたたまれたサブシステム | 入力が 1 つ、出力が 1 つあり、内部要素が複数ある |
| プレースホルダー | — | 対象範囲外のコンテンツを示すマーカー | 詳細を記述せずにコンテキストを示す |
関係性
| 関係 | 構文 | 意味 | 例 |
|---|---|---|---|
| 包含 | Place 列 | アフォーダンスが場所に属する | Place P2.1 内の U3 |
| 出力先 | → X | 制御フロー:トリガーする/呼び出す | → N4、→ P2 |
| 戻り先 | → X(「戻り先」列) | データフロー:出力が送られる先 | → U6、→ N3 |
| 省略フロー | |label| | 中間のステップを省略 | S4 -.-> |view query| U6 |
| 親子関係 | 階層 ID | サブ場所が場所に属する | P2.1 は P2 の子 |
包含と配線の違い
| 関係 | 意味 | 記録場所 |
|---|---|---|
| 包含 | アフォーダンスが場所に属する/場所内に存在する | Place 列(集合のメンバーシップ) |
| 配線 | アフォーダンスが何かをトリガーする/呼び出す | Wires Out 列(制御フロー) |
包含は集合のメンバーシップです:U1 ∈ P1 は、U1 が場所 P1 のメンバーであることを意味します。
配線は制御フローです:U1 → N1 は、U1 が N1 をトリガーすることを意味します。
各要素に該当する条件
場所(P):
- ブロッキングテストに合格する — 背後にあるものとはインタラクションできない
- 例:モーダル、編集モード(画面全体が変化する)、ルート/ページ
- 該当しないもの:ドロップダウン、ツールチップ、フィールドを表示するチェックボックス
場所の参照(_PlaceName):
- 切り離された場所を表す UI アフォーダンス
- 入れ子の場所にアフォーダンスが多数あり、親が煩雑になる場合に使用する
- 例:
_letter-browser、_user-profile-widget - 場所全体の定義に接続する:
_letter-browser --> P3
UI アフォーダンス(U):
- ユーザーが見たり、操作したりできる
- 例:ボタン、入力欄、リスト、スピナー、表示テキスト
- 該当しないもの:ラッパー要素、レイアウトコンテナー
コードアフォーダンス(N):
- 意味のある識別性があり、コード内で指し示せる
- 例:
handleSubmit()、query$ subscription、detectChanges() - 該当しないもの:内部変換、ナビゲーションの仕組み(以下を参照)
データストア(S):
- 読み書きされる状態
- 例:
results配列、loadingブール値、changedPostsリスト - 外部ストア:
Browser URL、localStorage、Clipboard— アプリの境界外にある状態を表す - 該当しないもの:一度設定した後に変化しない設定(設定用アフォーダンスとして検討する)
検証チェック
| チェック | 質問 | いいえの場合... |
|---|---|---|
| データを表示するすべての U | 入力ワイヤー(Wires Out または Returns To 経由)がありますか? | データソースを追加する |
| すべての N | Wires Out または Returns To(あるいはその両方)がありますか? | 調査する — デッドコードか、配線漏れの可能性があります |
| すべての S | そこからデータを読み取るもの(Returns To)がありますか? | 調査する — 使用されていない可能性があります |
| ナビゲーションの仕組み | この N は、ある場所への移動方法にすぎませんか? | 代わりに場所へ直接配線する |
| 副作用のある N | この N は外部状態(URL、ストレージ、クリップボード)に影響しますか? | 外部状態用のストアを追加する |
チャンク化
チャンク化すると、サブシステムがメイン図の単一ノードにまとめられ、詳細は別に表示されます。ブレッドボードの一部に次の特徴がある場合は、複雑さを管理するためにチャンク化を使用します。
- 入力ワイヤーが1本(単一の入口)
- 出力ワイヤーが1本(単一の出力)
- その間に多数の内部要素がある
チャンク化するタイミング
配線をたどると、「ボトルネック」が見つかる箇所を探します。多くの操作要素が単一の入力と単一の出力に集約されている箇所です。こうした箇所は、チャンク化の自然な境界になります。
例:dynamic-form コンポーネントはフォーム定義を受け取り、多数のフィールド(U7a-U7k)を描画し、変更時に検証(N26)を行い、単一の valid$ シグナルを出力します。メイン図では、次のようになります。
N24 -->|formDefinition| dynamicForm
dynamicForm -.->|valid$| U8
チャンク化の方法
- メイン図で、サブシステムをスタジアム型の単一ノードに置き換えます。
dynamicForm[["CHUNK: dynamic-form"]]
- 境界シグナルを使って、チャンクへの配線とチャンクからの配線を行います。
N24 -->|formDefinition| dynamicForm
dynamicForm -.->|valid$| U8
- 境界マーカーを含む内部構造を示す、別のチャンク図を作成します。
flowchart TB
input([formDefinition])
output(["valid$"])
subgraph chunk["dynamic-form internals"]
N25["N25: generateFormConfig()"]
U7a["U7a: field"]
N26["N26: form value changes"]
N27["N27: valid$ emission"]
end
input --> N25
N25 --> U7a
U7a --> N26
N26 --> N27
N27 --> output
classDef boundary fill:#b3e5fc,stroke:#0288d1,stroke-dasharray:5 5
class input,output boundary
- メイン図では、チャンクを独自のスタイルで表示します。
classDef chunk fill:#b3e5fc,stroke:#0288d1,color:#000,stroke-width:2px
class dynamicForm chunk
チャンクの色の規則
| 種類 | 色 | Hex |
|---|---|---|
| チャンクノード(メイン図) | ライトブルー | #b3e5fc |
| 境界マーカー(チャンク図) | ライトブルー、破線 | #b3e5fc、stroke-dasharray:5 5 |
利点
- メイン図の読みやすさを維持 — 複雑なサブシステムが単一のノードになります
- 詳細を保持 — 必要に応じてチャンク図で内部構造を確認できます
- 自然な境界 — チャンクは再利用可能なコンポーネントに対応することがよくあります
視覚化(Mermaid)
表が正です。Mermaid 図は、人が見るための任意の視覚化です。
基本構造
flowchart TB
U1["U1: search input"] --> N1["N1: activeQuery.next()"]
N1 --> N2["N2: subscription"]
N2 --> N3["N3: performSearch"]
N3 --> N4["N4: searchOneCategory"]
N4 -.-> N3
N3 --> N5["N5: loading store"]
N3 --> N6["N6: results store"]
N5 -.-> U2["U2: loading spinner"]
N6 -.-> U3["U3: results list"]
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
class U1,U2,U3 ui
class N1,N2,N3,N4,N5,N6 nonui
線の規則
| 線のスタイル | Mermaid 構文 | 用途 |
|---|---|---|
実線(-->) | A --> B | 出力ワイヤー:呼び出し、トリガー、書き込み |
破線(-.->) | A -.-> B | 戻り先:戻り値、データストアの読み取り |
ラベル付き ... | `A -.-> | ... |
スコープ外のフローを省略する
データフローに、ブレッドボードのスコープに関係のない中間ステップがある場合は、ソースから宛先へ ... ラベルを付けて直接配線し、省略します。
S4 -.->|...| U6
これは「中間ステップを省略して、データがS4からU6に流れる」ことを示します。次の場合に使用します。
- フローは存在するものの、その内部処理がスコープ外である
- クエリーチェーンの詳細を示さずに、データの発生元を示す必要がある
- ブレッドボードが1つのワークフロー(例:編集)に焦点を当てている一方で、別のワークフロー(例:表示)にも言及する必要がある
IDの接頭辞
| 接頭辞 | 種類 | 例 |
|---|---|---|
| P | プレイス | P1, P2, P3 |
| U | UI操作要素 | U1, U2, U3 |
| N | コード操作要素 | N1, N2, N3 |
| S | データストア | S1, S2, S3 |
色の規則
| 種類 | 色 | Hex |
|---|---|---|
| プレイス(サブグラフ) | 白/透明 | — |
| UI操作要素 | ピンク | #ffb6c1 |
| コード操作要素 | グレー | #d3d3d3 |
| データストア | ラベンダー | #e6e6fa |
| チャンク | ライトブルー | #b3e5fc |
| プレイス参照 | ピンク、破線の境界線 | #ffb6c1 |
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
classDef store fill:#e6e6fa,stroke:#9370db,color:#000
classDef chunk fill:#b3e5fc,stroke:#0288d1,color:#000,stroke-width:2px
classDef placeRef fill:#ffb6c1,stroke:#d87093,stroke-width:2px,stroke-dasharray:5 5
サブグラフのラベルとプレイスID
ナビゲーションの配線が正しく接続されるよう、プレイスIDをサブグラフIDとして使用します。
flowchart TB
subgraph P1["P1: CMS Page (Read Mode)"]
U1["U1: Edit button"]
N1["N1: toggleEditMode()"]
end
subgraph P2["P2: CMS Page (Edit Mode)"]
U2["U2: Save button"]
U3["U3: Add button"]
end
%% Navigation wires to Place ID
N1 --> P2
| 種類 | IDのパターン | ラベルのパターン | 目的 |
|---|---|---|---|
| プレイス | P1, P2... | P1: Page Name | ユーザーが訪れる、境界のあるコンテキスト |
| トリガー | — | TRIGGER: Name | フローを開始するイベント(ナビゲーション不可) |
| コンポーネント | — | COMPONENT: Name | 複数のプレイスに登場する、再利用可能なUIとロジック |
| システム | — | SYSTEM: Name | 複数のアプリケーションにまたがる場合 |
重要な点: サブグラフID(P1、P2)は、プレイス表のプレイスIDと一致している必要があります。これにより、N1 --> P2 のようなナビゲーションワイヤーがプレイスの境界に接続されます。
複数のシステムにまたがる場合
flowchart TB
subgraph frontend["SYSTEM: Frontend"]
U1["U1: submit button"]
N1["N1: handleSubmit()"]
end
subgraph backend["SYSTEM: Backend API"]
N10["N10: POST /orders"]
N11["N11: orderService.create()"]
end
U1 --> N1
N1 --> N10
N10 --> N11
ワークフローのステップ注釈(任意)
特定のワークフローをブレッドボード化する際、読者が流れを視覚的に追えるように、番号付きのステップマーカーを任意で追加できます。次のような場合に便利です。
- 図が複雑で、ワークフローの流れが分かりにくい場合
- 特定のユーザージャーニーを案内したい場合
- ブレッドボードを手順説明や学習用ツールとして使う場合
形式:
図の前にワークフローガイドの表を追加します。
| Step | Action | Where to look |
|------|--------|---------------|
| **1** | Click "Edit" button | U1 → N1 → S1 |
| **2** | Edit mode activates | S1 → N2 → U3 |
| **3** | Click "Add" | U3 → N3 → N8 |
Mermaid 図に、スタジアム形のノードを使ってステップマーカーを追加します。
flowchart TB
%% Step markers
step1(["1 - CLICK EDIT"])
step2(["2 - EDIT MODE ON"])
step3(["3 - CLICK ADD"])
%% Connect steps to relevant affordances with dashed lines
step1 -.-> U1
step2 -.-> N2
step3 -.-> U3
%% Style step markers green
classDef step fill:#90EE90,stroke:#228B22,color:#000,font-weight:bold
class step1,step2,step3 step
書式の注意点:
"1 - ACTION"形式を使用します(番号、スペース、ハイフン、スペース、アクション)。"1. ACTION"は避けてください。ピリオドによって Mermaid の Markdown リストパーサーが起動します。"1) ACTION"は避けてください。括弧も解析の問題を引き起こすことがあります。- ステップマーカーは破線(
-.->)でアフォーダンスに接続します。 - ステップは緑色にして、UI(ピンク)および Code(グレー)のアフォーダンスと区別します。
ブレッドボードのスライス化
スライス化では、ブレッドボードのアフォーダンスをグループ化し、垂直の実装スライスに分けます。スライス化の完全な例については、以下の例 Bを参照してください。
入力:
- ブレッドボード(配線付きのアフォーダンステーブル)
- Shape(R + メカニズム)— どのデモが重要かを判断する指針
出力:
- アフォーダンスを V1~V9 のスライスに割り当てたブレッドボード(最大 9 スライス)
垂直スライスとは?
垂直スライスとは、デモ可能な機能を実現する UI と Code のアフォーダンスのグループです。すべてのレイヤー(UI、ロジック、データ)を横断して、動作する増分を提供します。
対照的なのが水平スライスです。水平スライスでは、インターフェースからクリックできない作業(例:「すべてのデータモデルをセットアップする」)を、1 つのレイヤーで行います。
重要な制約
すべてのスライスに、デモできる可視の UI が必要です。 UI のないスライスは水平レイヤーであり、垂直スライスではありません。
- ✅ 「セルフサービス署名パス」(デモ:チェックアウト → 署名 → 署名を確認)
- ❌ 「データベーススキーマ」(デモができない)
デモ可能であるための条件:
- エントリーポイント(UI 操作またはトリガー)がある
- 観測可能な出力(UI の描画、または効果の発生)がある
- R に向けて意味のある進捗を示す
何を「意味のある進捗」とするかは Shape が指針になります。アフォーダンスを恣意的にグループ化するのではなく、メカニズムが機能する様子を示すためにグループ化します。
スライスのサイズ
- 小さすぎる: UI アフォーダンスが 1~2 個だけで、意味のあるデモができない → 関連するスライスと統合します
- 大きすぎる: アフォーダンスが 15 個以上、または複数の無関係なユーザージャーニーが含まれる → 分割します
- 適切なサイズ: 「これを実演します」と明確に示せる、一貫したユーザージャーニー
スライスは 9 個以下を目指してください。それ以上必要な場合は、1 サイクルで扱うには Shape が大きすぎる可能性があります。
将来のスライスへのワイヤー
スライスには、後続のスライスのアフォーダンスを指す Wires Out を持つアフォーダンスが含まれることがあります。これらのワイヤーはブレッドボード上に存在しますが、後続のスライスが実装されるまでは実装されておらず、スタブまたは何もしない処理となります。
これは通常のことです。ブレッドボードはシステム全体を示し、スライス化は実装順序を示します。
手順
ステップ 1:デモ可能な最小の増分を特定する
ブレッドボードと形状を確認します。「中核となる仕組みが動作することを示す、最小のサブセットは何か?」と考えてください。
通常、次のようなものです。
- 中核となるデータ取得
- 基本的なレンダリング
- 検索、ページネーション、状態の永続化はまだ含めない
これを V1 とします。
ステップ 2:追加機能をスライスとして重ねていく
形状に含まれる仕組みを確認します。各スライスでは、仕組みが動作することを示します。
- V2:検索入力(検索の仕組みを示す)
- V3:ページネーション/無限スクロール(ページネーションの仕組みを示す)
- V4:URL 状態の永続化(状態保持の仕組みを示す)
- など
**スライスは最大 9 つです。**それ以上ある場合は、関連する仕組みをまとめてください。単独では意味をなさない機能は、同じスライスに含めてください。
ステップ 3:アフォーダンスをスライスに割り当てる
すべてのアフォーダンスを確認し、そのスライスの仕組みを初めてデモする際に必要となるスライスに割り当てます。
| スライス | 仕組み | アフォーダンス |
|---|---|---|
| V1 | コア表示 | U2, U3, N3, N4, N5, N6, N7 |
| V2 | 検索 | U1, N1, N2 |
| V3 | ページネーション | U10, N11, N12, N13 |
一部のアフォーダンスには、後続のスライスへのワイヤーアウトがある場合がありますが、問題ありません。割り当てられたスライスで実装し、ワイヤーがまだ何もしないだけです。
ステップ 4:スライスごとにアフォーダンスの表を作成する
各スライスについて、追加するアフォーダンスだけを取り出します。
V2:検索が動作する
| # | コンポーネント | アフォーダンス | 操作 | ワイヤーアウト | 戻り先 |
|---|---|---|---|---|---|
| U1 | search-detail | 検索入力 | 入力 | → N1 | — |
| N1 | search-detail | activeQuery.next() | 呼び出し | → N2 | — |
| N2 | search-detail | activeQuery のサブスクリプション | 監視 | → N3 | — |
ステップ 5:スライスごとにデモ文を作成する
各スライスには、R に向けてその仕組みが動作することを示す具体的なデモが必要です。
- V1:「ウィジェットに API からの実データが表示される」
- V2:「「dharma」と入力すると、結果がリアルタイムで絞り込まれる」
- V3:「下にスクロールすると、さらに項目が読み込まれる」
デモは、進捗を示すものとして関係者に見せられる内容にしてください。
Mermaid でスライスを可視化する
各スライスの図にはブレッドボード全体を表示し、スタイルで対象範囲を区別してください。
| カテゴリ | スタイル | 説明 |
|---|---|---|
| このスライス | 明るい色 | 追加するアフォーダンス |
| 構築済み | グレーの塗りつぶし | 以前のスライス |
| 今後の予定 | 透明、破線の枠線 | まだ構築していないもの |
flowchart TB
U1["U1: search input"]
U2["U2: loading spinner"]
N1["N1: activeQuery.next()"]
N2["N2: subscription"]
N3["N3: performSearch"]
U1 --> N1
N1 --> N2
N2 --> N3
N3 --> U2
%% V2 scope (this slice) = green
classDef thisSlice fill:#90EE90,stroke:#228B22,color:#000
%% Already built (V1) = grey
classDef built fill:#d3d3d3,stroke:#808080,color:#000
%% Future = transparent dashed
classDef future fill:none,stroke:#ddd,color:#bbb,stroke-dasharray:3 3
class U1,N1,N2 thisSlice
class U2,N3 built
これにより、関係者は次のことを確認できます。
- 現在構築中のもの(強調表示)
- すでに存在するもの(グレー)
- 今後追加されるもの(薄く表示)
スライスの概要形式
| # | スライス | 仕組み | デモ |
|---|---|---|---|
| V1 | 実データを含むウィジェット | F1, F4, F6 | 「ウィジェットに API から文字が表示される」 |
| V2 | 検索が動作する | F3 | 「入力すると結果が絞り込まれる」 |
| V3 | 無限スクロール | F5 | 「下にスクロールすると、さらに読み込まれる」 |
| V4 | URL 状態 | F2 | 「更新しても検索状態が保持される」 |
「仕組み」列では、形状の各要素を参照し、各スライスでどの仕組みを示すのかを表しています。
例
例 A:既存システムのマッピング
この例では、複数のエントリーポイントを通じてデータがどのように流れるかを理解するために、既存システムをブレッドボーディングします。
入力
理解するワークフロー:「admin_organisation_countries はどのように変更され、その後どのように読み取られるのか?複数のエントリーポイントがあります。手動編集、チェックボックスの切り替え、バッチジョブです。」
出力
UI のアフォーダンス
| # | コンポーネント | アフォーダンス | 操作 | 接続先 | 戻り先 |
|---|---|---|---|---|---|
| U1 | SSO Admin | role_profiles のチェックボックス | 描画 | — | — |
| U2 | SSO Admin | 「Country Admin」チェックボックス | クリック | 選択状態を切り替え | — |
| U3 | SSO Admin | admin_countries の filter_horizontal | 描画 | — | — |
| U4 | SSO Admin | 利用可能な国のリスト | 描画 | — | — |
| U5 | SSO Admin | 選択済みの国のリスト | 描画 | — | — |
| U6 | SSO Admin | 追加 →/削除 ← | クリック | 選択内容を変更 | — |
| U7 | SSO Admin | 保存ボタン | クリック | → N3 | — |
| U20 | DWConnect | 「Country admins」セクション | 描画 | — | — |
| U21 | (不明) | システムメールの「From」フィールド | 描画 | — | — |
コードのアフォーダンス
| # | コンポーネント | アフォーダンス | 操作 | 接続先 | 戻り先 |
|---|---|---|---|---|---|
| N1 | sso/accounts/admin | get_fieldsets() | 呼び出し | → U3(条件付き) | — |
| N2 | sso/accounts/models | get_administrable_user_countries() | 呼び出し | — | → U4 |
| N3 | sso/accounts/admin | save_form() | 呼び出し | → N4、→ N5 | — |
| N4 | Django Admin | フォームの M2M 保存 | 呼び出し | → S2 | — |
| N5 | sso/forms/mixins | _update_user_m2m() | 呼び出し | → S1、→ N6 | — |
| N6 | sso/signals | user_m2m_field_updated シグナル | シグナル | → N10 | — |
| N7 | CLI/Scheduler | manage.py dwbn_cleanup | 実行 | → N15 | — |
| N10 | sso-dwbn-theme | dwbn_user_m2m_field_updated() | 受信 | → N11 | — |
| N11 | sso-dwbn-theme | dwbn_user_m2m_field_updated_task() | 呼び出し | → N12 | — |
| N12 | sso-dwbn-theme | Country Admin が追加され、かつ管理対象の国がゼロか? | 条件分岐 | → N20 | — |
| N15 | sso-dwbn-theme | admin_changes() | 呼び出し | → N16 | — |
| N16 | sso-dwbn-theme | 各 Country Admin について:ホームセンターの国が未設定か? | ループ | → N20 | — |
| N20 | sso-dwbn-theme | ホームセンターの国を取得 | 呼び出し | → N21 | — |
| N21 | sso-dwbn-theme | admin_organisation_countries.add() | 呼び出し | → S2 | — |
| N22 | sso-dwbn-theme | update_last_modified() | 呼び出し | — | — |
| N30 | dwconnect2-backend | findCenterAdmins() | 呼び出し | — | → U20 |
| N31 | sso/api | get_object_data() | 呼び出し | — | → 外部 |
データストア
| # | ストア | 説明 |
|---|---|---|
| S1 | role_profiles | M2M:ユーザーが持つロールプロファイル |
| S2 | admin_organisation_countries | M2M:ユーザーが管理する国 |
| S3 | organisations | ユーザーのホームセンター |
Mermaid 図
flowchart TB
subgraph stores["DATA STORES"]
S1["S1: role_profiles"]
S2["S2: admin_organisation_countries"]
S3["S3: organisations"]
end
subgraph ssoAdmin["PLACE: SSO Admin — User Change Page"]
subgraph permissions["Permissions fieldset"]
U1["U1: role_profiles checkboxes"]
U2["U2: 'Country Admin' checkbox"]
end
subgraph userAdmin["User admin fieldset (superuser only)"]
U3["U3: admin_countries filter_horizontal"]
U4["U4: Available countries"]
U5["U5: Selected countries"]
U6["U6: Add → / Remove ←"]
end
U7["U7: Save button"]
N1["N1: get_fieldsets()"]
N2["N2: get_administrable_user_countries()"]
N3["N3: save_form()"]
N4["N4: Form M2M save"]
N5["N5: _update_user_m2m()"]
N6["N6: user_m2m_field_updated signal"]
N1 -->|is_superuser| userAdmin
U3 --> U4
U3 --> U5
U6 --> U5
N2 -.-> U4
U2 --> U7
U6 --> U7
U7 --> N3
N3 --> N4
N3 --> N5
N5 --> N6
end
subgraph trigger["TRIGGER: Batch Cleanup"]
N7["N7: manage.py dwbn_cleanup"]
end
subgraph theme["sso-dwbn-theme"]
N10["N10: dwbn_user_m2m_field_updated()"]
N11["N11: dwbn_user_m2m_field_updated_task()"]
N12["N12: Country Admin added AND zero admin countries?"]
N15["N15: admin_changes()"]
N16["N16: For each Country Admin: home center country missing?"]
N20["N20: Get home center's country"]
N21["N21: admin_organisation_countries.add()"]
N22["N22: update_last_modified()"]
N6 --> N10
N10 --> N11
N11 --> N12
N7 --> N15
N15 --> N16
N12 -->|yes| N20
N16 -->|yes| N20
N20 --> N21
N21 --> N22
end
subgraph dwconnect["PLACE: DWConnect — Center Page"]
N30["N30: findCenterAdmins()"]
U20["U20: 'Country admins' section"]
N30 --> U20
end
subgraph api["TRIGGER: External API Request"]
N31["N31: get_object_data()"]
end
U21["U21: System email 'From' field"]
N4 --> S2
N5 --> S1
N21 --> S2
S1 -.-> N15
S3 -.-> N16
S3 -.-> N20
S2 -.-> U5
S2 -.-> N30
S2 -.-> N31
S2 -.-> U21
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
classDef store fill:#e6e6fa,stroke:#9370db,color:#000
classDef condition fill:#fffacd,stroke:#daa520,color:#000
classDef trigger fill:#98fb98,stroke:#228b22,color:#000
class U1,U2,U3,U4,U5,U6,U7,U20,U21 ui
class N1,N2,N3,N4,N5,N6,N10,N11,N15,N20,N21,N22,N30,N31 nonui
class N12,N16 condition
class N7 trigger
class S1,S2,S3 store
例 B:形状のある部品から設計する
Part 1:コンテキストの形成(ブレッドボーディングへの入力)
このセクションでは、形成から得られるもの、つまり要件、特定された既存のパターン、スケッチしたパーツを示します。これがブレッドボーディングに渡される入力です。
注: この例では、シェーピングの用語を使用しています。シェーピングでは、要件(Rs)を定義し、再利用する既存のパターンを特定して、パーツやメカニズムとしてソリューションをスケッチします。ブレッドボーディングでは、このように形成されたソリューションを引き継ぎ、具体的なアフォーダンスと接続を詳しく記述します。
R(要件)
| ID | 要件 |
|---|---|
| R0 | インデックスページからコンテンツを検索できるようにする |
| R2 | 詳細ページから戻ったときに、ページネーションの状態に戻る |
| R3 | 詳細ページから戻ったときに、検索の状態に戻る |
| R4 | 検索/ページネーションの状態がページの再読み込み後も保持される |
| R5 | ブラウザーの戻るボタンで、以前の検索/ページネーションの状態を復元する |
| R9 | 検索入力をデバウンスする(キー入力のたびに検索を実行しない) |
| R10 | 検索には最低3文字を必要とする |
| R11 | 読み込み中および空の状態で、ユーザーにフィードバックを提供する |
再利用可能なパターンを備えた既存システム(S-CUR)
アプリには、これらのRsの大半を実装するグローバル検索ページがすでにあります。シェーピングでは、パーツ/メカニズムのレベルで次のように記録されています。
| パーツ | メカニズム |
|---|---|
| S-CUR1 | URL の状態と初期化 |
| S-CUR1.1 | Router の queryParams observable が {q, category} を提供する |
| S-CUR1.2 | initializeState(params) が URL からクエリとカテゴリを設定する |
| S-CUR1.3 | ページの読み込み時に、URL の状態から初期検索を実行する |
| S-CUR2 | 検索入力 |
| S-CUR2.1 | 検索入力が activeQuery BehaviorSubject にバインドされる |
| S-CUR2.2 | activeQuery のサブスクリプションで90msのデバウンスを行う |
| S-CUR2.3 | 3文字以上で performNewSearch() を実行する |
| S-CUR3 | データ取得 |
| S-CUR3.1 | performNewSearch() が読み込み中の状態を設定し、検索サービスを呼び出す |
| S-CUR3.2 | 検索サービスが Typesense フィルターを作成し、rawSearch() を呼び出す |
| S-CUR3.3 | rawSearch() が Typesense にクエリを送り、{found, hits} を返す |
| S-CUR3.4 | 結果が detailResult データストアに書き込まれる |
| S-CUR4 | ページネーション |
| S-CUR4.1 | 一番下までスクロールすると、intercomService 経由で appendNextPage() が実行される |
| S-CUR4.2 | appendNextPage() がページを進め、検索を呼び出す |
| S-CUR4.3 | 新しいヒットが既存のヒットに連結される |
| S-CUR4.4 | sendMessage() がスクロール検出を再設定する |
| S-CUR5 | レンダリング |
| S-CUR5.1 | cdr.detectChanges() がテンプレートの再評価を実行する |
| S-CUR5.2 | 読み込み中のスピナー、「結果なし」、結果数をストアに基づいて表示する |
| S-CUR5.3 | *ngFor が各ヒットのタイルをレンダリングする |
| S-CUR5.4 | タイルをクリックすると詳細ページに移動する |
スケッチしたソリューション:S-CURを応用するパーツ
新しいソリューションのパーツでは、応用するS-CURのパターンを明示的に参照します。
| パーツ | メカニズム | 応用元 |
|---|---|---|
| F1 | ウィジェットを作成する(コンポーネント、定義、登録) | — |
| F2 | URL の状態と初期化(?q= を読み取り、読み込み時に復元) | S-CUR1 |
| F3 | 検索入力(デバウンス、最低3文字、検索を実行) | S-CUR2 |
| F4 | データ取得(フィルター付きの rawSearch()) | S-CUR3 |
| F5 | ページネーション(一番下までスクロール、ページを追加、再設定) | S-CUR4 |
| F6 | レンダリング(読み込み中、空の状態、検索結果リスト、行) | S-CUR5 |
パート 2:ブレッドボーディング(パーツ → アフォーダンスへの変換)
ここでブレッドボーディングを行います。形を整えたパーツを、明示的な接続を持つ具体的なアフォーダンスに変換します。成果物はアフォーダンステーブルと図です。
UI アフォーダンス
| # | コンポーネント | アフォーダンス | 操作 | 出力先 | 戻り先 |
|---|---|---|---|---|---|
| U1 | letter-browser | 検索入力 | 入力 | → N1 | — |
| U2 | letter-browser | 読み込みスピナー | 描画 | — | — |
| U3 | letter-browser | 結果なしメッセージ | 描画 | — | — |
| U4 | letter-browser | 結果数 | 描画 | — | — |
| U5 | letter-browser | 結果リスト | 描画 | → U6, U7, U8, U9 | — |
| U6 | letter-row | 行のクリック | クリック | → LD | — |
| U7 | letter-row | 日付 | 描画 | — | — |
| U8 | letter-row | 件名 | 描画 | — | — |
| U9 | letter-row | ティーザー | 描画 | — | — |
| U10 | letter-browser | スクロール | スクロール | → N11 | — |
| U11 | browser | 戻るボタン | クリック | → N9 | — |
| U12 | letter-browser | 「全 X 件の結果を見る」 | クリック | → LP | — |
| LD | — | レター詳細 | 配置 | — | — |
| LP | — | 全ページ | 配置 | — | — |
コードアフォーダンス
| # | コンポーネント | アフォーダンス | 操作 | 出力先 | 戻り先 |
|---|---|---|---|---|---|
| N1 | letter-browser | activeQuery.next() | 呼び出し | → N2 | → U12 |
| N2 | letter-browser | activeQuery のサブスクリプション | 監視 | → N3 | — |
| N3 | letter-browser | performSearch() | 呼び出し | → N4, → N6, → N7, → N8 | — |
| N4 | typesense.service | rawSearch() | 呼び出し | — | → N3, → N12 |
| N5 | letter-browser | parentId(設定) | 設定 | — | → N4 |
| N6 | letter-browser | loading ストア | 書き込み | — | → N8 |
| N7 | letter-browser | detailResult ストア | 書き込み | — | → N8, → N16 |
| N8 | letter-browser | detectChanges() | 呼び出し | → U2, → U3, → U4, → U5 | — |
| N9 | browser | URL ?q= | 読み取り | → N10 | — |
| N10 | letter-browser | initializeState() | 呼び出し | → N1, → N3 | — |
| N11 | intercom.service | スクロール subject | 監視 | → N12 | — |
| N12 | letter-browser | appendNextPage() | 呼び出し | → N4, → N7, → N8, → N13, → N14 | — |
| N13 | intercom.service | sendMessage() | 呼び出し | → N11 | — |
| N14 | router | navigate() | 呼び出し | — | → N9 |
| N15 | letter-browser | !compact の場合にサブスクライブ | 条件付き | → N11 | — |
| N16 | letter-browser | 切り詰められている場合にリンクを表示 | 条件付き | → U12 | — |
| N17 | letter-browser | compact(設定) | 設定 | — | → N4, → N15, → N16 |
| N18 | letter-browser | fullPageRoute(設定) | 設定 | — | → U12 |
Mermaid 図
flowchart TB
subgraph lettersIndex["PLACE: Letters Index Page"]
subgraph letterBrowser["COMPONENT: letter-browser"]
U1["U1: search input"]
U2["U2: loading spinner"]
U3["U3: no results msg"]
U4["U4: result count"]
U5["U5: results list"]
U12["U12: See all X results"]
N1["N1: activeQuery.next"]
N2["N2: activeQuery sub"]
N3["N3: performSearch"]
N6["N6: loading store"]
N7["N7: detailResult store"]
N8["N8: detectChanges"]
N10["N10: initializeState"]
N16["N16: if truncated show link"]
N5["N5: parentId (config)"]
N17["N17: compact (config)"]
N18["N18: fullPageRoute (config)"]
subgraph pagination["PAGINATION"]
U10["U10: scroll"]
N15["N15: if !compact subscribe"]
N12["N12: appendNextPage"]
end
end
end
subgraph letterRow["COMPONENT: letter-row"]
U6["U6: row click"]
U7["U7: date"]
U8["U8: subject"]
U9["U9: teaser"]
end
subgraph browser["BROWSER"]
U11["U11: back button"]
N9["N9: URL ?q="]
N14["N14: Router.navigate"]
end
subgraph services["SERVICES"]
N4["N4: rawSearch"]
N11["N11: intercom subject"]
N13["N13: sendMessage"]
end
subgraph letterDetail["PLACE: Letter Detail Page"]
LD["Letter Detail"]
end
U1 -->|type| N1
N1 --> N2
N2 -->|debounce 90ms, min 3| N3
N3 --> N4
N3 --> N6
N3 --> N7
N3 --> N8
N4 -.-> N3
N4 -.-> N12
N6 -.-> N8
N7 -.-> N8
N8 --> U2
N8 --> U3
N8 --> U4
N8 --> U5
U5 --> U6
U5 --> U7
U5 --> U8
U5 --> U9
U6 -->|navigate| LD
U11 -->|restore| N9
N9 --> N10
N10 --> N1
N10 --> N3
U10 --> N11
N15 -->|if !compact| N11
N11 --> N12
N12 --> N4
N12 --> N7
N12 --> N8
N12 --> N13
N12 --> N14
N13 -->|re-arm| N11
N5 -.->|filter| N4
N17 -.-> N4
N17 -.-> N15
N17 -.-> N16
N18 -.-> U12
N14 -.->|URL| N9
N1 -.-> U12
N7 -.-> N16
N16 -->|if truncated| U12
U12 -->|navigate with ?q| LP["Full Page"]
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
class U1,U2,U3,U4,U5,U6,U7,U8,U9,U10,U11,U12,LD,LP ui
class N1,N2,N3,N4,N5,N6,N7,N8,N9,N10,N11,N12,N13,N14,N15,N16,N17,N18 nonui
ブレッドボードのスライス
ブレッドボード全体が完成したら、縦方向の段階に分割します。各スライスでは、機構が動作する様子を示します。
スライスの概要
| # | スライス | 機構 | アフォーダンス | デモ |
|---|---|---|---|---|
| V1 | 実データを使うウィジェット | F1, F4, F6 | U2-U9, N3-N8, LD | 「ウィジェットに実データが表示される」 |
| V2 | 検索が機能する | F3 | U1, N1, N2 | 「『dharma』と入力すると、結果が絞り込まれる」 |
| V3 | 無限スクロール | F5 | U10, N11-N13 | 「下にスクロールすると、さらに読み込まれる」 |
| V4 | URL の状態 | F2 | U11, N9, N10, N14 | 「更新しても検索内容が保持される」 |
| V5 | コンパクトモード | — | U12, N15-N18, LP | 「『すべて見る』リンクが表示される」 |
スライス図
flowchart TB
subgraph slice1["V1: WIDGET WITH REAL DATA"]
U2["U2: loading spinner"]
U3["U3: no results msg"]
U4["U4: result count"]
U5["U5: results list"]
U6["U6: row click"]
U7["U7: date"]
U8["U8: subject"]
U9["U9: teaser"]
N3["N3: performSearch"]
N4["N4: rawSearch"]
N5["N5: parentId (config)"]
N6["N6: loading store"]
N7["N7: detailResult store"]
N8["N8: detectChanges"]
LD["Letter Detail"]
end
subgraph slice2["V2: SEARCH WORKS"]
U1["U1: search input"]
N1["N1: activeQuery.next"]
N2["N2: activeQuery sub"]
end
subgraph slice3["V3: INFINITE SCROLL"]
U10["U10: scroll"]
N11["N11: intercom subject"]
N12["N12: appendNextPage"]
N13["N13: sendMessage"]
end
subgraph slice4["V4: URL STATE"]
U11["U11: back button"]
N9["N9: URL ?q="]
N10["N10: initializeState"]
N14["N14: Router.navigate"]
end
subgraph slice5["V5: COMPACT MODE"]
U12["U12: See all X results"]
N15["N15: if !compact subscribe"]
N16["N16: if truncated show link"]
N17["N17: compact (config)"]
N18["N18: fullPageRoute (config)"]
LP["Full Page"]
end
U1 -->|type| N1
N1 --> N2
N2 -->|debounce| N3
N3 --> N4
N3 --> N6
N3 --> N7
N3 --> N8
N4 -.-> N3
N5 -.->|filter| N4
N6 -.-> N8
N7 -.-> N8
N8 --> U2
N8 --> U3
N8 --> U4
N8 --> U5
U5 --> U6
U5 --> U7
U5 --> U8
U5 --> U9
U6 -->|navigate| LD
U11 -->|restore| N9
N9 --> N10
N10 --> N1
N10 --> N3
U10 --> N11
N11 --> N12
N12 --> N4
N12 --> N7
N12 --> N8
N12 --> N13
N12 --> N14
N13 -->|re-arm| N11
N4 -.-> N12
N14 -.->|URL| N9
N15 -->|if !compact| N11
N17 -.-> N4
N17 -.-> N15
N17 -.-> N16
N18 -.-> U12
N1 -.-> U12
N7 -.-> N16
N16 -->|if truncated| U12
U12 -->|navigate with ?q| LP
style slice1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style slice2 fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
style slice3 fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style slice4 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style slice5 fill:#fff8e1,stroke:#ffc107,stroke-width:2px
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
class U1,U2,U3,U4,U5,U6,U7,U8,U9,U10,U11,U12,LD,LP ui
class N1,N2,N3,N4,N5,N6,N7,N8,N9,N10,N11,N12,N13,N14,N15,N16,N17,N18 nonui