業務で「なぜSingletonパターンを採用したのか?」とレビュー指摘をもらいました。
そう言われてみるとただ単に既存の処理がSingletonパターンを採用していたからそのまま模倣しただけであって、「なぜ採用したのか?」理由はありませんでした。
そこで改めてSingletonパターンとは何か?、どのようなケースで使うべきか等を自分の理解のためにまとめました。
※内容に誤りがありましたら遠慮なくご指摘ください。
技術スタック
- Ruby 3.0.3
- Rails 6.1.6.1
Singletonパターンとは
Singletonパターンとは、インスタンスを1つしか作成しないことを保証するデザインパターンです。
初めて呼び出された際にそのクラスのインスタンスを生成し、2回目以降は同じインスタンスが再利用されるのが特徴です。
Singletonパターンを使うべきケース
Singletonパターンが採用されるのは主に以下の2つのケースです。
- インスタンスが保持する情報をアプリケーション全体で絶対に統一したい場合
- インスタンスの生成コストが高い場合
順番にまとめます。
1. インスタンスが保持する情報をアプリケーション全体で絶対に統一したい場合
繰り返しになりますが、Singletonパターンは一度に1つのインスタンスを持つことを保証するデザインパターンです。
2つ以上作成できてしまうと、各インスタンスが独自に情報を更新し、アプリケーション間でデータの不整合が起きかねません。
なのでアプリケーション全体で絶対にデータの一貫性を保ちたい場合はSingletonパターンを用います。
2. インスタンスの生成コストが高い場合
「インスタンスの生成コストが高い場合」というのは、たとえば次のようにYAMLファイルを読み込んでインスタンスを生成するケースです。
SAMPLE = YAML.load_file(Rails.root.join('config/sample.yml'))
YAMLファイルのデータが膨大だとインスタンスを呼び出すたびに毎回ファイルをロードしなきゃいけないのでパフォーマンスに悪影響を及ぼしかねません。
こういったケースではSingletonパターンを検討しても良いでしょう。
ただし生成コストの高いオブジェクトを管理する方法としては他にもオブジェクトプールパターン、フライウェイトパターンなどが存在しているようです。
「生成コストが高い」だけではSingletonパターンを採用する決定打にはなり得ません。
あくまでも1の「インスタンスが保持する情報をアプリケーション全体で絶対に統一したい場合」が最大の指標と言えるでしょう。
Singletonパターンの問題点
1. ユニットテストがやりにくい
何度も言いますが、Singletonパターンは1つのインスタンスを使い回すデザインパターンです。
これはユニットテストにとって不都合しかありません。
インスタンスを再利用するということはインスタンスの情報がテスト間で引き継がれる、ということだからです。
クラス間に依存関係が生じてしまい、疎結合ではなくなってしまいます。
これではユニットテストがやりにくくて仕方がありません。
2. 拡張性が低い
クラスを作ったときは「このクラスのインスタンスは1つしかいらない」と思っていても、後で「やっぱり必要だ」となるケースは往々にしてあります。
そのため安易にSingletonパターンを採用してしまうと、後々の拡張性に悪影響を及ぼしかねません。
Singletonパターンを採用する際は「絶対にこのクラスのインスタンスは1つだけで良いのか?」を熟考した方が良いですね。
3. 保守性が低い
Singletonパターンを採用したクラスのインスタンスは、グローバル変数と同じようにどのクラスからでもアクセスできます。
グローバル変数と同様に1箇所でインスタンスの状態が変更されてしまうと、アプリケーション全体に影響を及ぼしてしまいます。
そのため状態が変更されないことが絶対に間違いないクラスだけにSingletonパターンを採用しなくてはいけません。
Singletonパターンの使い方
RailsでSingletonパターンを採用するのはとても簡単です。
次のようにファイルへ記述を追加すればOKです。
require 'singleton' # 追加
class SampleWordsClass
include Singleton # 追加
SAMPLE_WORDS_YAML = YAML.load_file(Rails.root.join('config/sample_words.yml'))
def initialize
@sample_words = SAMPLE_WORDS_YAML.values
end
#
# config/sample_words.ymlファイルに記載された単語が文章内に含まれているかどうかを返す。
#
# @param [String] text 確認対象の文章
# @return [Boolean] 単語が含まれている場合true
def include?(text)
/(#{@sample_words.join('|')})/i === text
end
#
# 確認対象の文章内に存在した単語の一覧を返す。
#
# @param [String] text 確認対象の文章
# @return [Array<String>] 存在した単語の一覧
def matched(text)
@sample_words.select{ |sample_word| text.match(/#{sample_word}/i) }
end
end
上記のinclude?メソッド・matchedメソッドを呼び出す際は次のようにinstanceメソッドを用いて記述します。
# include?メソッドを呼び出す
SampleWordsClass.instance.include?(text)
# matchedメソッドを呼び出す
SampleWordsClass.instance.matched(text)
最後に
Singletonパターンについて調べたことをまとめてみました。
デザインパターンの中ではわかりやすいため私のような初心者でも簡単に扱えてしまいますが、思わぬトラブルの温床になりやすいため、本当にSingletonパターンを採用すべきか慎重に考え、安易に使わないようにしましょう。