web-dev-qa-db-ja.com

Rubyデザインパターン:拡張可能なファクトリクラスを作成する方法?

わかりました、Rubyプログラムがバージョン管理ログファイルを読み取り、データを使って何かをするプログラムがあるとします(私はしませんが、状況は類似しており、これらの類似点を楽しんでいます)。今、BazaarとGitをサポートしたいとしましょう、使用されているバージョン管理ソフトウェアを示す何らかの引数を付けてプログラムが実行されるとしましょう。

これを踏まえて、バージョン管理プログラムの名前を指定すると、適切なログファイルリーダー(ジェネリックからサブクラス化された)を返し、ログファイルを読み取って正規の内部表現を吐き出すLogFileReaderFactoryを作成します。もちろん、BazaarLogFileReaderとGitLogFileReaderを作成してプログラムにハードコーディングすることもできますが、新しいバージョン管理プログラムのサポートを追加することは、新しいクラスファイルを展開するのと同じくらい簡単な方法でセットアップする必要があります。 BazaarおよびGitリーダーのあるディレクトリ。

したがって、現時点では、「ログを使用して何かを行う-ソフトウェアgit」や「ログを使用して何かを行う-ソフトウェアBazaar」を呼び出すことができます。これらのログリーダーがあるためです。私が欲しいのは、SVNLogFileReaderクラスとファイルを同じディレクトリに単に追加し、残りの部分に変更を加えることなく、「do-something-with-the-log --software svn」を自動的に呼び出すことができるようにすることです。プログラム。 (もちろん、ファイルに特定のパターンで名前を付け、require呼び出しでグロブ化することができます。)

これはRubyで実行できることはわかっています...どうすればいいのかわからないのです...あるいは、私がそれを実行する必要があるかどうかです。

50
Instance Hunter

LogFileReaderFactoryは必要ありません。 LogFileReaderクラスに、そのサブクラスをインスタンス化する方法を教えるだけです。

class LogFileReader
  def self.create type
    case type 
    when :git
      GitLogFileReader.new
    when :bzr
      BzrLogFileReader.new
    else
      raise "Bad log file type: #{type}"
    end
  end
end

class GitLogFileReader < LogFileReader
  def display
    puts "I'm a git log file reader!"
  end
end

class BzrLogFileReader < LogFileReader
  def display
    puts "A bzr log file reader..."
  end
end

ご覧のとおり、スーパークラスは独自のファクトリとして機能できます。では、自動登録はどうでしょうか?さて、登録したサブクラスのハッシュを保持し、それらを定義するときにそれぞれを登録しないのはなぜですか。

class LogFileReader
  @@subclasses = { }
  def self.create type
    c = @@subclasses[type]
    if c
      c.new
    else
      raise "Bad log file type: #{type}"
    end
  end
  def self.register_reader name
    @@subclasses[name] = self
  end
end

class GitLogFileReader < LogFileReader
  def display
    puts "I'm a git log file reader!"
  end
  register_reader :git
end

class BzrLogFileReader < LogFileReader
  def display
    puts "A bzr log file reader..."
  end
  register_reader :bzr
end

LogFileReader.create(:git).display
LogFileReader.create(:bzr).display

class SvnLogFileReader < LogFileReader
  def display
    puts "Subersion reader, at your service."
  end
  register_reader :svn
end

LogFileReader.create(:svn).display

そして、それがあります。それをいくつかのファイルに分割し、それらを適切に要求するだけです。

この種のことに興味がある場合は、Peter Norvigの 動的言語の設計パターン を読んでください。彼は、プログラミング言語の制限や不備を実際に回避しているデザインパターンの数を示しています。十分に強力で柔軟性のある言語を使用すれば、デザインパターンを実際に必要とせず、やりたいことを実装するだけです。彼は例としてDylanとCommon LISPを使用していますが、彼のポイントの多くはRubyにも関連しています。

Rubyの感動的なガイド 、特に第5章と第6章もご覧ください。

edit:ヨルクの答えをリフする。繰り返しを減らすのが好きなので、クラスと登録の両方でバージョン管理システムの名前を繰り返さないでください。 2番目の例に以下を追加すると、かなり単純で理解しやすいまま、はるかに単純なクラス定義を記述できます。

def log_file_reader name, superclass=LogFileReader, &block
  Class.new(superclass, &block).register_reader(name)
end

log_file_reader :git do
  def display
    puts "I'm a git log file reader!"
  end
end

log_file_reader :bzr do
  def display
    puts "A bzr log file reader..."
  end
end

もちろん、本番用コードでは、渡された名前に基づいて定数定義を生成することにより、実際にこれらのクラスに名前を付け、エラーメッセージを改善することができます。

def log_file_reader name, superclass=LogFileReader, &block
  c = Class.new(superclass, &block)
  c.register_reader(name)
  Object.const_set("#{name.to_s.capitalize}LogFileReader", c)
end
94
Brian Campbell

これは、ブライアンキャンベルのソリューションを実際に試しただけです。これが気に入ったら、お願い賛成his答えも:彼はすべての仕事をしてくれました。

_#!/usr/bin/env Ruby

class Object; def eigenclass; class << self; self end end end

module LogFileReader
  class LogFileReaderNotFoundError < NameError; end
  class << self
    def create type
      (self[type] ||= const_get("#{type.to_s.capitalize}LogFileReader")).new
    rescue NameError => e
      raise LogFileReaderNotFoundError, "Bad log file type: #{type}" if e.class == NameError && e.message =~ /[^: ]LogFileReader/
      raise
    end

    def []=(type, klass)
      @readers ||= {type => klass}
      def []=(type, klass)
        @readers[type] = klass
      end
      klass
    end

    def [](type)
      @readers ||= {}
      def [](type)
        @readers[type]
      end
      nil
    end

    def included klass
      self[klass.name[/[[:upper:]][[:lower:]]*/].downcase.to_sym] = klass if klass.is_a? Class
    end
  end
end

def LogFileReader type
_

ここでは、LogFileReaderというグローバルメソッド(実際にはプロシージャのような)を作成します。これは、モジュールLogFileReaderと同じ名前です。これはRubyでは合法です。あいまいさは次のように解決されます。モジュールが明らかにメソッド呼び出しである場合を除いて、モジュールは常に優先されます。つまり、最後に括弧を付けるか(Foo())、または引数を渡します(_Foo :bar_) 。

これはstdlibのいくつかの場所で使用されているトリックであり、Campingやその他のフレームワークでも使用されています。 includeextendのようなものは実際にはキーワードではなく、通常のパラメーターを取る通常のメソッドなので、実際のModuleを引数として渡す必要はありません。evaluatesを渡すこともできますModule。実際、これは継承でも機能します。class Foo < some_method_that_returns_a_class(:some, :params)を記述することは完全に合法です。

このトリックを使用すると、Rubyにジェネリックがない場合でも、ジェネリッククラスから継承しているように見せることができます。これは、たとえば次のようなことを行う委任ライブラリで使用されます。 class MyFoo < SimpleDelegator(Foo)、そして何が起こるかというと、SimpleDelegatormethodは、SimpleDelegatorclassの匿名サブクラスを動的に作成して返します。これにより、すべてのメソッド呼び出しがFooクラスのインスタンス。

ここでも同様のトリックを使用します。Moduleを動的に作成します。これは、クラスに混合されると、そのクラスをLogFileReaderレジストリに自動的に登録します。

_  LogFileReader.const_set type.to_s.capitalize, Module.new {
_

この行だけでたくさんのことが起こっています。右から始めましょう:_Module.new_は新しい匿名モジュールを作成します。渡されたブロックは、モジュールの本体になります。これは、moduleキーワードを使用するのと基本的に同じです。

では、_const_set_に移ります。定数を設定する方法です。つまり、事前に知る必要がなく、定数の名前をパラメーターとして渡すことができるのは、_FOO = :bar_、exceptと同じです。 LogFileReaderモジュールでメソッドを呼び出しているため、定数はその名前空間内で定義され、IOWは_LogFileReader::Something_という名前になります。

では、定数の名前isは何でしょうか?まあ、それは大文字小文字を区別してメソッドに渡されるtype引数です。したがって、_:cvs_を渡すと、結果の定数は_LogFileParser::Cvs_になります。

そして、定数を何に設定しますか?新しく作成されたanonymousモジュールに、これはもはや匿名ではありません!

これらすべては、_module LogFileReader::Cvs_を言うための長い時間のかかる方法ですが、 "Cvs"の部分が事前にわからなかったため、そのように記述することができませんでした。

_    eigenclass.send :define_method, :included do |klass|
_

これがモジュールの本体です。ここでは、_define_method_を使用して、includedというメソッドを動的に定義します。そして、実際にはモジュール自体でメソッドを定義するのではなく、モジュールのeigenclass(上記で定義した小さなヘルパーメソッドを介して)で定義します。これは、メソッドがインスタンスメソッドにならないことを意味します。むしろ「静的」メソッド(Java/.NET用語で)。

includedは実際には特別なフックメソッドであり、モジュールがクラスに含まれ、クラスが引数として渡されるたびにRubyランタイムによって呼び出されます。したがって、新しく作成されたモジュールどこかに含まれるときはいつでもそれを通知するフックメソッドを持っています。

_      LogFileReader[type] = klass
_

そして、これがフックメソッドの機能です。フックメソッドに渡されるクラスをLogFileReaderレジストリに登録します。そして、それを登録するキーは、上記のtypeメソッドからのLogFileReader引数です。クロージャーの魔法のおかげで、実際にはincludedメソッド内でアクセスできます。

_    end
    include LogFileReader
_

最後に重要なことですが、LogFileReaderモジュールを匿名モジュールに含めます。 [注:元の例ではこの行を忘れていました。]

_  }
end

class GitLogFileReader
  def display
    puts "I'm a git log file reader!"
  end
end

class BzrFrobnicator
  include LogFileReader
  def display
    puts "A bzr log file reader..."
  end
end

LogFileReader.create(:git).display
LogFileReader.create(:bzr).display

class NameThatDoesntFitThePattern
  include LogFileReader(:darcs)
  def display
    puts "Darcs reader, lazily evaluating your pure functions."
  end
end

LogFileReader.create(:darcs).display

puts 'Here you can see, how the LogFileReader::Darcs module ended up in the inheritance chain:'
p LogFileReader.create(:darcs).class.ancestors

puts 'Here you can see, how all the lookups ended up getting cached in the registry:'
p LogFileReader.send :instance_variable_get, :@readers

puts 'And this is what happens, when you try instantiating a non-existent reader:'
LogFileReader.create(:gobbledigook)
_

この新しい拡張バージョンでは、LogFileReadersを定義する3つの異なる方法を使用できます。

  1. 名前がパターン_<Name>LogFileReader_に一致するすべてのクラスは自動的に検出され、_:name_のLogFileReaderとして登録されます(参照:GitLogFileReader)、
  2. LogFileReaderモジュールで混在し、名前がパターン_<Name>Whatever_に一致するすべてのクラスは、_:name_ハンドラーに登録されます(BzrFrobnicatorを参照)。
  3. LogFileReader(:name)モジュールで混在するすべてのクラスは、名前に関係なく、_:name_ハンドラーに登録されます(NameThatDoesntFitThePatternを参照)。

これは非常に不自然なデモにすぎないことに注意してください。たとえば、これは間違いなくnotスレッドセーフです。また、メモリリークの可能性もあります。注意して使用してください!

18
Jörg W Mittag

ブライアンキャンベルの回答に対するもう1つのマイナーな提案-

では、継承されたコールバックを使用してサブクラスを実際に自動登録できます。つまり.

class LogFileReader

  cattr_accessor :subclasses; self.subclasses = {}

  def self.inherited(klass)
    # turns SvnLogFileReader in to :svn
    key = klass.to_s.gsub(Regexp.new(Regexp.new(self.to_s)),'').underscore.to_sym

    # self in this context is always LogFileReader
    self.subclasses[key] = klass
  end

  def self.create(type)
    return self.subclasses[type.to_sym].new if self.subclasses[type.to_sym]
    raise "No such type #{type}"
  end
end

今私たちは持っています

class SvnLogFileReader < LogFileReader
  def display
    # do stuff here
  end
end

登録する必要はありません

10
dlangevin

クラス名を登録する必要なく、これも機能するはずです

class LogFileReader
  def self.create(name)
    classified_name = name.to_s.split('_').collect!{ |w| w.capitalize }.join
    Object.const_get(classified_name).new
  end
end

class GitLogFileReader < LogFileReader
  def display
    puts "I'm a git log file reader!"
  end
end

そして今

LogFileReader.create(:git_log_file_reader).display
7
Robert Wahler