红宝石设计模式。如何制作一个可扩展的工厂类?[英] Ruby design pattern: How to make an extensible factory class?

本文是小编为大家收集整理的关于红宝石设计模式。如何制作一个可扩展的工厂类?的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

好吧,假设我有Ruby程序可以读取版本控制日志文件并使用数据做点事. (我没有,但是情况类似,我对这些类比很开心).现在假设我想支持集市和吉特.让我们假设该程序将通过某种参数执行,指示正在使用哪个版本控制软件.

给出了这个,我想制作一个logFileReaderFactory,给定版本控制程序的名称将返回适当的日志文件读取器(从通用的子分类)读取日志文件并吐出规范的内部表示.因此,当然,我可以使BazaarlogFilereDer和GitlogFilereDere并将它们硬编码到程序中,但是我希望以这样的方式进行设置,以至于添加对新版本控制程序的支持就像插入新的类文件一样简单在带有集市和git读者的目录中.

因此,现在您可以称呼" do something-the-the-the-the-offerware git"和" do something with-the-the-log-software Bazaar",因为有这些日志读取器.我想要的是,可以简单地将svnlogfilereader类添加到同一目录中,然后自动称呼" do do something-with-the-the-the-the-the-oftware svn",而没有任何更改的任何更改程序. (当然,文件可以用特定模式命名并在"要求呼叫"中被盖上.)

我知道这可以在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)和普通LISP示例,但他的许多观点也与Ruby有关.

您可能还想看一下为什么Ruby的凄美指南,尤其是第5和6章,第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

其他推荐答案

这实际上只是在布莱恩·坎贝尔的解决方案中即将来临.如果您喜欢这样,请 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).

.

这是一个技巧,用于stdlib的几个地方以及露营和其他框架.因为include或extend之类的东西实际上不是关键字,而是采用普通参数的普通方法,因此您不必将它们传递给实际Module作为参数,因此您也可以传递任何评估对Module.实际上,这甚至可以为继承作用,写class Foo < some_method_that_returns_a_class(:some, :params).

是完全合法的.

有了这个技巧,即使Ruby没有通用,您也可以使它看起来像从通用类中继承.例如,它在代表团库中使用,在其中您可以执行class MyFoo < SimpleDelegator(Foo)之类的事情,而发生的事情是, 方法动态创建并返回 class ,将所有方法委派给Foo class.

的实例.

我们在这里使用类似的技巧:我们将动态创建Module,当它混合成类时,它将自动注册该类,并向LogFileReader registry注册.

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

在这条线上有很多事情要做.让我们从右边开始:Module.new创建一个新的匿名模块.传递给它的块变成了模块的主体 - 基本上与使用module关键字.

现在,到const_set.这是设置常数的方法.因此,与说FOO = :bar,相同,除了我们可以以常数为参数的名称,而不必事先知道它.由于我们在LogFileReader模块上调用方法,因此将在该名称空间内定义常数,它将被命名为LogFileReader::Something.

.

那么,是常数的名称?好吧,这是type参数传递到该方法中,大写.因此,当我传递:cvs时,结果常数将为LogFileParser::Cvs.

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

所有这些实际上只是说module LogFileReader::Cvs的一种漫长的方式,除了我们不知道" CVS"部分,因此不能那样写.

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

这是我们模块的主体.在这里,我们使用define_method动态定义一种称为included的方法.而且我们实际上并未在模块本身上定义方法,而是在模块的 eigenclass 上(通过我们上面定义的小型助手方法),这意味着该方法不会成为实例方法,而是一种"静态"方法(用java/.net术语).

included实际上是一种特殊的挂钩方法,它被Ruby运行时调用,每次将模块包含在类中,并且该类作为参数传递.因此,我们新创建的模块现在具有挂钩方法,每当它包含在某个地方时,都会通知它.

      LogFileReader[type] = klass

这就是我们的钩方法的作用:它将被传递到挂钩方法的类中注册到LogFileReader注册表中.并且它将其记录到下面的关键是上面的type参数,而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 s:

的三种不同的方法
  1. 所有名称匹配模式<Name>LogFileReader的类都将自动找到并注册为:name的:name(见:GitLogFileReader),
  2. 所有混合在LogFileReader模块中的类,其名称匹配模式<Name>Whatever的所有类将为:name处理程序注册(请参阅:BzrFrobnicator)和
  3. 在LogFileReader(:name)模块中混合的所有类,无论其名称如何

请注意,这只是一个非常人为的演示.例如,它绝对是不是螺纹安全.它也可能泄漏内存.谨慎使用!

其他推荐答案

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

无需注册

本文地址:https://www.itbaoku.cn/post/627714.html

问题描述

Ok, suppose I have Ruby program to read version control log files and do something with the data. (I don't, but the situation is analogous, and I have fun with these analogies). Let's suppose right now I want to support Bazaar and Git. Let's suppose the program will be executed with some kind of argument indicating which version control software is being used.

Given this, I want to make a LogFileReaderFactory which given the name of a version control program will return an appropriate log file reader (subclassed from a generic) to read the log file and spit out a canonical internal representation. So, of course, I can make BazaarLogFileReader and GitLogFileReader and hard-code them into the program, but I want it to be set up in such a way that adding support for a new version control program is as simple as plopping a new class file in the directory with the Bazaar and Git readers.

So, right now you can call "do-something-with-the-log --software git" and "do-something-with-the-log --software bazaar" because there are log readers for those. What I want is for it to be possible to simply add a SVNLogFileReader class and file to the same directory and automatically be able to call "do-something-with-the-log --software svn" without ANY changes to the rest of the program. (The files can of course be named with a specific pattern and globbed in the require call.)

I know this can be done in Ruby... I just don't how I should do it... or if I should do it at all.

推荐答案

You don't need a LogFileReaderFactory; just teach your LogFileReader class how to instantiate its subclasses:

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

As you can see, the superclass can act as its own factory. Now, how about automatic registration? Well, why don't we just keep a hash of our registered subclasses, and register each one when we define them:

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

And there you have it. Just split that up into a few files, and require them appropriately.

You should read Peter Norvig's Design Patterns in Dynamic Languages if you're interested in this sort of thing. He demonstrates how many design patterns are actually working around restrictions or inadequacies in your programming language; and with a sufficiently powerful and flexible language, you don't really need a design pattern, you just implement what you want to do. He uses Dylan and Common Lisp for examples, but many of his points are relevant to Ruby as well.

You might also want to take a look at Why's Poignant Guide to Ruby, particularly chapters 5 and 6, though only if you can deal with surrealist technical writing.

edit: Riffing of off Jörg's answer now; I do like reducing repetition, and so not repeating the name of the version control system in both the class and the registration. Adding the following to my second example will allow you to write much simpler class definitions while still being pretty simple and easy to understand.

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

Of course, in production code, you may want to actually name those classes, by generating a constant definition based on the name passed in, for better error messages.

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

其他推荐答案

This is really just riffing off Brian Campbell's solution. If you like this, please upvote his answer, too: he did all the work.

#!/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

Here, we create a global method (more like a procedure, actually) called LogFileReader, which is the same name as our module LogFileReader. This is legal in Ruby. The ambiguity is resolved like this: the module will always be preferred, except when it's obviously a method call, i.e. you either put parentheses at the end (Foo()) or pass an argument (Foo :bar).

This is a trick that is used in a few places in the stdlib, and also in Camping and other frameworks. Because things like include or extend aren't actually keywords, but ordinary methods that take ordinary parameters, you don't have to pass them an actual Module as an argument, you can also pass anything that evaluates to a Module. In fact, this even works for inheritance, it is perfectly legal to write class Foo < some_method_that_returns_a_class(:some, :params).

With this trick, you can make it look like you are inheriting from a generic class, even though Ruby doesn't have generics. It's used for example in the delegation library, where you do something like class MyFoo < SimpleDelegator(Foo), and what happens, is that the SimpleDelegator method dynamically creates and returns an anonymous subclass of the SimpleDelegator class, which delegates all method calls to an instance of the Foo class.

We use a similar trick here: we are going to dynamically create a Module, which, when it is mixed into a class, will automatically register that class with the LogFileReader registry.

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

There's a lot going on in just this line. Let's start from the right: Module.new creates a new anonymous module. The block passed to it, becomes the body of the module – it's basically the same as using the module keyword.

Now, on to const_set. It's a method for setting a constant. So, it's the same as saying FOO = :bar, except that we can pass in the name of the constant as a parameter, instead of having to know it in advance. Since we are calling the method on the LogFileReader module, the constant will be defined inside that namespace, IOW it will be named LogFileReader::Something.

So, what is the name of the constant? Well, it's the type argument passed into the method, capitalized. So, when I pass in :cvs, the resulting constant will be LogFileParser::Cvs.

And what do we set the constant to? To our newly created anonymous module, which is now no longer anonymous!

All of this is really just a longwinded way of saying module LogFileReader::Cvs, except that we didn't know the "Cvs" part in advance, and thus couldn't have written it that way.

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

This is the body of our module. Here, we use define_method to dynamically define a method called included. And we don't actually define the method on the module itself, but on the module's eigenclass (via a small helper method that we defined above), which means that the method will not become an instance method, but rather a "static" method (in Java/.NET terms).

included is actually a special hook method, that gets called by the Ruby runtime, everytime a module gets included into a class, and the class gets passed in as an argument. So, our newly created module now has a hook method that will inform it whenever it gets included somewhere.

      LogFileReader[type] = klass

And this is what our hook method does: it registers the class that gets passed into the hook method into the LogFileReader registry. And the key that it registers it under, is the type argument from the LogFileReader method way above, which, thanks to the magic of closures, is actually accessible inside the included method.

    end
    include LogFileReader

And last but not least, we include the LogFileReader module in the anonymous module. [Note: I forgot this line in the original example.]

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

This new expanded version allows three different ways of defining LogFileReaders:

  1. All classes whose name matches the pattern <Name>LogFileReader will automatically be found and registered as a LogFileReader for :name (see: GitLogFileReader),
  2. All classes that mix in the LogFileReader module and whose name matches the pattern <Name>Whatever will be registered for the :name handler (see: BzrFrobnicator) and
  3. All classes that mix in the LogFileReader(:name) module, will be registered for the :name handler, regardless of their name (see: NameThatDoesntFitThePattern).

Please note that this is just a very contrived demonstration. It is, for example, definitely not thread-safe. It might also leak memory. Use with caution!

其他推荐答案

One more minor suggestion for Brian Cambell's answer -

In you can actually auto-register the subclasses with an inherited callback. I.e.

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

Now we have

class SvnLogFileReader < LogFileReader
  def display
    # do stuff here
  end
end

With no need to register it