わかりました、Rubyプログラムがバージョン管理ログファイルを読み取り、データを使って何かをするプログラムがあるとします(私はしませんが、状況は類似しており、これらの類似点を楽しんでいます)。今、BazaarとGitをサポートしたいとしましょう、使用されているバージョン管理ソフトウェアを示す何らかの引数を付けてプログラムが実行されるとしましょう。
これを踏まえて、バージョン管理プログラムの名前を指定すると、適切なログファイルリーダー(ジェネリックからサブクラス化された)を返し、ログファイルを読み取って正規の内部表現を吐き出すLogFileReaderFactoryを作成します。もちろん、BazaarLogFileReaderとGitLogFileReaderを作成してプログラムにハードコーディングすることもできますが、新しいバージョン管理プログラムのサポートを追加することは、新しいクラスファイルを展開するのと同じくらい簡単な方法でセットアップする必要があります。 BazaarおよびGitリーダーのあるディレクトリ。
したがって、現時点では、「ログを使用して何かを行う-ソフトウェアgit」や「ログを使用して何かを行う-ソフトウェアBazaar」を呼び出すことができます。これらのログリーダーがあるためです。私が欲しいのは、SVNLogFileReaderクラスとファイルを同じディレクトリに単に追加し、残りの部分に変更を加えることなく、「do-something-with-the-log --software svn」を自動的に呼び出すことができるようにすることです。プログラム。 (もちろん、ファイルに特定のパターンで名前を付け、require呼び出しでグロブ化することができます。)
これはRubyで実行できることはわかっています...どうすればいいのかわからないのです...あるいは、私がそれを実行する必要があるかどうかです。
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
これは、ブライアンキャンベルのソリューションを実際に試しただけです。これが気に入ったら、お願い賛成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やその他のフレームワークでも使用されています。 include
やextend
のようなものは実際にはキーワードではなく、通常のパラメーターを取る通常のメソッドなので、実際のModule
を引数として渡す必要はありません。evaluatesを渡すこともできますModule
。実際、これは継承でも機能します。class Foo < some_method_that_returns_a_class(:some, :params)
を記述することは完全に合法です。
このトリックを使用すると、Rubyにジェネリックがない場合でも、ジェネリッククラスから継承しているように見せることができます。これは、たとえば次のようなことを行う委任ライブラリで使用されます。 class MyFoo < SimpleDelegator(Foo)
、そして何が起こるかというと、SimpleDelegator
methodは、SimpleDelegator
classの匿名サブクラスを動的に作成して返します。これにより、すべてのメソッド呼び出しが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)
_
この新しい拡張バージョンでは、LogFileReader
sを定義する3つの異なる方法を使用できます。
<Name>LogFileReader
_に一致するすべてのクラスは自動的に検出され、_:name
_のLogFileReader
として登録されます(参照:GitLogFileReader
)、LogFileReader
モジュールで混在し、名前がパターン_<Name>Whatever
_に一致するすべてのクラスは、_:name
_ハンドラーに登録されます(BzrFrobnicator
を参照)。LogFileReader(:name)
モジュールで混在するすべてのクラスは、名前に関係なく、_:name
_ハンドラーに登録されます(NameThatDoesntFitThePattern
を参照)。これは非常に不自然なデモにすぎないことに注意してください。たとえば、これは間違いなくnotスレッドセーフです。また、メモリリークの可能性もあります。注意して使用してください!
ブライアンキャンベルの回答に対するもう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
登録する必要はありません
クラス名を登録する必要なく、これも機能するはずです
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