Дизайн шаблона Ruby: как сделать расширяемый класс factory?
Хорошо, предположим, что у меня есть программа Ruby для чтения файлов журнала управления версиями и что-то делать с данными. (Я не знаю, но ситуация аналогична, и я получаю удовольствие от этих аналогов). Предположим, что прямо сейчас я хочу поддержать Bazaar и Git. Предположим, что программа будет выполнена с каким-то аргументом, указывающим, какое программное обеспечение для управления версиями используется.
Учитывая это, я хочу создать LogFileReaderFactory, который, учитывая имя программы управления версиями, вернет подходящий читатель файла журнала (подкласса из общего), чтобы прочитать файл журнала и выплюнуть каноническое внутреннее представление. Поэтому, конечно, я могу сделать BazaarLogFileReader и GitLogFileReader и жестко закодировать их в программе, но я хочу, чтобы он был настроен таким образом, что добавление поддержки для новой программы управления версиями так же просто, как переполнение нового файла класса в каталоге с Bazaar и читателями Git.
Итак, прямо сейчас вы можете вызывать "делать-что-то-с-журналом" - программное обеспечение git "и" делать что-то-с-журналом - программный базар ", потому что есть журналы для тех, кто, Я хочу, чтобы было возможно просто добавить класс и файл SVNLogFileReader в один и тот же каталог и автоматически иметь возможность вызывать" do-something-with-the-log -software svn" без каких-либо изменений в остальной части программа. (Конечно, файлы могут быть названы с определенным шаблоном и globbed в требовании вызова.)
Я знаю, что это можно сделать в Ruby... Я просто не понимаю, как это сделать... или если я вообще это сделаю.
Ответы
Ответ 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