Это на самом деле просто срывает решение Брайана Кэмпбелла. Если вам это нравится, пожалуйста 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:
- Все классы, имена которых соответствуют шаблону
<Name>LogFileReader
, будут автоматически найдены и зарегистрированы как LogFileReader
для :name
(см .: GitLogFileReader
),
- Все классы, которые смешиваются в модуле
LogFileReader
и чье имя соответствует шаблону <Name>Whatever
, будут зарегистрированы для обработчика :name
(см .: BzrFrobnicator
) и
- Все классы, которые смешиваются в модуле
LogFileReader(:name)
, будут зарегистрированы для обработчика :name
независимо от их имени (см .: NameThatDoesntFitThePattern
).
Обратите внимание, что это очень надуманная демонстрация. Это, например, определенно не поточно-ориентированный. Это также может привести к утечке памяти. Используйте с осторожностью!