Rubydevise模式:如何创build一个可扩展的工厂类?
好吧,假设我有Ruby程序来读取版本控制日志文件,并对数据做些什么。 (我不知道,但情况是类似的,我对这些类比感到很开心)。 我们现在假设我想支持Bazaar和Git。 假设程序将会执行某种参数,指出正在使用哪个版本控制软件。
鉴于此,我想创build一个LogFileReaderFactory,给定版本控制程序的名称将返回一个适当的日志文件读取器(从通用的子类)读取日志文件,并吐出一个规范的内部表示。 所以,当然,我可以制作BazaarLogFileReader和GitLogFileReader,并将它们硬编码到程序中,但是我希望这样做可以增加对新版本控制程序的支持,就像添加一个新的类文件一样简单在与Bazaar和Git读者的目录中。
所以,现在你可以调用“do-something-with-the-log -software git”和“do-something-with-the-log-software bazaar”,因为那里有日志读取器。 我想要的是可以简单地将SVNLogFileReader类和文件添加到同一个目录,并自动调用“do-something-with-the-logs -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的dynamic语言devise模式 。 他演示了有多lessdevise模式实际上是围绕编程语言中的限制或不足进行的; 并且具有足够强大和灵活的语言,你并不需要devise模式,只需要实现你想要做的事情。 他使用Dylan和Common Lisp作为例子,但是他的许多观点也与Ruby相关。
你可能也想看看为什么Ruby的诗歌指南 ,特别是第5章和第6章,尽pipe只有你能够处理超现实主义的技术写作。
编辑 :现在closuresJörg的答案; 我喜欢减less重复,所以不要在类和注册中重复版本控制系统的名称。 在我的第二个示例中添加以下内容将允许您编写更简单的类定义,同时仍然非常简单易懂。
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
这真是刚刚摆脱了布赖恩·坎贝尔的解决scheme。 如果你喜欢这个, 请 把他的答复也提出来,他做了所有的工作。
#!/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
在这里,我们创build了一个名为LogFileReader
的全局方法(实际上更像是一个过程),它与我们的模块LogFileReader
。 这在Ruby中是合法的。 不明确性是这样解决的:模块将永远是首选的,除非明显是一个方法调用,也就是说,要么在末尾加上括号( Foo()
),要么传递一个参数( Foo :bar
)。
这是一个在stdlib中的一些地方使用的技巧,在露营和其他框架中也是如此。 因为像include
或者extend
这样的东西实际上并不是关键字,而是普通的方法,它们需要一些普通的参数,所以你不必将实际的Module
作为parameter passing给它们,也可以将任何评估结果传递给Module
。 实际上,这甚至可以用于inheritance,编写class Foo < some_method_that_returns_a_class(:some, :params)
是完全合法的。
有了这个技巧,即使Ruby没有generics,你也可以使它看起来像从一个generics类inheritance而来。 例如,在委托库中,您可以在其中执行class MyFoo < SimpleDelegator(Foo)
,并且会发生什么情况是, SimpleDelegator
方法会dynamic创build并返回SimpleDelegator
类的匿名子类, 该类将所有方法调用委托给Foo
类的实例。
我们在这里使用一个类似的技巧:我们将dynamic地创build一个Module
,当它被混合到一个类中时,它将自动地将该类注册到LogFileReader
registry中。
LogFileReader.const_set type.to_s.capitalize, Module.new {
这一行有很多。 我们从右侧开始: Module.new
创build一个新的匿名模块。 传递给它的块成为模块的主体 – 与使用module
关键字基本相同。
现在,到const_set
。 这是一个设置常量的方法。 所以,和FOO = :bar
, 不同的是我们可以传入常量的名字作为参数,而不必事先知道它。 由于我们正在调用LogFileReader
模块上的方法,常量将在该名称空间内定义,IOW将被命名为LogFileReader::Something
。
那么,常数的名字是什么? 那么,这是传递给方法的type
参数,大写。 所以,当我传入:cvs
,结果常量将是LogFileParser::Cvs
。
我们将常数设置为什么? 给我们新build的匿名模块,现在不再是匿名的!
所有这些实际上只是module LogFileReader::Cvs
一个很长的方式,除了我们事先不知道“Cvs”部分,因此不能这样写。
eigenclass.send :define_method, :included do |klass|
这是我们模块的主体。 在这里,我们使用define_method
来dynamic地定义一个名为included
的方法。 我们实际上并没有在模块本身上定义方法,而是在模块的本征类 (通过我们上面定义的一个小的辅助方法)上定义了方法,这意味着该方法不会成为一个实例方法,而是一个“静态”方法(以Java / .NET术语)。
included
的实际上是一个特殊的钩子方法,被Ruby运行时调用,每当一个模块被包含到一个类中,并且该类作为参数被传入。 所以,我们新build立的模块现在有一个钩子方法,只要它被包含在某个地方就会通知它。
LogFileReader[type] = klass
这就是我们的hook方法所做的事情:它将传入钩子方法的LogFileReader
注册到LogFileReader
registry中。 而它注册它的关键是从上面的LogFileReader
方法的type
参数,由于闭包的魔力,它实际上可以在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
的三种不同的方式:
- 所有名称与模式
<Name>LogFileReader
相匹配的类将自动被find并作为LogFileReader
注册为:name
(参见:GitLogFileReader
), - 所有在
LogFileReader
模块中混合使用的名称匹配模式<Name>Whatever
将被注册为:name
处理程序(参见:BzrFrobnicator
)和 - 在
LogFileReader(:name)
模块中混合的所有类将被注册为:name
处理程序,而不pipe它们的名称如何(请参阅:NameThatDoesntFitThePattern
)。
请注意,这只是一个非常人为的示范。 例如,它绝对不是线程安全的。 它也可能会泄漏内存。 谨慎使用!
Brian Cambell的回答还有一个小小的提示 –
在你实际上可以用inheritance的callback自动注册子类。 即
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