Menu
Coddy logo textTech

シングルトンパターン

CoddyのRubyジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 42/57。

デザインパターンとは、一般的な設計上の問題に対する実証済みの解決策です。シングルトンパターンは、クラスに必ず1つだけのインスタンスがあり、どこからでもアクセスできるようにします。Rubyの標準ライブラリにはこれが組み込まれており、require 'singleton'とinclude Singletonによってクラスメソッドinstanceが追加されます:

require 'singleton'

class Settings
  include Singleton
  attr_accessor :theme

  def initialize
    @theme = "light"
  end
end

Settings.instance.theme = "dark"
puts Settings.instance.theme
puts Settings.instance.equal?(Settings.instance)

出力:

dark
true

include Singletonはnewもprivateにするため、2つ目のインスタンスを作成できません。instanceを要求するプログラムのすべての部分が、同じオブジェクトを取得します:

require 'singleton'

class Counter
  include Singleton
  attr_reader :hits

  def initialize
    @hits = 0
  end

  def hit
    @hits += 1
  end
end

def page_a
  Counter.instance.hit
end

def page_b
  2.times { Counter.instance.hit }
end

page_a
page_b
puts Counter.instance.hits
begin
  Counter.new
rescue NoMethodError => e
  puts e.message
end

出力:

3
private method `new' called for class Counter

同じアイデアを手動で記述すると、初回使用時にインスタンスを作成するクラスメソッドと、privateなnewになります。これは、モジュールが代わりに行ってくれることを示しています。

class Config
  def self.instance
    @instance ||= new
  end
  private_class_method :new

  attr_accessor :level

  def initialize
    @level = 1
  end
end

Config.instance.level = 3
puts Config.instance.level

出力:

3

シングルトンは控えめに使いましょう。シングルトンはグローバル状態です。どのコードからでも変更でき、テストをクリーンなコピーから開始することもできません。ロガーや設定など、プログラムごとに本当に1つだけ存在するものには適しています。オブジェクトを引数として渡すほうが、多くの場合は明確です。

# テストしやすい: ロガーが渡される
def process(order, log)
  log.write("processing #{order.id}")
end
challenge icon

チャレンジ

簡単

AppLogger シングルトンを include Singleton で完成させてください。エントリの配列を保持し、log(message, level) を備えています。このメソッドは [INFO] message または [ERROR] message(レベルは大文字)を保存し、entries と error_count も提供します。functions.rb のヘルパー handle_info(message) と handle_error(message) はどちらも AppLogger.instance を通じてログを記録するため、同じオブジェクトに到達します。

提供されたコードは info,message または error,message の行を読み取り、ヘルパーを通じて処理し、すべてのエントリと errors: 1 を出力して、AppLogger.new が拒否されることを確認します。

コードは app_logger.rb と functions.rb に記述します。main.rb には提供された入力/出力コードが含まれており、編集できません。

自分で試してみよう

require_relative 'app_logger'
require_relative 'functions'

# 提供された入出力コード: そのまま維持する
input = []
while (line = gets)
  input << line.chomp
end
input.each do |line|
  level, message = line.split(",", 2)
  level == "error" ? handle_error(message) : handle_info(message)
end
AppLogger.instance.entries.each { |e| puts e }
puts "errors: #{AppLogger.instance.error_count}"
begin
  AppLogger.new
  puts "new is allowed"
rescue NoMethodError
  puts "new is private"
end
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Rubyオンラインコンパイラ