🎄アドカレ最終日🎄
くずばが25日目のアドカレを担当します。
はじめに
UML図(クラス図)を見た時、こんな疑問を持ったことはありませんか?
「なぜこの矢印は上向きなんだろう?」
Rubyでclass Dog < Animalと書く時、AnimalのメソッドがDogに「降りてくる」イメージなので下向きなのでは?と思うことがありました。
本記事では、後輩に説明する気持ちでUML図の矢印が表す「依存の方向」をRubyの実装と結びつけて整理し、最後Repository/Decoratorパターンの実装にどう活かされるかを整理します。
この記事で出てくるキーワード整理
UML図
├── 矢印の種類
│ ├── 白抜き三角矢印(継承 / インターフェース実装)
│ └── 黒矢印(依存・関連)
├── 記号の意味
│ ├── +(public)
│ ├── -(private)
│ └── #(protected)
└── 関係性
├── is-a関係(継承):「○○である」
├── can-do関係(インターフェース実装):「○○できる」
└── has-a関係(コンポジション):「○○を持つ」
デザインパターン
├── Repositoryパターン
│ ├── データアクセスの抽象化
│ ├── テスタビリティ向上
│ └── 依存性逆転の原則(DIP)
└── Decoratorパターン
├── 動的な機能追加
├── 既存コード変更不要
└── オープン・クローズドの原則
UML図の矢印が表すもの
このUML図を実装してみて考えていきます。
Rubyで書いてみる
class Animal
def walk
"トコトコ歩く"
end
end
class Dog < Animal
end
# Dogインスタンスで継承したwalkメソッドを呼び出す
dog = Dog.new
puts dog.walk # => トコトコ歩く
コードを書いている時、Animalのメソッドを使ったり、上書いたりと「Animalのメソッドが降りてくる感覚」になります。
しかし、UML図では矢印が上向きとなります。
これはどういうことでしょうか?
UML図の矢印は「誰が誰に依存しているか」を表す
UML図の矢印は依存の方向を表しています。
では「依存」とは何でしょうか?
それは「相手がいないと自分が定義できない関係」です。
コードを見てみましょう。Dogを定義する時、「Animalを継承する」と明示的に書く必要があります。
class Dog < Animal # ← Animalという名前を書かないと定義できない
end
つまり、DogはAnimalの存在を知っている必要があるということです。
(「呼吸する」「食べる」といった動物としての基本機能がないと犬は存在できないので、現実世界のイメージともあってる🐕)
一方、Animalを定義する時にDogについては何も書きません。
class Animal
def walk
"トコトコ歩く"
end
end
AnimalはDogには依存していないので、Dogについては知ったこっちゃない、という状態です。
まとめると、
- Dogクラス: Animalクラスを参照している(依存している)
- Animalクラス: Dogクラスを知らない(依存していない)
という関係がいえます。
これこそが、UML図の矢印が表している意味になります。
依存関係についてもう少し考えてみる
もしAnimalを削除したらどうなるでしょうか?
# class Animal
# def walk
# "トコトコ歩く"
# end
# end
class Dog < Animal # ← 継承するクラスがいないのでエラーになってしまう
end
AnimalがないとそもそもDogが定義できません。
しかし、逆だと違います。
class Animal
def walk
"トコトコ歩く"
end
end
# class Dog < Animal
# end
# 問題なく動く
animal = Animal.new
Dogを削除しても動くことから、AnimalはDogに依存していないということが確認できました。
つまり、
DogはAnimalに依存しているが、AnimalはDogに依存していない実装であるということがわかりました。
結局、矢印の向きは何を表しているのか?
上記のようなUML図の場合、クラス定義時の参照の方向(依存の方向) を表していて、メソッドの継承など機能の流れは逆になります。
| 視点 | 矢印の向き | 意味 |
|---|---|---|
| 機能の流れ | Animal から Dog | メソッドが降りてくる |
| 依存の方向 | Dog から Animal | Dogが親を参照している |
UML図の矢印は「機能の流れ」ではなく「依存の方向」を表します。
メソッド継承とは逆になる。
UML図の矢印の種類
先ほどは継承における依存を見てきました。
UML図には他にも、異なる関係性を表す矢印があります。
白抜き三角矢印:継承(is-a関係)
is-a関係は「○○である」という関係を表します。
class Dog < Animal
end
- Dog は Animalである
- UML図: 白抜き三角矢印(↑)
- 依存関係: DogがAnimalに依存
- コード:
class Dog < Animal
特徴:
- 子クラスは親クラスの型として扱える
- 継承によってメソッドを受け継ぐ
- 子クラスは親クラスなしでは定義できない
黒矢印:関連・依存(has-a関係)
has-a関係は「○○を持つ」という関係を表します。
class Person
def initialize(pet)
@pet = pet
end
def play_with_pet
@pet.walk
end
end
これも依存関係の一種です。PersonはPetを保持するため、Petがないと処理が完結しません。
- Person は Petを持つ
- UML図: 黒矢印(→)
- 依存関係: PersonがPetに依存
- Rubyコード:
@pet = pet
特徴:
- 別のオブジェクトのインスタンスを保持する
- 保持するオブジェクトのメソッドを呼び出す
- 保持するクラスがないと処理が完結しない
白抜き三角矢印のもう一つの使い方:インターフェース実装
実は、白抜き三角矢印は継承だけでなくインターフェースの実装も表します。
module Swimmable
def swim
raise NotImplementedError
end
end
class Dog < Animal
include Swimmable # インターフェース実装(モジュールの取り込み)
def swim
"犬かきで泳ぐ"
end
end
インターフェース実装はcan-do関係(○○できる)とも呼ばれます。
- Dog は 泳げる(能力を持つ)
- UML図: 白抜き三角矢印
- 依存関係: DogがSwimmableに依存
- コード:
include Swimmable/extend Swimmable
特徴:
- 複数のモジュールを取り込める
- 「能力」や「振る舞い」を追加できる
- モジュールがないと定義はできるが、メソッドが使えない
関係性の違いまとめ
| 関係 | 意味 | 矢印 | 依存 | Rubyコード | 例 |
|---|---|---|---|---|---|
| is-a | ○○である | 白抜き三角 | 子→親 | class Dog < Animal |
犬は動物である |
| can-do | ○○できる | 白抜き三角 | 実装→インターフェース | include Swimmable |
犬は泳げる |
| has-a | ○○を持つ | 黒矢印 | 保持者→被保持者 | @pet = pet |
人はペットを飼っている |
※UML仕様では継承は実線、インターフェース実装は点線で区別されますが、Mermaidなど多くのツールでは両方とも実線の白抜き三角矢印で表示されます。
UML図の記号
クラス図でよく使われる記号をまとめておきます。
| 記号 | 意味 | 例 |
|---|---|---|
| + | public(公開) | +speak() |
| - | private(非公開) | -internal_method() |
| # | protected | #family_method() |
| 白抜き三角 | 継承・実装(is-a) | Dog < Animal |
| 黒矢印 | 関連・依存(has-a) | Person has Pet |
class Dog
def walk # ← public(+)
internal_process
end
private
def internal_process # ← private(-)
"内部処理"
end
end
Repositoryパターン:has-a関係の活用
パターンの概要
Repositoryパターンは、データアクセスロジックをビジネスロジックから分離するパターンです。
ブログシステムを例に見てみましょう。
# インターフェース(抽象)
class ArticleRepository
def find_by_id(id)
raise NotImplementedError
end
end
# DB実装
class DBArticleRepository < ArticleRepository
def find_by_id(id)
DB.query("SELECT * FROM articles WHERE id = ?", id)
end
end
# テスト用実装
class MockArticleRepository < ArticleRepository
def find_by_id(id)
{ id: id, title: "テスト記事" }
end
end
# ブログサービス
class BlogService
def initialize(repository)
@repository = repository # has-a関係
end
def show_article(id)
@repository.find_by_id(id)
end
end
UML図で表すと
依存の方向:
- BlogService → ArticleRepository(has-a: 黒矢印)
- DBArticleRepository → ArticleRepository(is-a: 白抜き三角)
- MockArticleRepository → ArticleRepository(is-a: 白抜き三角)
使い方
# 本番環境
service = BlogService.new(DBArticleRepository.new)
# テスト環境
service = BlogService.new(MockArticleRepository.new)
BlogServiceは「どのRepository実装が渡されるか」を気にせず、find_by_idを呼ぶだけです。
なぜこの設計が良いのか
1. データアクセスの抽象化
BlogServiceは「どうやってデータを取るか」を知りません。SQLを書く必要がなく、ビジネスロジックに集中できます。
2. テストしやすい
RSpec.describe BlogService do
it '記事を取得できる' do
mock_repo = MockArticleRepository.new
service = BlogService.new(mock_repo)
article = service.show_article(1)
expect(article[:title]).to eq("テスト記事")
end
end
実際のDBなしでテストできます。
3. データストアを変更しても影響を受けない
# MongoDBに変更
class MongoArticleRepository < ArticleRepository
def find_by_id(id)
mongo_client.collection('articles').find({ _id: id })
end
end
# BlogServiceは変更不要
service = BlogService.new(MongoArticleRepository.new)
4. 依存性逆転の原則(DIP)
通常は上位層→下位層への依存ですが、DIPでは両方とも抽象に依存することで、下位層の変更が上位層に影響しなくなります。
Repositoryパターンの本質は「データアクセスの抽象化」です。
実装が1つしかない場合はインターフェースは不要ですが、テスタビリティを向上させたい場合は導入すると良さそうです。
Decoratorパターン:is-a と has-a の組み合わせ
パターンの概要
Decoratorパターンは、既存のオブジェクトに動的に機能を追加するパターンです。
バドミントンの試合設定を例にします。
(筆者はバドミントンをやっているのですが、もし審判アプリを作るとしたら、試合ルールの設定部分はこんな感じで実装するかなという例です🏸)
# 基本インターフェース
class BadmintonGame
def rules
raise NotImplementedError
end
def description
raise NotImplementedError
end
end
# 標準ルール
class StandardGame < BadmintonGame
def rules
{ games_to_win: 2, points_per_game: 21 }
end
def description
"2ゲーム先取、21点マッチ"
end
end
# デコレーター基底クラス
class GameDecorator < BadmintonGame
def initialize(game)
@game = game # has-a関係
end
end
# 1ゲーム先取に変更
class OneGameDecorator < GameDecorator
def rules
@game.rules.merge(games_to_win: 1)
end
def description
@game.description.gsub("2ゲーム", "1ゲーム")
end
end
# 15点マッチに変更
class FifteenPointDecorator < GameDecorator
def rules
@game.rules.merge(points_per_game: 15)
end
def description
@game.description.gsub("21点", "15点")
end
end
# デュースルール追加(ルール設定の追加例)
class DeuceDecorator < GameDecorator
def rules
@game.rules.merge(deuce_required: true)
end
def description
@game.description + "(デュースあり)"
end
end
UML図で表すと
重要なポイント:
- OneGameDecorator は BadmintonGame である(is-a: 継承)
- OneGameDecorator は BadmintonGameを持つ(has-a: 保持)
この2つの関係の組み合わせで、入れ子構造が実現できます。
使い方
# 標準ルール: 2ゲーム先取、21点
game = StandardGame.new
puts game.description # => "2ゲーム先取、21点マッチ"
puts game.rules # => { games_to_win: 2, points_per_game: 21 }
# 練習試合: 1ゲーム先取、21点
practice_game = OneGameDecorator.new(StandardGame.new)
puts practice_game.description # => "1ゲーム先取、21点マッチ"
puts practice_game.rules # => { games_to_win: 1, points_per_game: 21 }
# 時短ルール: 2ゲーム先取、15点
quick_game = FifteenPointDecorator.new(StandardGame.new)
puts quick_game.description # => "2ゲーム先取、15点マッチ"
puts quick_game.rules # => { games_to_win: 2, points_per_game: 15 }
# 初心者向け: 1ゲーム先取、15点
beginner_game = FifteenPointDecorator.new(OneGameDecorator.new(StandardGame.new))
puts beginner_game.description # => "1ゲーム先取、15点マッチ"
puts beginner_game.rules # => { games_to_win: 1, points_per_game: 15 }
# 公式戦: 2ゲーム先取、21点、デュースあり
official_game = DeuceDecorator.new(StandardGame.new)
puts official_game.description # => "2ゲーム先取、21点マッチ(デュースあり)"
puts official_game.rules # => { games_to_win: 2, points_per_game: 21, deuce_required: true }
なぜこの設計が良いのか
1. 既存コードを変更せず機能追加できる
StandardGameクラスは一切変更せず、新しいルールを追加できます。
# 新しいルール追加(ゴールデンポイント制)
class GoldenPointDecorator < GameDecorator
def rules
@game.rules.merge(golden_point: true)
end
def description
@game.description + "(ゴールデンポイント)"
end
end
# すぐに使える
game = GoldenPointDecorator.new(StandardGame.new)
2. 組み合わせが自由
# パターン1:
# 1ゲーム先取だけ(通常の練習だとこのルールにしたい)
OneGameDecorator.new(StandardGame.new)
# パターン2:
# 15点マッチだけ(本番の2セット先取に近づけたいけど時間がない時かな)
FifteenPointDecorator.new(StandardGame.new)
# パターン3:
# 1ゲーム先取 + 15点(10分以内で終わらせたい時、ミニゲームの時ですね)
FifteenPointDecorator.new(OneGameDecorator.new(StandardGame.new))
# パターン4:
# 1ゲーム先取 + 15点 + デュース(競った時の練習がしたい時はこれかな)
DeuceDecorator.new(
FifteenPointDecorator.new(
OneGameDecorator.new(StandardGame.new)
)
)
3. オープン・クローズドの原則
「拡張に対して開いており、修正に対して閉じている」という設計原則を満たします。
- 開いている: 新しいDecoratorを追加できる
- 閉じている: 既存クラス(StandardGame)を変更しない
Decoratorパターンでは、Decorator自身も同じインターフェースを実装します。
これにより「ゲームルール」が「ゲーム」として扱え、何層でも重ねられる構造になります。
まとめ
内容が盛りだくさんになったため、この記事で登場した主要な内容とキーワードを整理します。
UML図の矢印の意味
| UML図 | 意味 | Rubyコード | 関係性 |
|---|---|---|---|
| 白抜き三角(↑) | 継承 | class Dog < Animal |
is-a(○○である) |
| 白抜き三角(↑) | インターフェース実装 | include Swimmable |
can-do(○○できる) |
| 黒矢印(→) | 関連・依存 | @pet = pet |
has-a(○○を持つ) |
デザインパターンでの活用
| パターン | 使う関係 | 目的 |
|---|---|---|
| Repository | has-a + is-a/can-do | データアクセスの抽象化 |
| Decorator | is-a/can-do + has-a | 動的な機能追加 |