- 設計
UML クラス図の書き方|記号・関係線6種の一覧とMermaid/PlantUML
UML クラス図とは、システムに登場するクラスの「名前・属性・操作」と、クラス同士の関係を1枚に表す設計図です。 書き方の中心は、クラスを3段の箱で描くことと、関係を6種類の線(関連・集約・コンポジション・汎化・実現・依存)で描き分けることの2つです。
この記事では、記号と関係線を 「意味・線の形・Java コードでの対応・Mermaid 記法・PlantUML 記法」 の一覧表にまとめ、すべての図を mermaid-cli と PlantUML で実際に描画できることを確認しました。さらに、動く Java / TypeScript のコードからクラス図を起こす手順、集約とコンポジションの見分け方、矢印の向きなどの よくある誤りの直す前と後、AI に描かせるときの指示例まで紹介します。
検証環境: @mermaid-js/mermaid-cli 12.0.0(mermaid 12.0.0)、PlantUML 1.2026.8、Java 21.0.10、TypeScript 7.0.2、Node.js 22。
クラス図の書き方の早見表
| 描きたいもの | UML の記号 | Mermaid | PlantUML |
|---|---|---|---|
| クラス | 3区画の箱(名前/属性/操作) | class Order { ... } |
class Order { ... } |
| public / private | + / - |
+total() int / -String id |
+ total() : int / - id : String |
| protected / package | # / ~ |
#orderedAt / ~note |
# orderedAt / ~ note |
| static(クラスメンバー) | 下線 | 末尾に $ |
{static} |
| abstract(抽象) | 斜体 | 操作の末尾に *、クラスは <<abstract>> |
{abstract} / abstract class |
| インターフェース | «interface» |
<<interface>> |
interface Notifier |
| ステレオタイプ | «entity» など |
<<entity>> |
class Order <<entity>> |
| ジェネリクス | List<Member> |
List~Member~ |
List<Member> |
| 多重度 | 1、0..1、0..*、1..* |
Order *-- "1..*" OrderLine |
同じ書き方 |
| 関係の名前 | 線の上のラベル | A --> B : 名前 |
同じ書き方 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「UML クラス図の書き方|記号・関係線6種の一覧とMermaid/PlantUML」
関係線6種の一覧(意味・線・Java・Mermaid・PlantUML)
| 関係 | 意味 | 線の形 | Java での対応 | Mermaid | PlantUML |
|---|---|---|---|---|---|
| 関連 | 相手を知っていて使い続ける | 実線(矢印で参照の向き) | フィールドに持つ private Member orderedBy; |
Order --> Member |
Order --> Member |
| 集約 | 全体と部分。部分は単独でも存在できる | 実線+白抜きのひし形(全体側) | 外から受け取ってコレクションに持つ team.add(member) |
Team o-- Member |
Team o-- Member |
| コンポジション | 全体と部分。部分は全体と運命をともにする | 実線+黒塗りのひし形(全体側) | 内部で生成して外に出さない lines.add(new OrderLine(...)) |
Order *-- OrderLine |
Order *-- OrderLine |
| 汎化 | 「〜の一種」(継承) | 実線+白抜き三角(親側) | class PremiumMember extends Member |
PremiumMember --|> Member |
PremiumMember --|> Member |
| 実現 | インターフェースを実装する | 破線+白抜き三角(インターフェース側) | class MailNotifier implements Notifier |
MailNotifier ..|> Notifier |
MailNotifier ..|> Notifier |
| 依存 | 一時的に使う(変更の影響を受ける) | 破線+開いた矢印 | 引数・ローカル変数・戻り値で使うだけ | OrderService ..> TaxCalculator |
OrderService ..> TaxCalculator |
覚え方のポイント:
- 三角は「親・インターフェース」側、ひし形は「全体」側に付きます。
- 実線は「持っている(構造)」、破線は「使う・約束する」。汎化と実現は、実線か破線かだけが違います。
- Mermaid と PlantUML は関係線の記法がほぼ同じです。どちらも
Member <|-- PremiumMemberのように左右を逆にも書けます。
上の6種を1つの図にしたコードです(mermaid-cli と PlantUML の両方で描画を確認)。
classDiagram
direction LR
Order --> "1" Member : 関連(注文者)
Team o-- "0..*" Member : 集約
Order *-- "1..*" OrderLine : コンポジション
PremiumMember --|> Member : 汎化
MailNotifier ..|> Notifier : 実現
OrderService ..> TaxCalculator : 依存
@startuml
left to right direction
Order --> "1" Member : 関連(注文者)
Team o-- "0..*" Member : 集約
Order *-- "1..*" OrderLine : コンポジション
PremiumMember --|> Member : 汎化
MailNotifier ..|> Notifier : 実現
OrderService ..> TaxCalculator : 依存
interface Notifier
@enduml
クラスの箱の書き方:3区画・可視性・static・abstract
クラスは上から「クラス名」「属性」「操作」の3区画で描きます。属性は 可視性 名前 : 型、操作は 可視性 名前(引数 : 型) : 戻り値の型 が UML の書き方です(Mermaid は 型 名前 の順でも書けます)。
| 可視性 | 記号 | Java | TypeScript |
|---|---|---|---|
| public | + |
public |
public(省略時) |
| private | - |
private |
private / #field |
| protected | # |
protected |
protected |
| package | ~ |
修飾子なし | ― |
Mermaid:
classDiagram
class Order {
<<entity>>
-String id
-int status
#LocalDateTime orderedAt
~String note
+int MAX_LINES$
+addLine(Product product, int qty) void
+total() int
+of(String id)$ Order
}
class Shape {
<<abstract>>
+area()* double
}
class Notifier {
<<interface>>
+send(String to, String message) void
}
PlantUML:
@startuml
class Order <<entity>> {
- id : String
- status : int
# orderedAt : LocalDateTime
~ note : String
+ {static} MAX_LINES : int
+ addLine(product : Product, qty : int) : void
+ total() : int
+ {static} of(id : String) : Order
}
abstract class Shape {
+ {abstract} area() : double
}
interface Notifier {
+ send(to : String, message : String) : void
}
@enduml
描画すると、Mermaid では static が下線、abstract が斜体、<<entity>> などのステレオタイプがクラス名の上に «entity» として表示されました。PlantUML では可視性の記号がアイコン(例:- は四角、+ は丸)で表示されます。
多重度の書き方
| 多重度 | 意味 | 例 |
|---|---|---|
1 |
ちょうど1 | 注文には注文者が1人 |
0..1 |
0か1 | 会員の配送先(未登録もあり) |
0..*(*) |
0以上 | チームのメンバー |
1..* |
1以上 | 注文の明細(最低1行) |
2..5 |
範囲指定 | 備品の貸出上限 |
多重度は「相手側の端」に書きます。 Order *-- "1..*" OrderLine なら「1つの注文に明細が1件以上」と読みます。
コードからクラス図を起こす(Java)
次の Java を java OrderApp.java で実行し、動作を確認してからクラス図にしました。
import java.util.ArrayList;
import java.util.List;
public class OrderApp {
// 汎化の親
static class Member {
private final String name;
Member(String name) { this.name = name; }
String getName() { return name; }
int discountRate() { return 0; }
}
// 汎化(extends)
static class PremiumMember extends Member {
PremiumMember(String name) { super(name); }
@Override int discountRate() { return 10; }
}
// 集約:Team は Member を外から受け取って持つ。Team が消えても Member は残る
static class Team {
private final List<Member> members = new ArrayList<>();
void add(Member m) { members.add(m); }
int size() { return members.size(); }
}
record Product(String name, int price) {}
// コンポジション:OrderLine は Order の中で作られ、Order と運命をともにする
static class OrderLine {
private final Product product; // 関連(OrderLine → Product)
private final int qty;
private OrderLine(Product product, int qty) { this.product = product; this.qty = qty; }
int subtotal() { return product.price() * qty; }
}
static class Order {
private final Member orderedBy; // 関連(Order → Member、多重度 1)
private final List<OrderLine> lines = new ArrayList<>(); // コンポジション(1..*)
Order(Member orderedBy) { this.orderedBy = orderedBy; }
void addLine(Product p, int qty) { lines.add(new OrderLine(p, qty)); } // 部品は内部で生成
int total() { return lines.stream().mapToInt(OrderLine::subtotal).sum(); }
Member getOrderedBy() { return orderedBy; }
}
// 実現(implements)
interface Notifier { void send(String to, String message); }
static class MailNotifier implements Notifier {
public void send(String to, String message) { System.out.println("mail to " + to + ": " + message); }
}
static class TaxCalculator {
int withTax(int amount) { return amount * 110 / 100; }
}
// 依存:TaxCalculator は引数で受け取って使うだけ(フィールドに持たない)
// 関連:Notifier はフィールドに持つ
static class OrderService {
private final Notifier notifier;
OrderService(Notifier notifier) { this.notifier = notifier; }
int checkout(Order order, TaxCalculator tax) {
int base = order.total() * (100 - order.getOrderedBy().discountRate()) / 100;
int amount = tax.withTax(base);
notifier.send(order.getOrderedBy().getName(), "合計 " + amount + " 円");
return amount;
}
}
public static void main(String[] args) {
Member hana = new PremiumMember("hana");
Team team = new Team();
team.add(hana);
Order order = new Order(hana);
order.addLine(new Product("ノート", 300), 2);
order.addLine(new Product("ペン", 150), 4);
int amount = new OrderService(new MailNotifier()).checkout(order, new TaxCalculator());
System.out.println("team size=" + team.size() + ", total=" + order.total() + ", amount=" + amount);
}
}
実行結果(Java 21.0.10、-Dstdout.encoding=UTF-8 を指定):
mail to hana: 合計 1188 円
team size=1, total=1200, amount=1188
コードの書き方と関係線の対応は次のとおりです。
| コード | 関係 | 判断の決め手 |
|---|---|---|
class PremiumMember extends Member |
汎化 | extends |
class MailNotifier implements Notifier |
実現 | implements |
private final Member orderedBy; |
関連(多重度 1) | フィールドに持ち続ける |
private final List<OrderLine> lines + new OrderLine(...) を内部で実行 |
コンポジション | 部品を 自分で作り、外に渡さない |
private final List<Member> members + add(Member m) |
集約 | 部品を 外から受け取る。Team がなくても Member は残る |
checkout(Order order, TaxCalculator tax) |
依存 | 引数で受け取って使うだけで、フィールドに持たない |
private final Notifier notifier; |
関連(インターフェースへ) | 具体クラスではなくインターフェースを持つ |
Mermaid で描いたクラス図:
classDiagram
class Member {
-String name
+getName() String
+discountRate() int
}
class PremiumMember {
+discountRate() int
}
class Team {
-List~Member~ members
+add(Member m) void
}
class Order {
-Member orderedBy
-List~OrderLine~ lines
+addLine(Product p, int qty) void
+total() int
}
class OrderLine {
-Product product
-int qty
+subtotal() int
}
class Product {
+String name
+int price
}
class Notifier {
<<interface>>
+send(String to, String message) void
}
class MailNotifier
class TaxCalculator {
+withTax(int amount) int
}
class OrderService {
-Notifier notifier
+checkout(Order order, TaxCalculator tax) int
}
Member <|-- PremiumMember
Team o-- "0..*" Member
Order "0..*" --> "1" Member : orderedBy
Order *-- "1..*" OrderLine
OrderLine "0..*" --> "1" Product
Notifier <|.. MailNotifier
OrderService --> "1" Notifier
OrderService ..> TaxCalculator
OrderService ..> Order
PlantUML で描いたクラス図(hide empty members で空の区画を省略):
@startuml
hide empty members
class Member {
- name : String
+ getName() : String
+ discountRate() : int
}
class PremiumMember {
+ discountRate() : int
}
class Team {
- members : List<Member>
+ add(m : Member) : void
}
class Order {
- orderedBy : Member
- lines : List<OrderLine>
+ addLine(p : Product, qty : int) : void
+ total() : int
}
class OrderLine {
- product : Product
- qty : int
+ subtotal() : int
}
class Product <<record>> {
+ name : String
+ price : int
}
interface Notifier {
+ send(to : String, message : String) : void
}
class MailNotifier
class TaxCalculator {
+ withTax(amount : int) : int
}
class OrderService {
- notifier : Notifier
+ checkout(order : Order, tax : TaxCalculator) : int
}
Member <|-- PremiumMember
Team o-- "0..*" Member
Order "0..*" --> "1" Member : orderedBy
Order *-- "1..*" OrderLine
OrderLine "0..*" --> "1" Product
Notifier <|.. MailNotifier
OrderService --> "1" Notifier
OrderService ..> TaxCalculator
OrderService ..> Order
@enduml
List<OrderLine> を持つフィールドは、属性として書くか関係線で描くかのどちらかにそろえると読みやすくなります。上の図では練習のため両方を書いていますが、実務では「関係線で描いたものは属性から外す」ルールにすることが多いです。
TypeScript でも対応は同じ
// order.ts — TypeScript でも対応は同じ
interface Notifier { // インターフェース
send(to: string, message: string): void;
}
class MailNotifier implements Notifier { // 実現
send(to: string, message: string): void { console.log(`mail to ${to}: ${message}`); }
}
class Member {
constructor(private readonly name: string) {}
getName(): string { return this.name; }
discountRate(): number { return 0; }
}
class PremiumMember extends Member { // 汎化
override discountRate(): number { return 10; }
}
class Team {
private readonly members: Member[] = []; // 集約(外から受け取る)
add(m: Member): void { this.members.push(m); }
}
class OrderLine {
constructor(readonly productName: string, readonly price: number, readonly qty: number) {}
}
class Order {
private readonly lines: OrderLine[] = []; // コンポジション(内部で生成)
constructor(private readonly orderedBy: Member) {} // 関連
addLine(name: string, price: number, qty: number): void { this.lines.push(new OrderLine(name, price, qty)); }
total(): number { return this.lines.reduce((s, l) => s + l.price * l.qty, 0); }
get member(): Member { return this.orderedBy; }
}
class TaxCalculator { withTax(n: number): number { return Math.floor(n * 1.1); } }
class OrderService {
constructor(private readonly notifier: Notifier) {} // 関連(インターフェースに向ける)
checkout(order: Order, tax: TaxCalculator): number { // 依存(引数で使うだけ)
const amount = tax.withTax(order.total() * (100 - order.member.discountRate()) / 100);
this.notifier.send(order.member.getName(), `合計 ${amount} 円`);
return amount;
}
}
const order = new Order(new PremiumMember('hana'));
order.addLine('ノート', 300, 2);
order.addLine('ペン', 150, 4);
new Team().add(order.member);
console.log(new OrderService(new MailNotifier()).checkout(order, new TaxCalculator()));
$ tsc --strict --target es2022 --module nodenext --outDir out order.ts && node out/order.js
mail to hana: 合計 1188 円
1188
TypeScript の constructor(private readonly x: T)(パラメータプロパティ)はフィールドを宣言するのと同じなので、クラス図では -x : T の属性、または関連として描きます。
集約とコンポジションの見分け方
いちばん迷いやすいのがこの2つです。次の3つの質問に「はい」が多ければコンポジションです。
| 質問 | コンポジション(*--) |
集約(o--) |
|---|---|---|
| 部分は全体の中で作られる? | はい(new OrderLine を Order の中で実行) |
いいえ(外から受け取る) |
| 全体を消したら部分も消える? | はい(明細だけ残っても意味がない) | いいえ(チームを解散しても会員は残る) |
| 部分は同時に2つの全体に属せない? | はい(1つの明細は1つの注文だけ) | いいえ(会員は複数チームに所属できる) |
迷ったら普通の関連(-->)で描き、多重度で表すのも正しい選択です。UML 2 の仕様でも集約(白ひし形)の意味は厳密には定められていないため、チームで「白ひし形は使わない」と決めている現場もあります。
よくある誤りと直す前と後
classDiagram
Order o-- OrderLine
Member --|> PremiumMember
MailNotifier --|> Notifier
OrderService --> TaxCalculator
↓ 直した後:
classDiagram
Order *-- "1..*" OrderLine
PremiumMember --|> Member
MailNotifier ..|> Notifier
OrderService ..> TaxCalculator
class Notifier {
<<interface>>
}
| 直す前 | 何が起きているか | 直した後 |
|---|---|---|
Order o-- OrderLine |
明細は注文の中で作られ、注文と一緒に消えるのに集約になっている | Order *-- "1..*" OrderLine |
Member --|> PremiumMember |
三角が子の側を向いていて「会員はプレミアム会員の一種」と読めてしまう | PremiumMember --|> Member(三角は親側) |
MailNotifier --|> Notifier |
実線なので「継承」になっている | MailNotifier ..|> Notifier(破線で実現) |
OrderService --> TaxCalculator |
引数で使うだけなのに関連(フィールドに持つ)になっている | OrderService ..> TaxCalculator(依存) |
どちらも mermaid-cli と PlantUML で描画を確認しました(PlantUML 版は classDiagram 行を外し、@startuml / @enduml で囲んで interface Notifier を追加したもの)。
ツールで起きやすいつまずき
| 症状 | 原因 | 対処 |
|---|---|---|
Mermaid で -List<Member> members が -List members と表示される |
Mermaid ではジェネリクスを ~ で書く(エラーにならず <Member> が消える) |
-List~Member~ members |
| Mermaid で static や abstract が反映されない | 記号の位置が違う | static は末尾に $、抽象操作は () の後ろに * |
| PlantUML でインターフェースが普通のクラスになる | class で宣言している |
interface Notifier と宣言する |
PlantUML の -testdot で Dot executable does not exist |
Graphviz が入っていない | 今回の 1.2026.8 ではクラス図は Graphviz なしで描画できた。描けない版では !pragma layout smetana を追加するか Graphviz を入れる |
設計レビューで見るポイント
クラス図は「描けているか」より 「設計の問題に気づけるか」 に価値があります。レビューでは次の点を確認します。
| 観点 | 図のどこを見るか | 気づけること |
|---|---|---|
| 依存の向き | 矢印が集中しているクラス | 変更の影響が大きいクラス。インターフェースを挟む候補 |
| 循環 | A → B → A の矢印 | 片方を依存(引数)にする、共通部分を切り出す |
| 具体クラスへの関連 | OrderService --> MailNotifier のような線 |
インターフェース(Notifier)に向けるとテストで差し替えやすい |
| 巨大なクラス | 属性・操作が多い箱 | 責務の分割を検討 |
| 多重度 | * の付け忘れ、1 と 0..1 の違い |
未登録を許すか、必須かの仕様確認 |
| 継承の深さ | 三角が何段も続く | コンポジションで表せないか |
1枚にすべてを描かないのもコツです。レビューの目的(ドメインの構造、依存の向き、特定機能の周辺)ごとに、クラスを絞った図を複数作るほうが伝わります。
AI にクラス図を描かせる指示例
生成 AI はコードからクラス図を起こすのが得意ですが、関係の判定ルールを指示しないと、関連と依存、集約とコンポジションが混ざりやすくなります。次のように条件を渡すと安定します。
次の Java コードから、Mermaid の classDiagram を作成してください。
条件:
- extends は汎化(--|>)、implements は実現(..|>)
- フィールドで保持する参照は関連(-->)。多重度を "1" や "0..*" で書く
- クラス内部で new して外に渡さない部品はコンポジション(*--)
- 外から受け取ってコレクションに保持するものは集約(o--)
- 引数・ローカル変数・戻り値でのみ使うクラスは依存(..>)
- 可視性は + - # ~、static は $、抽象メソッドは * を付ける
- ジェネリクスは List~Member~ のように ~ で書く
- 関係線で描いたフィールドは属性の区画から省く
- 最後に、各関係をそう判定した根拠(該当するコード行)を表で示す
(ここにコードを貼る)
「判定の根拠を表で出させる」と、人がレビューするときに誤りを見つけやすくなります。生成された Mermaid は mermaid-cli(npx -p @mermaid-js/mermaid-cli mmdc -i diagram.mmd -o diagram.svg)で描画して確認できます。
よくある質問
Q. クラス図と ER 図はどう違いますか? A. ER 図はデータベースのテーブルと外部キーの関係、クラス図はプログラムのクラスと、振る舞い(操作)を含む関係を表します。クラス図には継承・インターフェース・依存がありますが、ER 図にはありません。
Q. 属性や操作はすべて書く必要がありますか? A. 図の目的に必要なものだけで十分です。概念を話し合う段階ではクラス名と関係だけ、詳細設計では型と可視性まで、と段階を分けると描きやすくなります。
Q. Mermaid と PlantUML はどちらを使えばよいですか? A. GitHub や多くの Markdown ツールでそのまま表示したいなら Mermaid、レイアウトの細かい調整や UML の表現力を重視するなら PlantUML が向いています。関係線の記法はほぼ共通なので、後から移し替えるのも簡単です。
Q. 関連の矢印はいつ付けますか?
A. 片方だけが相手を参照する(ナビゲーションできる)なら矢印付き、お互いに参照するなら矢印なしの実線(--)で描きます。コードでフィールドを持っている側から矢印を出すと考えるとわかりやすいです。
まとめ
- クラスは3区画(名前・属性・操作)、可視性は
+ - # ~、static は下線、abstract は斜体 - 関係は6種:関連(実線)、集約(白ひし形)、コンポジション(黒ひし形)、汎化(実線+三角)、実現(破線+三角)、依存(破線矢印)
- 三角は親・インターフェース側、ひし形は全体側に付ける
- フィールドで持つ=関連、引数で使う=依存、内部で作る部品=コンポジション
- Mermaid はジェネリクスを
~で書く(<>だと型が消える) - レビューでは依存の向き・循環・具体クラスへの関連を見る
- AI には判定ルールと根拠の表を指示すると精度が上がる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「UML クラス図の書き方|記号・関係線6種の一覧とMermaid/PlantUML」
株式会社RJCでは、コードと設計図を行き来しながら設計の考え方を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。