好吧,假设我有Ruby程序来读取版本控制日志文件并对数据执行某些操作。 (我没有,但情况类似,我对这些类比很开心)。我们现在假设我想支持Bazaar和Git。假设程序将使用某种参数来执行,该参数指示正在使用哪个版本控制软件。
鉴于此,我想创建一个LogFileReaderFactory,它给出版本控制程序的名称将返回一个适当的日志文件读取器(从泛型中继承)来读取日志文件并吐出规范的内部表示。所以,当然,我可以制作BazaarLogFileReader和GitLogFileReader并将它们硬编码到程序中,但是我希望它能够以这样的方式设置:添加对新版本控制程序的支持就像填充新的类文件一样简单在Bazaar和Git读者的目录中。
所以,现在你可以调用“do-something-with-log -software git”和“do-something-with-the-log --software bazaar”,因为有这些日志阅读器。我想要的是可以简单地将一个SVNLogFileReader类和文件添加到同一目录,并自动调用“do-something-with-the-log --software svn”,而不会对其余部分进行任何更改。程序。 (这些文件当然可以用特定模式命名,并在require调用中进行全局化。)
我知道这可以在Ruby中完成......我不知道应该怎么做......或者我是否应该这样做。
答案 0 :(得分:94)
您不需要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的Design Patterns in Dynamic Languages。他演示了有多少设计模式实际上正在解决编程语言中的限制或不足之处;并且使用足够强大和灵活的语言,您不需要设计模式,只需实现您想要的功能。他使用Dylan和Common Lisp作为例子,但他的许多观点也与Ruby有关。
您可能还想看一下Why's Poignant Guide to 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
答案 1 :(得分:18)
这真的只是让Brian Campbell的解决方案匆匆而过。如果你喜欢这个,请 upvote 他的答案:他做了所有的工作。
#!/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
)。 / p>
这是一个技巧,用于stdlib中的一些地方,也用于Camping和其他框架。因为include
或extend
之类的东西实际上不是关键字,而是普通参数的普通方法,所以你不必将实际Module
作为参数传递给它们,你也可以将评估的任何内容传递给Module
。事实上,这甚至适用于继承,写class Foo < some_method_that_returns_a_class(:some, :params)
是完全合法的。
使用这个技巧,你可以让它看起来像是继承自泛型类,即使Ruby没有泛型。例如,它在委托库中用于执行类似class MyFoo < SimpleDelegator(Foo)
的操作,会发生什么,SimpleDelegator
方法动态创建并返回{的匿名子类{1}} class ,它将所有方法调用委托给SimpleDelegator
类的实例。
我们在这里使用类似的技巧:我们将动态创建一个Foo
,当它混合到一个类中时,会自动使用Module
注册表注册该类。
LogFileReader
这一行有很多事情要做。让我们从右边开始: LogFileReader.const_set type.to_s.capitalize, Module.new {
创建一个新的匿名模块。传递给它的块成为模块的主体 - 它与使用Module.new
关键字基本相同。
现在,转到module
。这是一种设定常数的方法。所以,它与说const_set
,除了之外的一样,我们可以将常量的名称作为参数传递,而不必事先知道它。由于我们在FOO = :bar
模块上调用方法,因此常量将在该命名空间内定义,IOW将命名为LogFileReader
。
那么,是什么常量的名称?好吧,这是传递给方法的LogFileReader::Something
参数,大写。因此,当我传入type
时,结果常量将为:cvs
。
我们将常数设置为什么?到我们新创建的匿名模块,现在不再是匿名模块了!
除了我们事先并不知道“Cvs”部分之外,所有这一切都只是一种说法LogFileParser::Cvs
的长篇大论,因此无法以这种方式编写。
module LogFileReader::Cvs
这是我们模块的主体。在这里,我们使用 eigenclass.send :define_method, :included do |klass|
动态定义名为define_method
的方法。我们实际上并没有在模块本身上定义方法,而是在模块的 eigenclass 上(通过我们上面定义的小辅助方法),这意味着该方法不会成为实例方法,而是一个“静态”方法(用Java / .NET术语)。
included
实际上是一个特殊的钩子方法,每次将一个模块包含在一个类中时,它都会被Ruby运行时调用,并且该类作为参数传入。因此,我们新创建的模块现在有一个钩子方法,只要它被包含在某个地方就会通知它。
included
这就是我们的钩子方法所做的:它将传递给钩子方法的类注册到 LogFileReader[type] = klass
注册表中。它注册它的关键是来自上面LogFileReader
方法的type
参数,由于闭包的魔力,它实际上可以在LogFileReader
方法中访问。 / p>
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
的{{1}}(请参阅:LogFileReader
),:name
模块中混合且名称与模式GitLogFileReader
匹配的所有类都将为LogFileReader
处理程序注册(请参阅:<Name>Whatever
)和:name
模块中混合的所有类都将为BzrFrobnicator
处理程序注册,无论其名称如何(请参阅:LogFileReader(:name)
)。请注意,这只是一个非常人为的演示。例如,肯定不线程安全。它也可能泄漏内存。请谨慎使用!
答案 2 :(得分:10)
对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
无需注册
答案 3 :(得分:7)
这也应该有效,无需注册类名
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