PlantUMLでWebの3層構造を描く、失敗例を交えながら

## はじめに

みなさん、UMLでプログラム設計図を書いていますか？

去年のアドベントカレンダーでは、[Pythonソースコードを読み込んでオブジェクト図のplantumlを生成するPythonスクリプト](https://qiita.com/shiracamus/items/f3331752848520358dae)を作りました。
今年もplantumlネタでいきます。

私はこれまでに簡単なUML図をplantumlでいろいろ描いてきました。
最近、Webの3層構造を描く機会があったのですが、思いどおりに描くのに苦労したので、原因を交えながら解説します。

最終的に描けた図はこれです。

```plantuml
@startuml

left to right direction

together {
  actor "利用者" as user

  card Client {
    package "Webクライアント" as webclient {
      component "Webブラウザ" as webbrowser
      file "HTML\n(コピー)" as htmlcopy
      component "JavaScript\n(コピー)" as jscopy
    }
  }

  card Server {
    package "Webサーバ" as webserver {
      interface "web:80" as webport
      component "Webサービス" as webservice
      file "HTML" as html
      component "JavaScript" as js
    }
    package "APIサーバ" as apiserver {
      interface "api:8888" as apiport
      component "APIサービス" as apiservice
      interface "websocket:8889" as wsport
    }
    package "DBサーバ" as dbserver {
      database "data"
    }
  }
}

htmlcopy -- jscopy
jscopy - apiport
jscopy -- wsport
wsport -- apiservice
apiport -- apiservice
apiservice -- dbserver

htmlcopy - webbrowser
user -- webbrowser
webbrowser -- webport
webport -- webservice
webservice -- html
html -- js

@enduml
```

## 部品を並べる

まずは、[plantumlの配置図](https://plantuml.com/ja/deployment-diagram)の要素を使って部品を並べてみました。

```text
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
  package "APIサーバ" as apiserver {
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
    component "APIサービス" as apiservice
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
}

@enduml
```

Qiitaでは、コードブロックを使って言語にplantumlを指定する（コードを<code>\`\`\`plantuml</code> と <code>\`\`\`</code> で囲む）と、上のコードを下図に変換してくれます。なお、Qiitaでは、`@startuml` と `@enduml` を書く必要はないようです。

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
  package "APIサーバ" as apiserver {
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
    component "APIサービス" as apiservice
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
}

@enduml
```

テキストで書いた順序とは無関係に部品が並んでいますが、配線すれば位置が自動調整されるはずです。

## 枠内を配線する

次に、card内やpackage内の配線をしてみます。
plantumlのコードは、Qiitaの「`...`」メニューにある「Markdownで本文を見る」でご確認ください。

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
    webbrowser - htmlcopy
    htmlcopy - jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
    webport - webservice
    webservice - html
    html - js
  }
  package "APIサーバ" as apiserver {
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
    component "APIサービス" as apiservice
    apiport - apiservice
    wsport - apiservice
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
}

@enduml
```

「websocket」から「APIサービス」への線が大きく迂回した曲線になってしまいました。
横並びにならないように「websocket:8889」を下側に配置してみます。

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
    webbrowser - htmlcopy
    htmlcopy - jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
    webport - webservice
    webservice - html
    html - js
  }
  package "APIサーバ" as apiserver {
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
    component "APIサービス" as apiservice
    apiport - apiservice
    apiservice -- wsport
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
}

@enduml
```

## 枠間を接続する

枠をまたがる配線を追記します。

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
    webbrowser - htmlcopy
    htmlcopy - jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
    webport - webservice
    webservice - html
    html - js
  }
  package "APIサーバ" as apiserver {
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
    component "APIサービス" as apiservice
    apiport - apiservice
    apiservice -- wsport
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
  apiservice -right- dbserver
}

user - webbrowser
webbrowser - webport
jscopy - apiport
jscopy - wsport

@enduml
```

「利用者」が一番右のままですし、その他の位置関係もおかしいです。
試行錯誤してみたところ、配線する順序が重要で、card や component の中に配線を記述せずに、配線定義は定義順序を気にしながら最後にまとめて記述するのがよさそうです。
Server内のcomponent定義も逆順にしてみます。

## 配線定義を最後にまとめる

まとめた配線定義を以下に示します。

```
user - webbrowser

webbrowser - htmlcopy
htmlcopy - jscopy
webbrowser - webport
jscopy - apiport
jscopy - wsport

webport - webservice
webservice - html
html - js

apiport - apiservice
apiservice -- wsport
apiservice - dbserver
```

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "DBサーバ" as dbserver {
    database "data"
  }
  package "APIサーバ" as apiserver {
    component "APIサービス" as apiservice
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
  }
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
}

user - webbrowser

webbrowser - htmlcopy
htmlcopy - jscopy
webbrowser - webport
jscopy - apiport
jscopy - wsport

webport - webservice
webservice - html
html - js

apiport - apiservice
apiservice -- wsport
apiservice - dbserver

@enduml
```
「Webブラウザ」と「Webサーバ」の２つを上段に移動すれば、配線が綺麗になりそうに思えます。

## 2段構成にする

縦配線を使って配置位置を調整してみます。

```
user - webbrowser
webbrowser - webport
webport - webservice
webservice - html
html - js
apiport - apiservice

webserver -[hidden]- apiserver

webbrowser -- htmlcopy
htmlcopy - jscopy
jscopy - apiport
jscopy -- wsport
apiservice -- wsport
apiservice - dbserver
```

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "DBサーバ" as dbserver {
    database "data"
  }
  package "APIサーバ" as apiserver {
    component "APIサービス" as apiservice
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
  }
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
}

user - webbrowser
webbrowser - webport
webport - webservice
webservice - html
html - js
apiport - apiservice

webserver -[hidden]- apiserver

webbrowser -- htmlcopy
htmlcopy - jscopy
jscopy - apiport
jscopy -- wsport
apiservice -- wsport
apiservice - dbserver

@enduml
```

配置的には思いどおりなのですが、

* 「Webブラウザ」から「web:80」への接続線
* 「JavaScript(コピー)」から「api:8888」と「websocket:8889」への接続線

が大きく迂回しているのが気になります。直線で結びたいところです。
ですが、いろいろ試してみもうまくいかず、Qiitaの質問箱でも質問してみました。
質問回答を待つ間に、いろいろ調べて試行錯誤試してみたところ、私の plantuml の理解が足りていないことが分かってきました。

## 描画方向指定

PlantUML の描画方向指定には「`top to bottom direction`」と「`left to right direction`」があります。
普通に部品を並べると横方向に並ぶので「`left to right direction`」が標準なのかと思っていたのですが、方向指定がないと「`top to bottom direction`」になります。
どうやら、`--` で結んだときの配置方向と考えるのがよさそうです。`-` で結んだときだけ配置の方向が変わります。

今回の図では、横方向の配線が多いので「`top to left direction`」を指定して、基本は `--` で接続し、縦に配線するときに `-` にするのが良さそうです。

| 部品・接続線 ＼ 方向指定| top to bottom<br>（上から下へ） | left to right<br>（左から右へ） |
| --- | --- | --- |
| 部品 | 横方向に列が揃う | 縦方向に列が揃う  |
| `-` | 横    | 縦  |
| `--` （基準） | 縦  | 横 |
| `---` （`-`の数だけ遠くに配置） | 縦  | 横 |

## left to right direction にして Web三層構造を描く

```plantuml
@startuml

left to right direction

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "DBサーバ" as dbserver {
    database "data"
  }
  package "APIサーバ" as apiserver {
    component "APIサービス" as apiservice
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
  }
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
}

user -- webbrowser
'Client
webbrowser -- webport
htmlcopy - webbrowser
htmlcopy -- jscopy
jscopy -- apiport
jscopy -- wsport
'Server
webport -- webservice
webservice -- html
html -- js
apiport -- apiservice
wsport -- apiservice
apiservice -- dbserver

@enduml
```

「Webブラウザ」と「Webサーバ」を上側に配置したいのですが、下側に配置されてしまいました。
上段と下段を入れ替えればいいだけなので、接続方向（left, right, up, down）を指定すればいいだろうと思って試行錯誤したのですが、どうにもうまくいきません。
以下の図までは描けて、あとは「HTML(コピー)」と「JavaScript(コピー)」を接続すれば完成です。

```plantuml
@startuml

left to right direction

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
  package "APIサーバ" as apiserver {
    component "APIサービス" as apiservice
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
}

user -- webbrowser
webbrowser -- webport
webport -- webservice
webservice -- html
html -- js

apiport -- apiservice
wsport -- apiservice
apiservice -- dbserver

jscopy -- apiport
jscopy -- wsport

htmlcopy - webbrowser
'htmlcopy -- jscopy  この定義を有効にすると1段目と2段目が入れ替わってしまう

@enduml
```

いざ「`htmlcopy -- jscopy`」の接続定義を入れると、先ほどの図と同じく上段と下段が入れ替わってしまうのです。
「JavaScript(コピー)」を右側ではなく下側に配置するように「`htmlcopy - jscopy`」の縦接続定義にすると下図のように上段・下段の配置は思い通りに描画できました。

```plantuml
@startuml

left to right direction

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
  package "APIサーバ" as apiserver {
    component "APIサービス" as apiservice
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
  }
  package "DBサーバ" as dbserver {
    database "data"
  }
}

user -- webbrowser
webbrowser -- webport
webport -- webservice
webservice -- html
html -- js

apiport -- apiservice
wsport -- apiservice
apiservice -- dbserver

jscopy -- apiport
jscopy -- wsport

htmlcopy - webbrowser
'htmlcopy -- jscopy
'上の定義ではうまくいかなかったので下の定義に変更
jscopy - htmlcopy

@enduml
```

この図で意図が伝わるので、これで完成！
にしてもいいのですが、最初に思い描いた配置にできないのが悔しいので、挑戦を続けます。

## together を使う

部品を近くに配置する together を使えばいいのではないかと考え、試行錯誤してみました。
そしてできあがったのが以下の定義で、最初に示した図の完成です。

```
@startuml

left to right direction

together {
  actor "利用者" as user

  card Client {
    package "Webクライアント" as webclient {
      component "Webブラウザ" as webbrowser
      file "HTML\n(コピー)" as htmlcopy
      component "JavaScript\n(コピー)" as jscopy
    }
  }

  card Server {
    package "Webサーバ" as webserver {
      interface "web:80" as webport
      component "Webサービス" as webservice
      file "HTML" as html
      component "JavaScript" as js
    }
    package "APIサーバ" as apiserver {
      interface "api:8888" as apiport
      component "APIサービス" as apiservice
      interface "websocket:8889" as wsport
    }
    package "DBサーバ" as dbserver {
      database "data"
    }
  }
}

htmlcopy -- jscopy
jscopy - apiport
jscopy -- wsport
wsport -- apiservice
apiport -- apiservice
apiservice -- dbserver

htmlcopy - webbrowser
user -- webbrowser
webbrowser -- webport
webport -- webservice
webservice -- html
html -- js

@enduml
```

```plantuml
@startuml

left to right direction

together {
  actor "利用者" as user

  card Client {
    package "Webクライアント" as webclient {
      component "Webブラウザ" as webbrowser
      file "HTML\n(コピー)" as htmlcopy
      component "JavaScript\n(コピー)" as jscopy
    }
  }

  card Server {
    package "Webサーバ" as webserver {
      interface "web:80" as webport
      component "Webサービス" as webservice
      file "HTML" as html
      component "JavaScript" as js
    }
    package "APIサーバ" as apiserver {
      interface "api:8888" as apiport
      component "APIサービス" as apiservice
      interface "websocket:8889" as wsport
    }
    package "DBサーバ" as dbserver {
      database "data"
    }
  }
}

htmlcopy -- jscopy
jscopy - apiport
jscopy -- wsport
wsport -- apiservice
apiport -- apiservice
apiservice -- dbserver

htmlcopy - webbrowser
user -- webbrowser
webbrowser -- webport
webport -- webservice
webservice -- html
html -- js

@enduml
```

## top to bottom direction での迂回曲線解決

その後、いろいろ試行錯誤してみたところ、top to bottom direction でも線が大きく迂回しない図を描けるようになりました。
原因は、「Webサーバ」と「APIサーバ」の大枠をhidden線で結んだのがまずかったようです。
webポートとapiポートをhidden線で結ぶことで直線に近い線になりました。直線にならないのが残念です。

```plantuml
@startuml

actor "利用者" as user

card Client {
  package "Webクライアント" as webclient {
    component "Webブラウザ" as webbrowser
    file "HTML\n(コピー)" as htmlcopy
    component "JavaScript\n(コピー)" as jscopy
  }
}

card Server {
  package "DBサーバ" as dbserver {
    database "data"
  }
  package "APIサーバ" as apiserver {
    component "APIサービス" as apiservice
    interface "api:8888" as apiport
    interface "websocket:8889" as wsport
  }
  package "Webサーバ" as webserver {
    interface "web:80" as webport
    component "Webサービス" as webservice
    file "HTML" as html
    component "JavaScript" as js
  }
}

user - webbrowser

webbrowser - webport
webport - webservice
webservice - html
html - js
apiport - apiservice

webport -[hidden]- apiport

webbrowser -- htmlcopy
htmlcopy - jscopy
jscopy - apiport
jscopy -- wsport
apiservice -- wsport
apiservice - dbserver

@enduml
```

## さいごに

図は描けたのですが、もっとスマートに書ける方法があると期待しています。
もっといい描き方をご存知でしたら、是非ご教示ください。

## 参考資料

* 配置図の構文と機能: https://plantuml.com/ja/deployment-diagram