【问题标题】:Ruby design pattern: How to make an extensible factory class?Ruby 设计模式:如何制作可扩展的工厂类?
【发布时间】:2009-04-14 03:12:48
【问题描述】:

好的,假设我有 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 中完成……我只是不知道我应该怎么做……或者我是否应该这样做。

【问题讨论】:

    标签: ruby design-patterns


    【解决方案1】:

    您不需要 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
    

    【讨论】:

    • 只有两个快速建议:您似乎遇到了 Java-flu 的情况,在 Ruby 中,异常是用“raise”而不是“throw”引发的。 ('throw' 用于通用轻量级非本地控制流)。其次,case 语句,尤其是 switch 变量命名为“type”的语句,是一个很好的 ...
    • ... 向多态性重构的指标。否则,非常酷。太酷了,事实上,我只是不得不玩弄它:-)
    • 你可以用反射替换 case,就像我做的那样 (const_get("#{type.to_s.capitalize}LogFileReader").new),但你是对的:这可能是矫枉过正。跨度>
    • 作为一名 Ruby Newby,我真的希望这个示例的“最佳”版本出现在一个地方。我无法弄清楚如何处理 @@subclasses 数组——它应该在最终版本中吗?
    • 关于这个例子的吹毛求疵:类变量(@@)在 Ruby 中有令人惊讶的行为,应该避免。改用一个类实例变量——也就是说,在类上下文中使用一个@。您将获得一个范围为类本身的变量,但其行为方式不那么奇怪。 (在这种情况下,@subclasses 上的一个 @ 可以完美运行。)
    【解决方案2】:

    这实际上只是在模仿 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 中是合法的。歧义是这样解决的:该模块将始终是首选,除非它显然是一个方法调用,即您要么将括号放在末尾 (Foo()) 要么传递一个参数 (Foo :bar)。

    这是一个在 stdlib 中的一些地方使用的技巧,也用于 Camping 和其他框架。因为像 includeextend 这样的东西实际上不是关键字,而是接受普通参数的普通方法,你不必将实际的 Module 作为参数传递给它们,你也可以传递任何 评估为Module。事实上,这甚至适用于继承,写class Foo &lt; some_method_that_returns_a_class(:some, :params)是完全合法的。

    使用这个技巧,您可以让它看起来像是继承自一个泛型类,即使 Ruby 没有泛型。例如,它在委托库中使用,您可以在其中执行class MyFoo &lt; SimpleDelegator(Foo) 之类的操作,然后发生的情况是SimpleDelegator 方法 动态创建并返回SimpleDelegator 的匿名子类class,它将所有方法调用委托给Foo 类的实例。

    我们在这里使用了类似的技巧:我们将动态创建一个Module,当它混合到一个类中时,将自动将该类注册到LogFileReader 注册表中。

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

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

    现在,转到const_set。这是一种设置常数的方法。所以,就像说FOO = :bar,except一样,我们可以将常量的名称作为参数传入,而不必事先知道。由于我们在 LogFileReader 模块上调用该方法,因此常量将在该命名空间内定义,IOW 将命名为 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注册表中。它注册它的关键是上面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)
    

    这个新的扩展版本允许定义LogFileReaders 的三种不同方式:

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

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

    【讨论】:

    • 好吧,这其中的一些肯定是超出了我的想象。 “def LogFileReader 类型”是怎么回事?
    【解决方案3】:

    对布赖恩·坎贝尔的回答还有一个小建议 -

    实际上,您可以使用继承的回调自动注册子类。即

    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
    

    无需注册

    【讨论】:

      【解决方案4】:

      这也应该可以,不需要注册类名

      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
      

      【讨论】:

      • 在这个页面上的所有代码中,这个对于像我这样的 Ruby 菜鸟来说是最容易获得的。我也喜欢 self.create 中没有存储类型名称,感觉更像 Rails。 +1
      • 这个方法有问题;您仍然需要在调用代码中事先了解类名。这个 create 方法非常通用,它会很高兴地返回一个 any 类型的新类,所以像 LogFileReader.create(:string) 这样的东西会返回一个新的空字符串!使用类型注册表的优点是用于查找读取器类的符号 :git 可用于查找其他内容。例如,对数旋转器。
      • 另外,出于安全考虑,重要的是不允许用户创建任何东西...如果您使用的是 Rails,name.to_s.split('_').collect!{ |w| w.capitalize }.join 可以替换为 name.camelcase
      【解决方案5】:

      这就是我制作可扩展工厂类的方式。

      module Factory
        class Error < RuntimeError
        end
      
        class Base
          @@registry = {}
      
          class << self
            def inherited(klass)
              type = klass.name.downcase.to_sym
              @@registry[type] = klass
            end
      
            def create(type, *args, **kwargs)
              klass = @@registry[type]
              return klass.new(*args, **kwargs) if klass
              raise Factory::Error.new "#{type} is unknown"
            end
          end
        end
      end
      
      class Animal < Factory::Base
        attr_accessor :name
      
        def initialize(name)
          @name = name
        end
      
        def walk?
          raise NotImplementedError
        end
      end
      
      class Cat < Animal
        def walk?; true; end
      end
      
      class Fish < Animal
        def walk?; false; end
      end
      
      class Salmon < Fish
      end
      
      duck = Animal.create(:cat, "Garfield")
      salmon = Animal.create(:salmon, "Alfredo")
      pixou = Animal.create(:duck, "Pixou") # duck is unknown (Factory::Error)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-12-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多