Подтвердить что ты не робот

Дизайн шаблона Ruby: как сделать расширяемый класс factory?

Хорошо, предположим, что у меня есть программа Ruby для чтения файлов журнала управления версиями и что-то делать с данными. (Я не знаю, но ситуация аналогична, и я получаю удовольствие от этих аналогов). Предположим, что прямо сейчас я хочу поддержать Bazaar и Git. Предположим, что программа будет выполнена с каким-то аргументом, указывающим, какое программное обеспечение для управления версиями используется.

Учитывая это, я хочу создать LogFileReaderFactory, который, учитывая имя программы управления версиями, вернет подходящий читатель файла журнала (подкласса из общего), чтобы прочитать файл журнала и выплюнуть каноническое внутреннее представление. Поэтому, конечно, я могу сделать BazaarLogFileReader и GitLogFileReader и жестко закодировать их в программе, но я хочу, чтобы он был настроен таким образом, что добавление поддержки для новой программы управления версиями так же просто, как переполнение нового файла класса в каталоге с Bazaar и читателями Git.

Итак, прямо сейчас вы можете вызывать "делать-что-то-с-журналом" - программное обеспечение git "и" делать что-то-с-журналом - программный базар ", потому что есть журналы для тех, кто, Я хочу, чтобы было возможно просто добавить класс и файл SVNLogFileReader в один и тот же каталог и автоматически иметь возможность вызывать" do-something-with-the-log -software svn" без каких-либо изменений в остальной части программа. (Конечно, файлы могут быть названы с определенным шаблоном и globbed в требовании вызова.)

Я знаю, что это можно сделать в Ruby... Я просто не понимаю, как это сделать... или если я вообще это сделаю.

4b9b3361

Ответ 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

Как вы можете видеть, суперкласс может действовать как собственный factory. Теперь, как насчет автоматической регистрации? Итак, почему бы нам просто не сохранить хэш наших зарегистрированных подклассов и зарегистрировать каждый, когда мы их определяем:

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 Poignant Guide to Ruby, особенно главы 5 и 6, хотя только если вы можете иметь дело с сюрреалистическое техническое письмо.

edit: сейчас нужно выполнить риффу от Йорга; Мне нравится сокращать повторение, и поэтому не повторяю имени системы контроля версий как в классе, так и в регистрации. Добавление следующего к моему второму примеру позволит вам писать гораздо более простые определения классов, хотя они все еще довольно просты и понятны.

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

Ответ 2

Это действительно просто отгоняет решение Брайана Кэмпбелла. Если вам это нравится, пожалуйста, подтвердите свой ответ тоже: он сделал всю работу.

#!/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 и других фреймворках. Поскольку такие вещи, как include или extend, на самом деле не ключевые слова, а обычные методы, которые принимают обычные параметры, вам не нужно передавать им фактический Module в качестве аргумента, вы также можете передавать все, что оценивается 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. И мы фактически не определяем метод на самом модуле, а на модуле eigenclass (с помощью небольшого вспомогательного метода, который мы определили выше), что означает, что метод не станет методом экземпляра, а скорее "статическим" методом (в терминах Java/.NET).

included на самом деле является специальным методом hook, который вызывается средой Ruby, каждый раз, когда модуль включается в класс, и класс передается в качестве аргумента. Итак, наш недавно созданный модуль теперь имеет метод hook, который будет информировать его всякий раз, когда он куда-то включается.

      LogFileReader[type] = klass

И это то, что делает наш метод hook: он регистрирует класс, который передается методу hook в реестр 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 s:

  • Все классы, имя которых соответствует шаблону <Name>LogFileReader, будут автоматически найдены и зарегистрированы как LogFileReader для :name (см.: GitLogFileReader),
  • Все классы, которые смешиваются в модуле LogFileReader и чье имя совпадает с шаблоном <Name>Whatever, будут зарегистрированы для обработчика :name (см. BzrFrobnicator) и
  • Все классы, которые смешиваются в модуле LogFileReader(:name), будут зарегистрированы для обработчика :name, независимо от их имени (см. NameThatDoesntFitThePattern).

Обратите внимание, что это всего лишь очень надуманная демонстрация. Это, например, определенно не является потокобезопасным. Это может также привести к утечке памяти. Используйте с осторожностью!

Ответ 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