好吧,假设我有Ruby程序来读取版本控制日志文件并对数据执行某些操作.(我没有,但情况类似,我对这些类比很开心).我们现在假设我想支持Bazaar和Git.假设程序将以某种参数执行,指示正在使用哪个版本控制软件.
鉴于此,我想创建一个LogFileReaderFactory,它给出版本控制程序的名称将返回一个适当的日志文件读取器(从泛型中继承)以读取日志文件并吐出规范的内部表示.所以,当然,我可以制作BazaarLogFileReader和GitLogFileReader并将它们硬编码到程序中,但是我希望它能够以这样的方式设置:添加对新版本控制程序的支持就像填充新的类文件一样简单在Bazaar和Git阅读器的目录中.
所以,现在你可以调用"do-something-with-the-log --software git"和"do-something-with-the-log --software 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章,尽管只有你可以处理超现实主义的技术写作.
编辑:现在关闭Jörg的答案; 我喜欢减少重复,所以不要在类和注册中重复版本控制系统的名称.将以下内容添加到我的第二个示例中将允许您编写更简单的类定义,同时仍然非常简单易懂.
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
这真的只是对Brian Campbell的解决方案的抨击.如果你喜欢这个,请给予好评他的回答,太:他做了所有的工作.
#!/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中是合法的.模糊性就像这样解决:模块总是首选,除非它显然是一个方法调用,即你要么将括号放在end(Foo()
)或传递一个参数(Foo :bar
).
这是stdlib中的一些地方使用的技巧,也是在Camping和其他框架中使用的技巧.因为事实上include
或者extend
实际上不是关键字,而是采用普通参数的普通方法,你不必将它们Module
作为参数传递给它们,你也可以传递任何评估为a的东西Module
.事实上,这甚至适用于继承,写入是完全合法的class Foo < some_method_that_returns_a_class(:some, :params)
.
有了这个技巧,你可以让它看起来像是继承自泛型类,即使Ruby没有泛型.例如,它在委托库中用于执行类似的操作class MyFoo < SimpleDelegator(Foo)
,并且会发生什么,该SimpleDelegator
方法动态创建并返回类的匿名子类,SimpleDelegator
该类将所有方法调用委托给Foo
类的实例.
我们在这里使用类似的技巧:我们将动态创建一个Module
,当它混合到一个类中时,将自动注册该类与LogFileReader
注册表.
LogFileReader.const_set type.to_s.capitalize, Module.new {
这一行有很多事情要做.让我们从右边开始:Module.new
创建一个新的匿名模块.传递给它的块成为模块的主体 - 它与使用module
关键字基本相同.
现在,来吧const_set
.这是一种设定常数的方法.所以,它与说法相同FOO = :bar
,只是我们可以将常量的名称作为参数传递,而不必事先知道它.因为我们在LogFileReader
模块上调用方法,所以常量将在该命名空间内定义,IOW它将被命名LogFileReader::Something
.
那么,什么是常量的名字吗?好吧,这是type
传递给方法的参数,大写.因此,当我传入时:cvs
,结果常量将是LogFileParser::Cvs
.
我们将常数设置为什么?到我们新创建的匿名模块,现在不再是匿名的!
所有这一切都只是一种长篇大论的说法module LogFileReader::Cvs
,除了我们事先并不知道"Cvs"部分,因此不能这样写.
eigenclass.send :define_method, :included do |klass|
这是我们模块的主体.在这里,我们使用define_method
动态定义一个名为的方法included
.我们实际上并没有在模块本身上定义方法,而是在模块的本征类上(通过我们上面定义的小辅助方法),这意味着该方法不会成为实例方法,而是"静态"方法(用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
:
名称与模式匹配的所有类
将自动被找到并注册为LogFileReader
for :name
(参见:) GitLogFileReader
,
混合在LogFileReader
模块中且名称与模式匹配的所有类
都将为:name
处理程序注册(请参阅:) BzrFrobnicator
和
LogFileReader(:name)
模块中混合的所有类都将为:name
处理程序注册,无论其名称如何(请参阅:) NameThatDoesntFitThePattern
.
请注意,这只是一个非常人为的演示.例如,它绝对不是线程安全的.它也可能泄漏内存.谨慎使用!
Brian Cambell回答的另一个小建议 -
您实际上可以使用继承的回调自动注册子类.即
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