前回は、Obsidian版でまずER図とクラス図を形にした話を書いた。
今回は、その中で見えてきた 「Relationの正本をどこに置くか」 という話を整理してみたい。
ER図、クラス図、DFDは、どれも見た目だけなら「箱と線」で表現できる。
テーブルやクラスやプロセスを箱として置き、それらを線でつなぐ。
見た目だけで言えば、かなり似ている。
ただ、実際にフォーマットとして設計しようとすると、その「線」が表している意味はかなり違う。
ER図の線は、主にテーブル間の外部キーに近い。
クラス図の線は、関連、依存、継承、集約、実装などを表す。
DFDの線は、データがどこからどこへ流れるかを表す。
同じRelationとして一括りにしたくなるが、Model Weaveではあえて持ち方を揃えすぎないことにした。
今回は、ER・Class・DFDで、RelationやFlowの正本をどう置き分けたのかを書いてみる。
図に出てくる「線」は全部同じではなかった
モデリングツールを作っていると、最初は図に出てくる線を共通化したくなる。
「fromがあって、toがあって、kindがあって、labelがある」
という形にすれば、ERでもClassでもDFDでも同じように扱えそうに見える。
実際、内部的な描画処理だけを考えるなら、それでもある程度は動く。
画面上に箱を置き、線を引き、ラベルを出す。
そのレベルでは共通化できる部分も多い。
ただ、Model Weaveでやりたいのは、単に図を描くことではない。
Markdownを正本として残し、図やPreviewやPNGはそこから生成される派生物として扱うことだ。READMEでも、Markdown model filesをsource of truthとし、diagrams、previews、diagnostics、PNG exportsはMarkdownから生成される派生出力として整理している。
そうなると大事なのは、図に線を出せるかどうかではなく、その線の意味をどこに書いておくのが自然かになる。
全部をdiagramファイルに書けば、図単位ではわかりやすい。
でもその場合、関係の正本が図の中に閉じやすくなる。
逆に、全部を単体オブジェクト側に書けば、再利用はしやすい。
でもdiagram固有の説明や、その図の中だけで見せたい関係を表しにくくなる。
このバランスを考えると、「Relationは常にここに置く」と一律で決めるより、図の種類ごとに自然な置き場所を決めた方がよいと感じた。
ERでは、関係をEntity側に持たせた
ERでは、RelationはEntity側に持たせるのが自然だった。
ERの主役は、やはりテーブル定義だ。
どんなカラムを持つのか。
どのカラムが主キーなのか。
どのインデックスを持つのか。
そして、どのテーブルを参照しているのか。
この情報は、図の都合というよりテーブル定義そのものに近い。
そのため、er_entity ではColumns、Indexes、Relationsを持ち、Relationsは自テーブルから出るoutbound relationのみを正本として持つ方針にした。
たとえば、注文テーブルが顧客テーブルを参照するなら、その関係は注文テーブル側に書く。
注文テーブルの定義を読めば、「このテーブルは顧客を参照している」とわかる。
この方が、ERの設計資産としては自然だと思った。
一方で、er_diagram の方はRelationを直接持たない。er_diagram は、ER図に含めるEntityを列挙し、表示時に対象EntityからRelationを集約してedgeを生成する。
つまり、ERではこういう役割分担になる。
er_entity
- Columns
- Indexes
- outbound Relations
er_diagram
- 表示対象のEntity一覧
- Relationは持たず、Entityから集約して表示
この形にすると、Entity定義が正本として強くなる。
diagramは、あくまで「どのEntity群を見たいか」を指定するビューになる。
また、inbound relationは自分で二重に書かない。
必要ならプラグイン側で逆引きして表示すればよい。
同じ関係を両側に書き始めると、必ずどこかでズレる。
それを避ける意味でも、ERではoutboundだけを正本にするのが扱いやすかった。
Classでは、単体定義とDiagram定義の両方に意味があった
Classでは、ERほど単純にはいかなかった。
クラスにも、自分自身を起点とする自然な関係がある。
たとえば、OrderService が OrderRepository を使う、Order が OrderItem を持つ、といった関係は、そのクラス自身の定義に書いておくのが自然だ。
そのため、class のRelationsは、そのファイル自身を起点とする関係だけを書く形にした。
Spec04では、class のRelationsから from 列を削除し、from は常にそのファイル自身の id とみなす方針に整理している。
これはかなり大きな整理だった。
以前のように、class単体ファイルの中で from と to を両方持てるようにすると、そのファイル自身とは関係のないRelationまで書けてしまう。
すると、どこが正本なのかわかりにくくなる。
そこで、class単体ファイルではこう考えることにした。
このclassファイルに書くRelationは、
このclass自身から出る関係だけ
一方で、クラス図にはクラス図の事情がある。
特定のdiagramの中で、説明上見せたい関係がある。
アプリケーション層だけを見せたい図、ドメインモデルを見せたい図、依存関係を見せたい図。
図の目的によって、同じクラス群でも見せたいRelationが変わることがある。
そのため、class_diagram では、対象オブジェクトと関係性一覧を持てるようにしている。
Class単体定義は別の class ファイルで管理し、diagramファイルでは対象オブジェクトと関係性一覧を保持する方針だ。
つまりClassでは、こういう分け方になる。
class
- そのclass自身の属性・メソッド
- そのclass自身から出るRelation
class_diagram
- 図に含めるclass
- 図全体として見せたいRelation
ERと比べると、Classではdiagram側にもRelationを持たせる余地を残している。
これは不統一にも見えるが、実際にはその方が自然だった。
ERのRelationは、テーブル定義と強く結びついている。
一方、ClassのRelationは、設計の見せ方によって切り取り方が変わりやすい。
だから、単体定義側とdiagram側の両方に役割を持たせた。
DFDでは、FlowそのものがDiagramの主役になった
DFDでは、さらに考え方が変わる。
DFDの主役は、外部実体やプロセスやデータストアそのものではなく、どのデータが、どこからどこへ流れるかだ。
そのため、Flowの正本は dfd_diagram 側の ## Flows に置く方針にした。
これはERとはかなり違う。
ERでは、RelationはEntity側に置いた。
でもDFDで同じことをやろうとすると、かなり不自然になる。
たとえば、あるプロセスが複数の外部実体やデータストアとデータをやり取りする場合、そのFlowを各dfd_object側に分散して書くと、全体の流れが見えにくくなる。
DFDで見たいのは、個別のオブジェクトの性質というより、データの通り道そのものだ。
そのため、dfd_object は外部実体・プロセス・データストアの単体要素を定義する部品として扱い、データフローそのものは持たない。DFDの流れは dfd_diagram 側で定義し、dfd_object は再利用可能な部品として扱う方針だ。
整理すると、DFDではこうなる。
dfd_object
- 外部実体
- プロセス
- データストア
- Flowは持たない
dfd_diagram
- Objects
- Flows
- データがどこからどこへ流れるか
また、DFDではdiagram内だけで使うlocal objectも重要になる。
初期のシステム間連携図や、かなりラフなデータフローを描きたいときに、いちいちすべての外部実体やプロセスを個別ファイル化するのは重い。
そこで、dfd_diagram.Objects では、外部 dfd_object 参照とdiagram内local objectの両方を扱えるようにしている。
ここは、ERやClassとは違うDFDらしさだと思う。
DFDは、詳細な部品定義よりも、まず流れを軽く描きたい場面が多い。
そのため、Flowはdiagram側に置き、Objectは必要に応じて外部定義にもlocal objectにもできる形にした。
無理に共通化しない方が、むしろ設計しやすかった
最初は、Relationの持ち方をできるだけ共通化した方がよいのではないかと思っていた。
たしかに、形式が揃っていると実装はわかりやすく見える。from、to、kind、label のような列を共通にすれば、パーサーもレンダラーも整理しやすそうに思える。
ただ、実際にはそこまで単純ではなかった。
ERでは、Relationはテーブル定義の一部としてEntity側に置くのが自然だった。
Classでは、自分自身から出るRelationはclass側に置きつつ、図全体として見せたいRelationはdiagram側にも置ける方が自然だった。
DFDでは、Flowそのものがdiagramの主役なので、diagram側に置くのが自然だった。
つまり、同じ「線」に見えても、意味が違う。
ここを無理に共通フォーマットへ押し込むと、表面上は整って見えるかもしれない。
しかし、実際にMarkdownを書いたときに不自然になる。
書く場所に迷う。
同じ情報を重複して書く。
あとから正本がどこかわからなくなる。
Model Weaveでは、それを避けたかった。
共通化するべきなのは、「すべてのRelationを同じ列構成で書くこと」ではない。
共通化するべきなのは、もっと上位の考え方だと思う。
それは、Markdownを正本にし、図は派生ビューにするという考え方だ。
Markdownの中で、意味として自然な場所に情報を書く。
図はそこから生成する。
この順番を守るなら、ER・Class・DFDで持ち方が違っていても問題ない。
むしろ、その方がテキストとして読みやすい。
図の都合ではなく、設計情報の置き場所から考える
今回の設計で学んだのは、図を先に考えすぎると、設計情報の置き場所を間違えやすいということだった。
「図に線を出したい」から考えると、全部をdiagram側に書きたくなる。
それは実装としてはわかりやすい。
でも、長く運用する設計資産としては、少し弱い。
逆に、「この情報は何の定義に属しているのか」から考えると、置き場所が見えてくる。
外部キーなら、参照元のEntityに置く。
クラス自身の依存なら、そのClassに置く。
図の文脈で説明したい関係なら、class_diagramに置く。
データの流れそのものなら、dfd_diagramに置く。
この判断は、Model Weaveのフォーマットを考えるうえでかなり重要だった。
テキストベースの設計では、書きやすさも大事だが、あとから読んだときに迷わないことも同じくらい大事だ。
どこを見れば正しい情報があるのか。
どこを直せば図に反映されるのか。
どこに書くと重複になるのか。
このあたりが曖昧だと、結局はExcel設計書と同じように、見た目は存在しているが再利用しにくい資産になってしまう。
だからこそ、Relationの正本をどこに置くかは、単なる実装都合ではなく、ツールの思想に関わる話だった。
次は、DFDをどう軽く描けるようにするか
ここまで整理して、ER・Class・DFDでRelationやFlowの持ち方を変える理由はだいぶ見えてきた。
ERではEntity側。
Classでは単体定義とdiagram側。
DFDではdiagram側のFlows。
この違いは、かなり大事だと思っている。
特に次に扱うDFDでは、この考え方がさらに重要になる。
DFDは、ERやClassと比べて、もっとラフに描き始めたい場面が多い。
システム間連携図のように、まず名前だけの外部システムやプロセスを置いて、どんなデータが流れるかを見たいことがある。
そのためには、すべての部品を最初から個別ファイル化するより、diagram内local objectを使って軽く描ける方がよい。
一方で、再利用したい外部実体やプロセスは dfd_object として切り出せた方がよい。
つまりDFDでは、軽さと再利用性のバランスがかなり重要になる。
次回は、Model WeaveでDFDをどう扱うことにしたのかを書いてみたい。
特に、Mermaid-firstに寄せた理由、local objectを正式に扱うことにした理由、そして「軽く描けるDFD」をどう考えたのかを整理していくつもりだ。

コメント