Шаблон проектирования Ruby: как создать расширяемый фабричный класс? - PullRequest
50 голосов
/ 14 апреля 2009

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

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

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

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

Ответы [ 4 ]

94 голосов
/ 14 апреля 2009

Вам не нужен 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

И вот оно у тебя. Просто разбейте это на несколько файлов и потребуйте их соответствующим образом.

Вам следует прочитать Шаблоны проектирования Питера Норвиг на динамических языках , если вы заинтересованы в подобных вещах. Он демонстрирует, сколько шаблонов дизайна фактически работают вокруг ограничений или недостатков в вашем языке программирования; и с достаточно мощным и гибким языком вам не нужен шаблон проектирования, вы просто реализуете то, что хотите сделать. Он использует Дилана и Common Lisp в качестве примеров, но многие из его пунктов также актуальны и для Ruby.

Возможно, вы также захотите взглянуть на Почему Poignant Guide to 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
18 голосов
/ 14 апреля 2009

Это на самом деле просто срывает решение Брайана Кэмпбелла. Если вам это нравится, пожалуйста 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, а также в 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, константа будет определена внутри этого пространства имен, поэтому она будет называться 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

И вот что делает наш метод 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:

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

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

10 голосов
/ 24 сентября 2011

Еще одно незначительное предложение для ответа Брайана Кэмбелла -

В действительности вы можете автоматически регистрировать подклассы с помощью унаследованного обратного вызова. * Т.е. 1003 *

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

Без необходимости регистрировать

7 голосов
/ 10 ноября 2011

Это тоже должно работать, без необходимости регистрации имен классов

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...