Ruby设计模式:如何制作可扩展的工厂类?

好的,假设我有Ruby程序来读取版本控制日志文件并对数据进行某些处理。 (我没有,但是情况是类似的,我很喜欢这些类比)。 假设现在我想支持Bazaar和Git。 让我们假设该程序将以某种参数执行,该参数指示正在使用的版本控制软件。

鉴于此,我想制作一个LogFileReaderFactory,它给出一个版本控制程序的名称,它将返回一个适当的日志文件阅读器(从通用类继承),以读取日志文件并吐出规范的内部表示形式。 因此,当然,我可以制作BazaarLogFileReader和GitLogFileReader并将其硬编码到程序中,但是我希望将其设置为添加对新版本控制程序的支持就像放入一个新的类文件一样简单。 在Bazaar和Git阅读器的目录中。

因此,现在您可以称为“用日志执行某事-软件git”和“用日志进行某事-软件义卖市场”,因为这些工具都有日志读取器。 我想要的是可以简单地将SVNLogFileReader类和文件添加到同一目录中,并自动调用“ do-some-the-the-log --software svn”,而无需对其余部分进行任何更改 程序。 (当然,可以使用特定的模式来命名文件,并在require调用中使用它们。)

我知道这可以在Ruby中完成...我只是不怎么做...或者根本不应该这样做。

Instance Hunter asked 2020-01-23T12:33:30Z
4个解决方案
95 votes

您不需要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章,尽管前提是您必须处理超现实主义的技术写作。

编辑。 我确实喜欢减少重复,所以不要在类和注册中都重复版本控制系统的名称。 在我的第二个示例中添加以下内容将使您可以编写更简单的类定义,同时仍然非常简单易懂。

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 answered 2020-01-23T12:34:06Z
18 votes

这实际上只是在简化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(:name)的全局方法(实际上更像一个过程),它与模块:name相同。在Ruby中这是合法的。 可以通过以下方式解决歧义:模块将始终是首选模块,除非它显然是方法调用,即您可以在括号的末尾(NameThatDoesntFitThePattern)或传递参数(BzrFrobnicator)。

这是一个技巧,在stdlib中的某些地方以及Camping和其他框架中都使用过。 因为诸如LogFileReader(:name):name之类的东西实际上并不是关键字,而是采用普通参数的普通方法,所以您不必将实际的5315311234450195458传递给它们作为参数,因此,您也可以传递任何计算结果为BzrFrobnicator的值。 即使可以继承,编写class Foo < some_method_that_returns_a_class(:some, :params)也完全合法。

使用此技巧,即使Ruby没有泛型,您也可以使其看起来像是从泛型类继承的。 例如,它在委托库中使用,您在其中执行了LogFileReader(:name)之类的操作,结果是:name方法动态创建并返回了NameThatDoesntFitThePattern类的匿名子类,该子类将所有方法调用委托给BzrFrobnicator类的实例。

我们在此处使用类似的技巧:我们将动态创建一个LogFileReader(:name),将其混合到一个类中后,将自动在:name注册表中注册该类。

  LogFileReader.const_set type.to_s.capitalize, Module.new {

仅此行发生了很多事情。 让我们从右边开始:LogFileReader(:name)创建一个新的匿名模块。 传递给它的块成为模块的主体–与使用:name关键字基本相同。

现在,转到LogFileReader(:name)。这是一种设置常量的方法。 因此,这与说:name相同,只是我们可以将常量的名称作为参数传递,而不必事先知道。 由于我们在NameThatDoesntFitThePattern模块上调用该方法,因此将在该名称空间(IOW)中定义该常量,并将其命名为BzrFrobnicator

那么,该常数的名称是什么? 好吧,这是将LogFileReader(:name)参数传递给该方法的首字母大写。 因此,当我传递:name时,所得常数将为NameThatDoesntFitThePattern

我们将常数设置为什么? 到我们新创建的匿名模块,该模块现在不再是匿名的了!

所有这些实际上只是一个漫不经心的说法,即LogFileReader(:name),只是我们事先不知道“ Cvs”部分,因此无法以这种方式编写。

    eigenclass.send :define_method, :included do |klass|

这是我们模块的主体。 在这里,我们使用LogFileReader(:name)动态定义一个称为:name的方法。实际上,我们并未在模块本身上定义该方法,而是在模块的本征类上定义了(通过上面定义的一个小辅助方法),这意味着该方法将 不是成为实例方法,而是成为“静态”方法(以Java / .NET术语)。

LogFileReader(:name)实际上是一个特殊的钩子方法,每次模块被包含到类中并且将该类作为参数传递时,Ruby运行时就会调用该方法。 因此,我们新创建的模块现在具有一个hook方法,只要将它包含在某处,它就会通知它。

      LogFileReader[type] = klass

这就是我们的hook方法的作用:它将传递给hook方法的类注册到LogFileReader(:name)注册表中。 它注册的密钥是上面NameThatDoesntFitThePattern方法中的:name参数,由于闭包的神奇之处,实际上可以在BzrFrobnicator方法内部访问它。

    end
    include LogFileReader

最后但并非最不重要的一点,我们在匿名模块中包括LogFileReader(:name)模块。 [注意:我在原始示例中忘记了这一行。]

  }
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)s:

  1. 将自动找到名称与模式LogFileReader(:name)匹配的所有类,并将其注册为NameThatDoesntFitThePattern:name(请参见:BzrFrobnicator),
  2. 所有在LogFileReader(:name)模块中混合且名称与模式:name匹配的类都将为NameThatDoesntFitThePattern处理程序注册(请参阅:BzrFrobnicator),并且
  3. 混用到LogFileReader(:name)模块中的所有类,无论其名称如何,都将为:name处理程序注册(请参阅:NameThatDoesntFitThePattern)。

请注意,这只是一个非常人为的演示。 例如,它绝对不是线程安全的。 它还可能会泄漏内存。 请谨慎使用!

Jörg W Mittag answered 2020-01-23T12:35:53Z
10 votes

对于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

无需注册

dlangevin answered 2020-01-23T12:36:26Z
7 votes

这也应该工作,不需要注册类名

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
Robert Wahler answered 2020-01-23T12:36:50Z
translate from https://stackoverflow.com:/questions/746207/ruby-design-pattern-how-to-make-an-extensible-factory-class