😀

PlantUML デザインパターン

2020/05/20に公開

はじめに

PlantUMLは、テキストベースでUMLを記述するための、おそらく世界で最も普及したツール。

テキストベースなので、gitを使ったUMLの変更管理などが容易で、マウスを使わずにUMLをさくっと書けるメリットがある。
ここでは、PlantUMLを記述する上で参考になるデザインパターンを紹介する。

  1. パターン未適用
    デザインパターン未適用の状態のPlantUMLファイルとその課題を述べる。
  2. パターン適用
    デザインパターンを適用したPlantUMLファイルとその利点を述べる。

Skinparam分離パターン

パターン未適用

PlantUMLファイルのデザインの設定(sksinparam)を個別のクラス図ファイルの中に記載している。
ファイルが多くなるほど、共通的なデザインの設定を管理しにくくなる。

skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true
left to right direction

package shop {
  class Shop
  Shop *-- Item
}
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true

package customer {
  class Customer
  Customer --> Item : buy
}
shop-class.puml
# 共通設定
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true
# 個別設定
left to right direction

package shop {
  class Shop
  Shop *-- Item
}
customer-class.puml
# 共通設定
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true

package customer {
  class Customer
  Customer --> Item : buy
}

クラスが増えるほどファイルが肥大化してメンテナンス性が劣化する。

パターン適用

共通設定を 専用ファイル skinparam.puml に切り出し、図毎に個別に設定する個別設定だけを各ファイルの中で管理する。
以下の例では、3つの共通設定を skinparam.puml に切り出し、1つの個別設定だけをshopクラスのファイルshop-class.pumlで管理する。

# 共通設定
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true
# 個別設定
left to right direction
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true
left to right direction

package shop {
  class Shop
  Shop *-- Item
}
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true

package customer {
  class Customer
  Customer --> Item : buy
}
skinparam.puml
# 共通設定
skinparam linetype ortho
skinparam shadowing false
skinparam handwritten true
shop-class.puml
# 共通設定
!include skinparam.puml
# 個別設定
left to right direction

package shop {
  class Shop
  Shop *-- Item
}
customer-class.puml
# 共通設定
!include skinparam.puml

package customer {
  class Customer
  Customer --> Item : buy
}

クラス増加時にもファイルが肥大化しにくく、メンテナンス性を維持できる。

Packageパターン

パターン未適用

1つのPlantUmlファイルに複数のパッケージが定義される。

package shop {
  class Shop
  Shop *-- Item
}

package customer {
  class Customer
  Customer --> Item : buy
}
パターン未適用
package shop {
  class Shop
  Shop *-- Item
}

package customer {
  class Customer
  Customer --> Item : buy
}

クラスが増えるほどファイルが肥大化してメンテナンス性が劣化する。

パターン適用

パッケージ毎にファイルを分ける。

package shop {
  class Shop
  Shop *-- Item
}
package customer {
  class Customer
  Customer --> Item : buy
}
shop.puml
package shop {
  class Shop
  Shop *-- Item
}
customer.puml
package customer {
  class Customer
  Customer --> Item : buy
}

クラス増加時にもファイルが肥大化しにくく、メンテナンス性を維持できる。

Arrowパターン

パターン未適用

オブジェクト間の依存関係をextendsimplementsといったキーワードで記述する。

class Parent
class Child extends Parent
Arrowパターン未適用
class Parent
class Child extends Parent

この表現には以下の問題がある。

  • 矢印向きを指定できない。
  • 個々のオブジェクトを表現するclass宣言と、"矢印"というレイアウトを指定するextendsキーワードが混ざっている。

パターン適用

Arrowパターンを適用した例を以下に示す。

class Parent
class Child

Parent <|-up- Child
Arrowパターン適用
class Parent
class Child

Parent <|-up- Child

Arrowパターンは個々のオブジェクトのコンテキストを保持するclass宣言からレイアウト情報を保持する矢印を分離する。

コンテキストとレイアウトの分離により、より柔軟な図表現が可能となる。上記の例では矢印にupキーワードを使うことで矢印の向きを逆向きにした。

Includeパターン

パターン未適用

start/endsubincludesub キーワードをつかって、PlantUMLファイルをオブジェクト指向の構造にまとめよう。

次のようなUMLを書くことを考えてみよう。

!startsub interface
package activities {
  class Activity
}
!endsub

!startsub interface
abstract class ActivityEdge
!endsub

Activity "0..1" *-- "*" ActivityEdge

Activityクラスはactivitiesパッケージの一部であり、アブストラクトクラスActivityEdgeを集約する。このUMLを、1つのPlantUMLファイルに記述すると次のようになるだろう。

all.puml
@startuml

package activities {
  class Activity
}

abstract class ActivityEdge

Activity "0..1" *-- "*" ActivityEdge

@enduml

このとき、activitiesパッケージ内のActivityクラスとパッケージ外のActivityEdgeクラスが、同じファイル内に書かれることになる。

こうすると、たとえばactivitiesパッケージだけを図示したい場合や、他のクラス図にActivityクラスを載せたい場合にコードクローンするしかなく、不便である。

パターン適用例

ここで使えるのが、start/endsubincludesub キーワードである。前述のPlantUMLファイルをこのキーワードを使って2つのファイルに分割する。

activity.puml
@startuml

!startsub interface
package activities {
  class Activity
}
!endsub

@enduml
index.puml
@startuml

!includesub activity.puml!interface

!startsub interface
abstract class ActivityEdge
!endsub

Activity "0..1" *-- "*" ActivityEdge

@enduml

感嘆符!は、PlantUMLのプリプロセッサ命令を意味する記号である。

activity.puml!startsub interface ... !endsubは、interfaceという名前のサブパートを宣言するプリプロセッサ命令である。このサブパートは、!includesubキーワードを使って、別のPlantUmlから呼び出すことができる。

index.puml!includesub activity.puml!interfaceは、activity.pumlinterfaceサブパートを呼び出すプリプロセッサ命令である。

このように、start/endsubincludesub キーワードを用いてactivitiesパッケージを再利用しやすい別のファイルに分割することができた。

Compactパターン

パターン未適用

フィールドやメソッドを持たないクラスまで、フィールドとメソッドのセクションが表示されて見にくい。
たとえば、フィールドを持たないClerkクラスと、メソッドを持たないItemクラスがあったとする。

class Clerk {
  + sell()
}

class Item {
  - price
}
Compactパターン未適用
class Clerk {
  + sell()
}

class Item {
  - price
}

PlantUML は、デフォルトではフィールド、メソッドを持たない場合もセクションを表示してしまう。
クラスが1つ、2つの場合はまだいいが、クラスが増えてきたらどうだろうか。

package headquarters {
  class Headquarter
  Headquarter *-- CEO : belong
  Headquarter *-- CTO : belong
  Headquarter *-- CFO : belong
}

package shop {
  class Shop

  class Clerk {
    + sell()
  }

  class Item {
    - price
  }

  Shop *-- Item : have
  Shop *-- Clerk : belong

  Order *-- Item : include
}

package customer {
  class Customer {
    - wallet
    + buy()
  }
}

Headquarter *-- Shop : belong
Customer --> Item : buy
Compactパターン未適用
package headquarters {
  class Headquarter
  Headquarter *-- CEO : belong
  Headquarter *-- CTO : belong
  Headquarter *-- CFO : belong
}

package shop {
  class Shop

  class Clerk {
    + sell()
  }

  class Item {
    - price
  }

  Shop *-- Item : have
  Shop *-- Clerk : belong

  Order *-- Item : include
}

package customer {
  class Customer {
    - wallet
    + buy()
  }
}

Headquarter *-- Shop : belong
Customer --> Item : buy

クラスが増えるほど見にくくなるだろう。

パターン適用

"hide empty"を使って、空のフィールドとメソッドを非表示にする。
ファイルの冒頭に次の2行を追加する。

hide empty fields
hide empty methods

1行目は、空のフィールドを持つセクションを非表示にするスキンパラメータである。
2行目は、空のメソッドを持つセクションを非表示にするスキンパラメータである。

適用例を以下に示す。

hide empty fields
hide empty methods

package headquarters {
  class Headquarter
  Headquarter *-- CEO : belong
  Headquarter *-- CTO : belong
  Headquarter *-- CFO : belong
}

package shop {
  class Shop

  class Clerk {
    + sell()
  }

  class Item {
    - price
  }

  Shop *-- Item : have
  Shop *-- Clerk : belong

  Order *-- Item : include
}

package customer {
  class Customer {
    - wallet
    + buy()
  }
}

Headquarter *-- Shop : belong
Customer --> Item : buy
Compactパターン適用
hide empty fields
hide empty methods

package headquarters {
  class Headquarter
  Headquarter *-- CEO : belong
  Headquarter *-- CTO : belong
  Headquarter *-- CFO : belong
}

package shop {
  class Shop

  class Clerk {
    + sell()
  }

  class Item {
    - price
  }

  Shop *-- Item : have
  Shop *-- Clerk : belong

  Order *-- Item : include
}

package customer {
  class Customer {
    - wallet
    + buy()
  }
}

Headquarter *-- Shop : belong
Customer --> Item : buy

このように、hide empty命令を効果的に使うことでグッとクラス図を読みやすくできる。

Discussion