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 ,当它被混合到一个类中时,它将自动地将该类注册到LogFileReaderregistry中。

  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注册到LogFileReaderregistry中。 而它注册它的关键是从上面的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的三种不同的方式:

  1. 所有名称与模式<Name>LogFileReader相匹配的类将自动被find并作为LogFileReader注册为:name (参见: GitLogFileReader ),
  2. 所有在LogFileReader模块中混合使用的名称匹配模式<Name>Whatever将被注册为:name处理程序(参见: BzrFrobnicator )和
  3. 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