UML の図の多くは教科書の中にしか登場しません。その中でクラス図は、エンジニアがいまも実際に描く図です。システムが扱う対象、それぞれが持つデータ、できること、そして互いの関係を 1 枚で表します。うまく描けば、誰かがマイグレーションや API を書く前に、ドメインモデルをめぐる議論に決着がつきます。
この記事では、小さなオンライン講座プラットフォームの UML クラス図を、判断を 1 つずつ積み重ねながら作っていきます。可視性、多重度、関連、コンポジション、継承、インターフェースといった記法は、出てきた時点で説明します。最後に、クラス図を役に立たなくしてしまう間違いと、図をコードに落とし込む方法を簡単に紹介します。
クラス図が適しているとき
UML 2.5.1 仕様は、図を構造図と振る舞い図に分けています。クラス図は構造図で、型とその属性・操作、そして型同士の関係を示します。時間の概念はありません。
| 答えたい問い | 図 |
|---|---|
| どんな概念があり、何を知っていて、どう関係するか | クラス図 |
| データをどう保存するか:テーブル、キー、外部キー | ER 図 |
| 参加者の間で何がどの順番で起きるか | シーケンス図 |
| 1 つのオブジェクトがどの状態を経るか | ステートマシン図 |
クラス図が役立つのは、ドメインモデル、ライブラリの公開型、これから作る設計を描くときです。リポジトリ内の全クラスを書き出す用途には向きません。それは IDE のほうが上手に見せてくれますし、図は次のコミットで古くなります。
2 つの表で見る記法
クラスは最大 3 つの区画を持つ箱です。名前、属性、操作の区画で、どの区画も省略できます。
| 要素 | 記法 | 意味 |
|---|---|---|
| 抽象クラス | 名前を斜体にするか {abstract} |
インスタンス化できず、サブクラスが完成させる |
| インターフェース | 名前の上に «interface» |
操作の契約 |
| 可視性 | + public、- private、# protected、~ package |
その機能を使えるのは誰か |
| 属性 | name: Type [multiplicity] = default |
オブジェクトが持つデータ |
| 操作 | name(param: Type): ReturnType |
オブジェクトが提供する振る舞い |
| 静的な機能 | 下線 | インスタンスではなくクラスに属する |
関係はクラスの間の線で表し、意味の大部分は線の端が担います。
| 関係 | 線 | 読み方 |
|---|---|---|
| 関連 | 実線。開いた矢じりは誘導可能な端を示す | 「〜と関係がある」 |
| 集約 | 実線、全体の側に白抜きのひし形 | 「まとめる」。部分は単独でも存在する |
| コンポジション | 実線、全体の側に塗りつぶしのひし形 | 「〜から成る」。部分は全体と運命を共にする |
| 汎化 | 実線、親の側に白抜きの三角形 | 「〜の一種である」 |
| 実現 | 破線、インターフェースの側に白抜きの三角形 | 「実装する」 |
| 依存 | 破線、依存先の側に開いた矢じり | 「使う」。依存先の変更が依存元を壊しうる |
厳密なのはコンポジションです。仕様では、部分のオブジェクトは同時に最大 1 つの複合オブジェクトにしか属さず、複合オブジェクトが削除されると部分も一緒に削除されます。一方、共有集約(白抜きのひし形)はあいまいです。仕様自身が、その正確な意味は適用分野やモデルを描く人によって異なると述べています。チームでその意味に合意できている場合だけ使いましょう。普通の関連なら、同じことを議論少なく表せます。
多重度
多重度は、関連の一方の端にいくつのオブジェクトが位置しうるかを示します。ちょうど 1 つなら 1、任意なら 0..1、いくつでもよければ *(0..* と同じ)、少なくとも 1 つなら 1..*、2..5 のような範囲も書けます。数える対象のクラスの隣の端に書きます。Course 1 — 1..* Module は「講座は 1 つ以上のモジュールを持ち、各モジュールはちょうど 1 つの講座に属する」と読みます。
多重度の書き漏れは、クラス図が議論を終わらせるどころか議論を始めてしまう最大の原因です。以下の例では、すべての関連の両端に多重度を書いています。
例:オンライン講座プラットフォーム
箱からではなく、普通の文から始めます。
- 講師が講座を公開する。1 つの講座には 1 人以上の講師がいる。
- 講座は順序付きのモジュールから、モジュールは順序付きのレッスンから成る。
- レッスンは動画、クイズ、課題のいずれかである。
- クイズと課題は採点されるが、動画は採点されない。
- 受講者は講座に受講登録する。受講登録は進捗を記録し、受講者の提出物を保持する。
- 講座を修了した受講者は、その受講登録に対して修了証を 1 つ受け取る。
- 講座はカテゴリーに分類され、1 つの講座が複数のカテゴリーに入ることもある。
ユーザー:1 つのクラス、2 つの役割
最初に思いつくのは、Student と Instructor を User のサブクラスにすることです。しかしこれは、講師がほかの人の講座を受講した瞬間に破綻します。ほとんどのプログラミング言語では、オブジェクトは生涯ずっと 1 つのクラスのままなので、両方にはなれず、後から切り替えることもできません。役割は関連として表しましょう。User は講座を担当し、User は受講登録を持ちます。継承は、クイズがレッスンの一種であるように、生涯ずっと何かの一種であるものに取っておきます。
ライフサイクルが結び付いているところはコンポジション
モジュールは講座の外では意味を持たず、レッスンもモジュールの外では意味を持ちません。講座を削除すれば両方が消えます。これがコンポジションで、Course と Module の側に塗りつぶしのひし形を付け、全体の側に 1 を書きます。カテゴリーは事情が違います。「データエンジニアリング」カテゴリーを削除しても、その講座まで消えてはいけません。そのため Category と Course の間は、両端が * の集約です。
継承とインターフェース
Lesson は抽象クラスで、VideoLesson、Quiz、Assignment がそのタイトルと位置を継承します。採点はこの木を横断します。クイズと課題は採点され、動画は採点されません。来年追加するピアレビューは、レッスンではないのに採点されるかもしれません。これはインターフェース Gradable で、Quiz と Assignment が実現します。GradingService は具体的なクラスではなく Gradable に依存し、破線の依存の矢印がまさにそれを表します。
独自のデータを持つ関連
Enrollment は User と Course の間に位置します。UML にはこのための専用の記法として関連クラスがあり、関連の線に破線でつなぎます。とはいえ多くのチームは、2 つの関連を持つ普通のクラスとして描きます。受講登録日、進捗、提出物、最大 1 つの修了証という、独自の識別性、データ、振る舞いを持つからです。
ダイアグラムの内容
- Category
- Course
- User
- Module
- Enrollment
- Certificate
- Lesson
- Submission
- VideoLesson
- Quiz
- Assignment
- Gradable
- GradingService
- groups
- teaches
- enrolls
- for
- earns
- holds
- answers
- «use»
図の読み方は次のとおりです。
- 左上はカタログ。
CourseはModuleから成り、そのモジュールはさらにLessonから成ります。全体の側には塗りつぶしのひし形があります。Categoryは白抜きのひし形で講座をまとめます。 - 左下はレッスンの種類。
Lessonは抽象クラスです。VideoLesson、Quiz、Assignmentが白抜きの三角形でそれを指しています。 - 右側は学習。
Userは講座を担当し(講座ごとに講師は1..*)、任意の数の受講登録を持ちます。各受講登録はちょうど 1 つの講座に対応します。Enrollmentは提出物を保持し、最大 1 つのCertificateを得ます。 - 右下は採点。
QuizとAssignmentは、破線と白抜きの三角形でGradableを実現します。各Submissionは採点対象の 1 項目に対応し、GradingServiceはインターフェースだけに依存します。
よくある間違い
- コードベースの全クラスを載せる。 1 枚の図に 10〜15 クラスが目安です。大きなモデルは領域ごとに分けるか、残りを区画なしの名前だけで示します。
- getter と setter を操作として書く。 アクセサではなく、
publish()やenroll()のようにドメイン上の意味を持つ操作を挙げます。 - テーブルをクラスとして描く。
courseIdのような属性は ER 図に書くものです。クラス図では関連の線そのものが参照を表します。 - 役割に継承を使う。 オブジェクトがその役割を得たり失ったりできるなら、それはサブクラスではなく関連です。
- 意味のないひし形。 全体を削除したときに部分がどうなるのか誰も説明できないなら、普通の関連を使いましょう。
- ドメインレベルに実装の詳細を持ち込む。 図の主題でない限り、フレームワークのアノテーション、DTO、リポジトリは省きます。
図からコードへ
この図はそのままコードに対応します。TypeScript で書くと次のとおりです。
interface Gradable {
maxScore(): number;
autoGrade(submission: Submission): Score | null;
}
abstract class Lesson {
constructor(public title: string, public position: number) {}
}
class Quiz extends Lesson implements Gradable {
questions: Question[] = [];
maxScore(): number {
return this.questions.length;
}
autoGrade(submission: Submission): Score | null {
return submission.scoreAgainst(this.questions);
}
}
class Course {
private readonly modules: Module[] = []; // composition: owned and removed with the course
categories: Category[] = []; // aggregation: categories exist on their own
addModule(title: string): Module {
const created = new Module(title, this.modules.length);
this.modules.push(created);
return created;
}
}
コンポジションは所有になります。講座が自分のモジュールを作り、講座がなくなった後にモジュールを生かし続けるものは何もありません。集約は、独自のライフサイクルを持つオブジェクトへの参照になります。コードと図が食い違ったら、同じプルリクエストで図を更新しましょう。古くならないアーキテクチャ図で説明しているとおり、図がスクリーンショットではなくリンクであれば、これはずっと簡単になります。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- UML パックから概念ごとに クラス を追加し、属性と操作を 1 行に 1 つずつ書きます。
Lessonのステレオタイプはabstractに設定します。 Gradable用に インターフェース を追加し、2 つの操作を書きます。- クラスをつなぎます。全体の側の矢印の端を、コンポジションなら 塗りつぶしひし形、集約なら ひし形 に、継承なら親の側を 三角形 に設定します。実現はインターフェースの側に 三角形 を付けた破線で、依存は オープン矢印 を付けた破線で描きます。
- 多重度は各端の隣にテキストで置き、「teaches」のように関連の名前をエッジのラベルに書きます。
- 役割を関連にした理由のような判断は、UML の ノート に残します。
この例で使うダイアグラムノードは 13 個で、Free プランの範囲に収まります。MCP で接続した AI エージェントを使えば、要件を説明してクラスの下書きを任せ、多重度だけ自分で直すこともできます。
よくある質問
クラス図とオブジェクト図の違いは何ですか? クラス図は型と型の間のルールを示します。オブジェクト図は、ある時点の具体的なインスタンスを値やリンクとともに示すもので、クラス図を実際の例と照らし合わせて確かめるのに役立ちます。
集約とコンポジションの違いは何ですか? コンポジションでは、部分は 1 つの全体だけに属し、全体と一緒に削除されます。集約では、部分は単独でも存在でき、共有もできます。正確な意味はモデルを描く人に委ねられているため、代わりに普通の関連を使うチームも多くあります。
クラス図はコードと完全に一致させるべきですか? ドメインモデルが一致させるべきなのは概念と関係であり、すべてのフィールドやメソッドではありません。プロダクトマネージャーとエンジニアの両方が読めるレベルに保ちましょう。
クラス図は C4 のコンポーネント図とどう違いますか? C4 のコンポーネント図は、デプロイ可能な構成要素とその責務を示します。クラス図はさらに 1 段深く、その中にある型まで降ります。C4 モデルの図の例も参照してください。
コードからクラス図を自動で作れますか? 多くの IDE でできますが、結果にはすべてのクラスが並び、読みにくくなります。ドメインモデルは手で、または AI エージェントと一緒に描き、重要なクラスだけに絞りましょう。