Murayama blog.

プログラミング教育なブログ

Decorator

Rubyによるデザインパターンの勉強です。
提供は、

Rubyによるデザインパターン

Rubyによるデザインパターン



本日はSingletonパターンを勉強します。


デザインパターンと言えばSingletonかっていうくらい有名なパターンです。
Singletonとは「オブジェクトを1つしか作らない」ことを指します。


そういえば、僕が研修のとき、
初めて教えてもらったデザインパターンはSingletonパターンだったような気がする。


今回も本に載っているとおり、
Logger(ログを出力する)クラスを作ってSingletonパターンを実装してみます。


まずはログを出力するSimpleLoggerクラスを作成します。
SimpleLoggerクラスにはerror、warning、infoの3つのメソッドが存在します。
それぞれのメソッドはログの出力レベル(@level)によって出力有無を切り替えます。

class SimpleLogger
  
  attr_accessor :level
  
  ERROR = 1
  WARNING = 2
  INFO = 3
  
  def initialize
    @log = File.open("log.txt", "w")
    @level = WARNING
  end
  
  def error(msg)
    @log.puts(msg)
    @log.flush
  end

  def warning(msg)
    @log.puts(msg) if @level >= WARNING
    @log.flush
  end

  def info(msg)
    @log.puts(msg) if @level >= INFO
    @log.flush
  end
end

上記のSimpleLoggerクラスはまだSingletonになっていません。
なので通常とおりオブジェクトを生成して利用します。
呼び出しもとプログラムは以下のとおりです。

logger = SimpleLogger.new
logger.level = SimpleLogger::INFO

logger.info("info1")
logger.warning("warning2")
logger.error("error3")

ログファイル(log.txt)には以下のように出力されます。

info1
warning2
error3


上記のコードだと、newメソッドを呼び出すたびに新しいオブジェクトを生成することができます。
(つまり、Singletonではない、ということになります)


それでは、SimpleLoggerクラスを修正してSingletonにしてみます。
まずはSingletonとなるオブジェクトを保持するクラス変数を追加します。
次に、クラス変数に追加したオブジェクトを返すクラスメソッドを追加します。

class SimpleLogger
  
  attr_accessor :level
  
  ERROR = 1
  WARNING = 2
  INFO = 3
  
  def initialize
    @log = File.open("log.txt", "w")
    @level = WARNING
  end
  
  def error(msg)
    @log.puts(msg)
    @log.flush
  end

  def warning(msg)
    @log.puts(msg) if @level >= WARNING
    @log.flush
  end

  def info(msg)
    @log.puts(msg) if @level >= INFO
    @log.flush
  end

  @@instance = SimpleLogger.new
  
  def self.instance
    @@instance
  end
end

追加したのはコードの最後の部分です。

  @@instance = SimpleLogger.new
  
  def self.instance
    @@instance
  end

この変更により、呼び出しもとでは、newメソッドの呼び出しからinstanceメソッドの呼び出しに変更することでSingletonなオブジェクトを取得することができます。

logger = SimpleLogger.instance
logger.level = SimpleLogger::INFO

logger.info("info1")
logger.warning("warning2")
logger.error("error3")


もう少し修正してみます。
今のコードでは相変わらずnewメソッドを呼び出すことができてしまいます。
そこで、newメソッドの可視性をprivateに変更します。

class SimpleLogger
  
  attr_accessor :level
  
  ERROR = 1
  WARNING = 2
  INFO = 3
  
  def initialize
    @log = File.open("log.txt", "w")
    @level = WARNING
  end
  
  def error(msg)
    @log.puts(msg)
    @log.flush
  end

  def warning(msg)
    @log.puts(msg) if @level >= WARNING
    @log.flush
  end

  def info(msg)
    @log.puts(msg) if @level >= INFO
    @log.flush
  end

  @@instance = SimpleLogger.new
  
  def self.instance
    @@instance
  end
  
  private_class_method :new
end

newメソッドをprivateにするために最後の1行を追加しました。
Rubyには、とってつけたようなメソッドが用意されてるのがスゴいと思う。ちなみにインスタンスメソッドをprivateにする場合は、privateメソッドを利用するみたい。

  private_class_method :new

これで直接newメソッドを呼び出すことを禁止できました。


以上がクラスベースでSingletonパターンを実現する流れです。


ここからはRubyらしくSingletonパターンを実現するにはどうするか、ってお話です。


というか、、
Singletonパターンを実現するためにRubyにはSingletonモジュールが用意されています。
#なんでもアリな気がしてきました。。
Singletonモジュールを使って、さきほどのSimpleLoggerクラスを修正してみます。
requireと、includeを追加するだけです。

require "singleton"
class SimpleLogger
  include Singleton
  
  attr_accessor :level
  
  ERROR = 1
  WARNING = 2
  INFO = 3
  
  def initialize
    @log = File.open("log.txt", "w")
    @level = WARNING
  end
  
  def error(msg)
    @log.puts(msg)
    @log.flush
  end

  def warning(msg)
    @log.puts(msg) if @level >= WARNING
    @log.flush
  end

  def info(msg)
    @log.puts(msg) if @level >= INFO
    @log.flush
  end
end

Singletonモジュールをincludeすると、instanceメソッドが定義され、newメソッドがprivateに設定されます。


以上、おしまい。