T-CREATOR

Mermaid class diagram 速習:継承・集約・関連の表し分けとアンチパターン

Mermaid class diagram 速習:継承・集約・関連の表し分けとアンチパターン

設計書やドキュメントでクラス図を描く際、「この関係は継承?集約?それとも関連?」と悩んだ経験はありませんか。Mermaid の class diagram は手軽にクラス図を作成できる優れたツールですが、継承(inheritance)、集約(aggregation)、コンポジション(composition)、関連(association)の記法を正しく使い分けないと、意図しない設計を表現してしまう可能性があります。

本記事では、Mermaid class diagram における各関係性の正しい表記方法と使い分けのポイント、そして陥りがちなアンチパターンを具体例とともに解説します。初心者の方でも図を見ながら理解できるよう、実際の Mermaid コードと図解を豊富に用意しました。これを読めば、明日からのドキュメント作成で自信を持って適切な記法を選べるようになるでしょう。

背景

クラス図が果たす役割

ソフトウェア開発において、クラス図は設計段階で最も頻繁に使われる図の一つです。クラス同士の関係性を視覚的に表現することで、システム全体の構造を俯瞰でき、チームメンバー間での認識のズレを防げます。特に、オブジェクト指向設計では、クラス間の依存関係や責務の分担を明確にすることが品質の高いコードにつながります。

従来の UML ツールは高機能ですが、セットアップが煩雑だったり有料だったりすることが多く、気軽に図を更新するハードルが高いという課題がありました。そこで登場したのが Mermaid です。

Mermaid の利点と普及背景

Mermaid はテキストベースで図を記述できるため、バージョン管理システムとの相性が抜群です。Markdown ファイルに直接埋め込めば、GitHub や GitLab、Notion などで自動的にレンダリングされ、ドキュメントとコードを同じリポジトリで管理できます。

以下の図は、Mermaid を使った開発フローの概要を示しています。

mermaidflowchart LR
  dev["開発者"] -->|コード編集| editor["エディタ"]
  editor -->|Markdown + Mermaid| repo["Git リポジトリ"]
  repo -->|自動レンダリング| viewer["GitHub/Notion"]
  viewer -->|視覚的確認| team["チーム"]

図で理解できる要点:

  • Markdown にコードとして記述するため、差分管理が容易
  • 特別なツールなしでブラウザ上で図が表示される
  • チーム全体でリアルタイムに最新の設計を共有できる

このように、Mermaid はチーム開発におけるドキュメント作成の負担を大幅に軽減し、設計の透明性を高めてくれます。しかし、手軽さゆえに記法を曖昧に覚えてしまうと、誤った設計意図を伝えてしまうリスクもあるのです。

課題

継承・集約・関連の混同問題

クラス図を描く際、最も混同しやすいのが「継承(inheritance)」「集約(aggregation)」「コンポジション(composition)」「関連(association)」の 4 つの関係性です。これらは見た目が似ているものの、それぞれが表すクラス間の依存度やライフサイクルの管理方法が大きく異なります。

#関係性意味依存度ライフサイクル
1継承(inheritance)"is-a" の関係強い親クラスに依存
2コンポジション(composition)"has-a" の関係(強い所有)強い親が削除されると子も削除
3集約(aggregation)"has-a" の関係(弱い所有)中程度親が削除されても子は残る
4関連(association)単なる参照・利用関係弱い独立して存在

以下の図は、各関係性がどのような依存度を持つかを示しています。

mermaidflowchart TD
  strong["強い依存"] --> inheritance["継承<br/>inheritance"]
  strong --> composition["コンポジション<br/>composition"]
  medium["中程度の依存"] --> aggregation["集約<br/>aggregation"]
  weak["弱い依存"] --> association["関連<br/>association"]

よくある間違いとその影響

初心者が陥りがちな間違いとして、以下のようなケースがあります。

間違い 1: すべてを継承で表現してしまう

オブジェクト指向を学び始めたばかりの頃は、「継承=再利用」と捉えて、何でもかんでも継承で表現してしまいがちです。しかし、継承は「is-a(〜は〜である)」の関係が成立する場合にのみ使うべきで、単にコードを共有したいだけなら集約や関連を使うべきです。

間違い 2: 集約とコンポジションを区別しない

両者とも「has-a(〜を持つ)」の関係ですが、ライフサイクルの管理が異なります。コンポジションは親オブジェクトが削除されると子オブジェクトも削除される強い所有関係ですが、集約は親が削除されても子は独立して存在できます。この違いを理解せずに記法を選ぶと、メモリ管理やリソース解放のバグにつながる可能性があります。

間違い 3: 双方向の関連を無意識に作ってしまう

関連は単方向でも双方向でも表現できますが、双方向の関連は循環参照を生み出しやすく、テストやメンテナンスが困難になります。必要性を十分に検討せずに双方向関連を作ると、後々のリファクタリングで苦労することになるでしょう。

次のセクションでは、これらの課題を解決するために、Mermaid における正しい記法と使い分けの原則を詳しく見ていきます。

解決策

継承(Inheritance)の正しい表記

継承は「is-a」の関係を表し、子クラスが親クラスの性質を受け継ぐことを意味します。Mermaid では <|-- 記号を使って表現します。

継承の基本記法

mermaidclassDiagram
  Animal <|-- Dog
  Animal <|-- Cat

  class Animal {
    +String name
    +int age
    +eat()
    +sleep()
  }

  class Dog {
    +bark()
  }

  class Cat {
    +meow()
  }
markdown```mermaid
classDiagr
  Animal <|-- Dog
  Animal <|-- Cat

  class Animal {
    +String name
    +int age
    +eat()
    +sleep()
  }

  class Dog {
    +bark()
  }

  class Cat {
    +meow()
  }
```

継承を使うべきケース

継承は以下のような場合に適切です。

  1. 明確な「is-a」関係が成立する場合: 「Dog は Animal である」という関係が自然に成り立つ
  2. 共通の振る舞いを複数のクラスで共有したい場合: eat()sleep() といったメソッドを重複なく実装できる
  3. ポリモーフィズムを活用したい場合: 親クラス型の変数に子クラスのインスタンスを代入して扱える

コンポジション(Composition)の正しい表記

コンポジションは「has-a」の関係のうち、親が子のライフサイクルを完全に管理する強い所有関係を表します。Mermaid では *-- 記号を使います。

コンポジションの基本記法

mermaidclassDiagram
  House *-- Room
  Room *-- Window

  class House {
    +String address
    +List~Room~ rooms
    +build()
    +demolish()
  }

  class Room {
    +String name
    +int size
    +decorate()
  }

  class Window {
    +int width
    +int height
    +open()
    +close()
  }
markdown```mermaid
classDiagram
  House *-- Room
  Room *-- Window

  class House {
    +String address
    +List~Room~ rooms
    +build()
    +demolish()
  }

  class Room {
    +String name
    +int size
    +decorate()
  }

  class Window {
    +int width
    +int height
    +open()
    +close()
  }
```

コンポジションを使うべきケース

  1. 親オブジェクトが削除されたら子オブジェクトも不要になる場合: 家が取り壊されたら部屋も消滅する
  2. 子オブジェクトが親なしでは意味を持たない場合: 部屋単体では住所や所属が不明確
  3. 親が子の生成と破棄を責任を持って管理する場合: メモリリークを防ぐ設計が必要

集約(Aggregation)の正しい表記

集約は「has-a」の関係のうち、親と子が独立したライフサイクルを持つ弱い所有関係を表します。Mermaid では o-- 記号を使用します。

集約の基本記法

mermaidclassDiagram
  University o-- Professor
  University o-- Student

  class University {
    +String name
    +List~Professor~ professors
    +List~Student~ students
    +enroll()
    +graduate()
  }

  class Professor {
    +String name
    +String department
    +teach()
  }

  class Student {
    +String name
    +int grade
    +study()
  }
markdown```mermaid
classDiagram
  University o-- Professor
  University o-- Student

  class University {
    +String name
    +List~Professor~ professors
    +List~Student~ students
    +enroll()
    +graduate()
  }

  class Professor {
    +String name
    +String department
    +teach()
  }

  class Student {
    +String name
    +int grade
    +study()
  }
```

集約を使うべきケース

  1. 親オブジェクトが削除されても子オブジェクトは存続する場合: 大学が閉校しても教授や学生は存在し続ける
  2. 子オブジェクトが複数の親に所属できる場合: 学生が複数の大学に在籍する(編入など)
  3. 子オブジェクトが親とは独立して生成・管理される場合: 教授は大学に雇用される前から存在している

関連(Association)の正しい表記

関連は最も緩い関係性で、単なる参照や利用関係を表します。Mermaid では -- または --> 記号を使い、矢印の向きで依存の方向を示せます。

関連の基本記法(双方向)

mermaidclassDiagram
  Customer -- Order

  class Customer {
    +String customerId
    +String name
    +placeOrder()
  }

  class Order {
    +String orderId
    +Date orderDate
    +process()
  }
markdown```mermaid
classDiagram
  Customer -- Order

  class Customer {
    +String customerId
    +String name
    +placeOrder()
  }

  class Order {
    +String orderId
    +Date orderDate
    +process()
  }
```

関連の基本記法(単方向)

mermaidclassDiagram
  Order --> Product

  class Order {
    +String orderId
    +List~Product~ products
    +addProduct()
  }

  class Product {
    +String productId
    +String name
    +Decimal price
  }
markdown```mermaid
classDiagram
  Order --> Product

  class Order {
    +String orderId
    +List~Product~ products
    +addProduct()
  }

  class Product {
    +String productId
    +String name
    +Decimal price
  }
```

関連を使うべきケース

  1. 一時的な参照関係の場合: 注文が商品を参照するが、所有はしない
  2. 疎結合を保ちたい場合: クラス間の依存を最小限にする
  3. どちらのオブジェクトも独立して存在できる場合: 顧客と注文はそれぞれ独立したエンティティ

多重度(Multiplicity)の表記

関係性の記述に加えて、多重度を明示することで、より正確な設計を表現できます。

多重度の記法例

mermaidclassDiagram
  Customer "1" -- "0..*" Order : places
  Order "1" *-- "1..*" OrderItem : contains
  Product "1" -- "0..*" OrderItem : included in

  class Customer {
    +String customerId
    +String name
  }

  class Order {
    +String orderId
    +Date orderDate
  }

  class OrderItem {
    +int quantity
    +Decimal unitPrice
  }

  class Product {
    +String productId
    +String name
  }
markdown```mermaid
classDiagram
  Customer "1" -- "0..*" Order : places
  Order "1" *-- "1..*" OrderItem : contains
  Product "1" -- "0..*" OrderItem : included in

  class Customer {
    +String customerId
    +String name
  }

  class Order {
    +String orderId
    +Date orderDate
  }

  class OrderItem {
    +int quantity
    +Decimal unitPrice
  }

  class Product {
    +String productId
    +String name
  }
```

図で理解できる要点:

  • 1 : ちょうど 1 つ
  • 0..* : 0 個以上(0 または複数)
  • 1..* : 1 個以上(必ず 1 つ以上)
  • : places のようにラベルを付けることで関係の意味を明示できる

多重度を記述することで、ビジネスルールやデータの整合性制約を図上で表現でき、実装時の誤解を防げます。

具体例

正しい使い分けの実践例

ここでは、オンラインショッピングシステムを題材に、継承・集約・関連・コンポジションを適切に使い分けた設計例を示します。

全体設計図

mermaidclassDiagram
  Payment <|-- CreditCardPayment
  Payment <|-- BankTransferPayment

  ShoppingCart *-- CartItem
  Order o-- Customer
  Order --> ShoppingCart
  Order --> Payment
  CartItem --> Product

  class Payment {
    +String paymentId
    +Decimal amount
    +Date paymentDate
    +process()*
  }

  class CreditCardPayment {
    +String cardNumber
    +String cvv
    +authorize()
  }

  class BankTransferPayment {
    +String bankName
    +String accountNumber
    +verify()
  }
markdown```mermaid
classDiagram
  Payment <|-- CreditCardPayment
  Payment <|-- BankTransferPayment

  ShoppingCart *-- CartItem
  Order o-- Customer
  Order --> ShoppingCart
  Order --> Payment
  CartItem --> Product

  class Payment {
    +String paymentId
    +Decimal amount
    +Date paymentDate
    +process()*
  }

  class CreditCardPayment {
    +String cardNumber
    +String cvv
    +authorize()
  }

  class BankTransferPayment {
    +String bankName
    +String accountNumber
    +verify()
  }
```

設計の意図と使い分けのポイント

#関係性クラス間選択理由
1継承Payment <-- CreditCardPayment「クレジットカード決済は決済である」という is-a 関係が成立。ポリモーフィズムで決済方法を抽象化できる
2コンポジションShoppingCart *-- CartItemカートが削除されたらカートアイテムも不要。強い所有関係
3集約Order o-- Customer注文が削除されても顧客は残る。弱い所有関係
4関連Order --> ShoppingCart注文がカートを参照するが、所有はしない。単方向の依存

アンチパターン 1: 継承の乱用

悪い例:不適切な継承

mermaidclassDiagram
  Database <|-- UserService
  Database <|-- ProductService

  class Database {
    +String connectionString
    +connect()
    +disconnect()
    +query()
  }

  class UserService {
    +getUser()
    +createUser()
  }

  class ProductService {
    +getProduct()
    +createProduct()
  }
markdown```mermaid
classDiagram
  Database <|-- UserService
  Database <|-- ProductService

  class Database {
    +String connectionString
    +connect()
    +disconnect()
    +query()
  }

  class UserService {
    +getUser()
    +createUser()
  }

  class ProductService {
    +getProduct()
    +createProduct()
  }
```

問題点の分析

この設計では、「UserService は Database である」という関係が成立していません。UserService は Database を「使う」のであって、Database そのものではないのです。この設計は以下の問題を引き起こします。

  1. 違和感のある is-a 関係: ユーザーサービスがデータベースである、という表現は意味的におかしい
  2. 不要なメソッドの公開: UserService が connect()disconnect() を持つ必要はない
  3. テストの困難さ: モックオブジェクトに置き換えづらく、単体テストが書きにくい

改善例:コンポジションへの変更

mermaidclassDiagram
  UserService *-- Database
  ProductService *-- Database

  class Database {
    +String connectionString
    +connect()
    +disconnect()
    +query()
  }

  class UserService {
    -Database db
    +getUser()
    +createUser()
  }

  class ProductService {
    -Database db
    +getProduct()
    +createProduct()
  }
markdown```mermaid
classDiagram
  UserService *-- Database
  ProductService *-- Database

  class Database {
    +String connectionString
    +connect()
    +disconnect()
    +query()
  }

  class UserService {
    -Database db
    +getUser()
    +createUser()
  }

  class ProductService {
    -Database db
    +getProduct()
    +createProduct()
  }
```

この設計では、各サービスが Database を内部に持つコンポジションに変更されています。これにより、サービスとデータベースの責務が明確に分離され、テストやメンテナンスがしやすくなります。

アンチパターン 2: 集約とコンポジションの混同

悪い例:間違った集約の使用

mermaidclassDiagram
  Car o-- Engine

  class Car {
    +String model
    +Engine engine
    +start()
    +stop()
  }

  class Engine {
    +int horsepower
    +run()
  }
markdown```mermaid
classDiagram
  Car o-- Engine

  class Car {
    +String model
    +Engine engine
    +start()
    +stop()
  }

  class Engine {
    +int horsepower
    +run()
  }
```

問題点の分析

この設計では、車とエンジンの関係を集約で表現していますが、実際には車が廃棄されたらエンジンも一緒に廃棄されるべきです。エンジンが車なしで独立して存在することは通常ありません(中古パーツとして取り外される特殊ケースを除く)。

集約を使うと、以下のような誤解を招きます。

  1. ライフサイクル管理の曖昧さ: 車を削除してもエンジンを残すべきか判断がつかない
  2. メモリリーク: ガベージコレクション言語でも、適切な参照管理が行われない可能性
  3. 設計意図の不明確さ: レビュー時に「なぜ集約なのか」という疑問が生じる

改善例:コンポジションへの変更

mermaidclassDiagram
  Car *-- Engine

  class Car {
    +String model
    +Engine engine
    +start()
    +stop()
  }

  class Engine {
    +int horsepower
    +run()
  }
markdown```mermaid
classDiagram
  Car *-- Engine

  class Car {
    +String model
    +Engine engine
    +start()
    +stop()
  }

  class Engine {
    +int horsepower
    +run()
  }
```

コンポジションに変更することで、エンジンのライフサイクルが車に完全に依存することが明確になり、リソース管理の責任が明確化されます。

アンチパターン 3: 双方向関連の濫用

悪い例:不必要な双方向関連

mermaidclassDiagram
  Author -- Book
  Book -- Author

  class Author {
    +String name
    +List~Book~ books
    +writeBook()
  }

  class Book {
    +String title
    +Author author
    +publish()
  }
markdown```mermaid
classDiagram
  Author -- Book
  Book -- Author

  class Author {
    +String name
    +List~Book~ books
    +writeBook()
  }

  class Book {
    +String title
    +Author author
    +publish()
  }
```

問題点の分析

この設計では、著者が書籍のリストを持ち、書籍も著者への参照を持つ双方向関連になっています。一見便利に見えますが、以下のような問題があります。

  1. 循環参照: JSON シリアライズ時に無限ループが発生する可能性
  2. 整合性の維持が困難: 片方だけ更新すると不整合が生じる
  3. テストの複雑化: モックオブジェクトの準備が煩雑になる
  4. メモリ管理: 双方が参照を持つため、ガベージコレクションが効きにくい

改善例:単方向関連への変更

mermaidclassDiagram
  Book --> Author

  class Author {
    +String authorId
    +String name
    +writeBook()
  }

  class Book {
    +String bookId
    +String title
    +Author author
    +publish()
  }
markdown```mermaid
classDiagram
  Book --> Author

  class Author {
    +String authorId
    +String name
    +writeBook()
  }

  class Book {
    +String bookId
    +String title
    +Author author
    +publish()
  }
```

単方向関連に変更することで、依存の方向が明確になり、循環参照のリスクが排除されます。著者の書籍リストが必要な場合は、別途クエリで取得する設計にすることで、整合性の問題も解決できます。

アンチパターン 4: インターフェースと実装の混同

悪い例:具象クラスへの直接依存

mermaidclassDiagram
  OrderService --> MySQLRepository

  class OrderService {
    -MySQLRepository repository
    +createOrder()
    +getOrder()
  }

  class MySQLRepository {
    +insert()
    +select()
    +update()
    +delete()
  }
markdown```mermaid
classDiagram
  OrderService --> MySQLRepository

  class OrderService {
    -MySQLRepository repository
    +createOrder()
    +getOrder()
  }

  class MySQLRepository {
    +insert()
    +select()
    +update()
    +delete()
  }
```

問題点の分析

この設計では、OrderService が具象クラス MySQLRepository に直接依存しています。これは以下の問題を引き起こします。

  1. データベースの変更が困難: PostgreSQL に変更する際、OrderService も修正が必要
  2. テストの困難さ: インメモリリポジトリに差し替えづらい
  3. 依存関係逆転の原則違反: 上位モジュールが下位モジュールの詳細に依存している

改善例:インターフェースの導入

mermaidclassDiagram
  OrderService --> OrderRepository
  OrderRepository <|.. MySQLRepository
  OrderRepository <|.. PostgreSQLRepository

  class OrderService {
    -OrderRepository repository
    +createOrder()
    +getOrder()
  }

  class OrderRepository {
    <<interface>>
    +save()*
    +findById()*
  }

  class MySQLRepository {
    +save()
    +findById()
  }

  class PostgreSQLRepository {
    +save()
    +findById()
  }
markdown```mermaid
classDiagram
  OrderService --> OrderRepository
  OrderRepository <|.. MySQLRepository
  OrderRepository <|.. PostgreSQLRepository

  class OrderService {
    -OrderRepository repository
    +createOrder()
    +getOrder()
  }

  class OrderRepository {
    <<interface>>
    +save()*
    +findById()*
  }

  class MySQLRepository {
    +save()
    +findById()
  }

  class PostgreSQLRepository {
    +save()
    +findById()
  }
```

インターフェースを導入することで、OrderService は抽象に依存するようになり、データベースの実装を自由に差し替えられます。<|.. 記号はインターフェースの実装(realization)を表し、実線の三角形矢印(継承)とは区別されます。

記法の一覧表

混乱を避けるために、Mermaid class diagram で使用する主要な記法を一覧にまとめました。

#記号名称意味使用例
1<--継承(inheritance)is-a の関係Animal <-- Dog
2*--コンポジション(composition)強い has-a 関係House *-- Room
3o--集約(aggregation)弱い has-a 関係University o-- Professor
4--関連(association)双方向の参照関係Customer -- Order
5-->関連(association)単方向の参照関係Order --> Product
6<..実現(realization)インターフェース実装List <.. ArrayList
7..>依存(dependency)一時的な使用関係Service ..> Logger

この表を参考に、設計意図に合った記法を選択してください。

まとめ

本記事では、Mermaid class diagram における継承・集約・関連の表し分けと、陥りがちなアンチパターンを詳しく解説しました。

重要なポイントの再確認

**継承(<|--)**は「is-a」の関係が明確な場合にのみ使用し、単なるコードの再利用目的では使わないようにしましょう。ポリモーフィズムを活用したい場合に最適です。

**コンポジション(*--)**は親オブジェクトが子オブジェクトのライフサイクルを完全に管理する強い所有関係を表します。親が削除されたら子も削除されるべき場合に使用してください。

**集約(o--)**は親と子が独立したライフサイクルを持つ弱い所有関係です。子オブジェクトが複数の親に所属できる場合や、親が削除されても子が残る場合に適しています。

**関連(-- または -->)**は最も緩い関係性で、単なる参照や利用関係を表します。疎結合を保ちたい場合は単方向関連を選び、双方向関連は本当に必要な場合のみに限定しましょう。

アンチパターンを避けるための原則

  1. is-a と has-a を明確に区別する: 継承は is-a、コンポジションと集約は has-a
  2. ライフサイクルを考慮する: 親が削除されたら子も削除すべきか?
  3. 依存の方向を意識する: 双方向関連は循環参照の温床
  4. 抽象に依存する: 具象クラスではなくインターフェースに依存する設計を心がける

これらの原則を守ることで、保守性が高く、テストしやすい設計を Mermaid で表現できます。クラス図は単なるドキュメントではなく、チーム全体の設計思想を共有する重要なツールです。正しい記法で正確な意図を伝えることが、品質の高いソフトウェア開発につながります。

明日からのドキュメント作成では、本記事で学んだ記法と使い分けの原則を活かして、わかりやすく正確なクラス図を描いてください。Mermaid の手軽さと表現力を最大限に活用し、チームメンバーとの認識のズレをなくしていきましょう。

関連リンク