3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🎄アドカレ最終日🎄
くずばが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 動的な機能追加

参考文献

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?