问题
I have the a Singleton class ExchangeRegistry
which keeps all the Exchange objects.
Instead of needing to call:
ExchangeRegistry.instance.exchanges
I want to be able to use:
ExchangeRegistry.exchanges
This works, but I'm not happy with the repetition:
require 'singleton'
# Ensure an Exchange is only created once
class ExchangeRegistry
include Singleton
# Class Methods ###### Here be duplication and dragons
def self.exchanges
instance.exchanges
end
def self.get(exchange)
instance.get(exchange)
end
# Instance Methods
attr_reader :exchanges
def initialize
@exchanges = {} # Stores every Exchange created
end
def get(exchange)
@exchanges[Exchange.to_sym exchange] ||= Exchange.create(exchange)
end
end
I'm not happy with the duplication in the class methods.
I've tried using Forwardable
and SimpleDelegator
but can't seem to get this to DRY out. (Most examples out there are not for class methods but for instance methods)
回答1:
The forwardable module will do this. Since you are forwarding class methods, you have to open up the eigenclass and define the forwarding there:
require 'forwardable'
require 'singleton'
class Foo
include Singleton
class << self
extend Forwardable
def_delegators :instance, :foo, :bar
end
def foo
'foo'
end
def bar
'bar'
end
end
p Foo.foo # => "foo"
p Foo.bar # => "bar"
回答2:
The accepted answer is clever, but seems needlessly complex (not to mention to performance penalty of method_missing
.
The usual way to solve this is to just assign the instance to a constant.
class ExchangeRegistrySingleton
include Singleton
# ...
end
ExchangeRegistry = ExchangeRegistrySingleton.instance
回答3:
You can take advantage of method_missing
hook and delegate the method calls to instance
.
require 'singleton'
class ExchangeRegistry
include Singleton
# Missing methods will be delegated to `instance` if an implementation is available.
# Else `NoMethodError` will be raised via call to `super`
def self.method_missing method_name, *args
if instance.respond_to? method_name
puts "** Defining new method: '#{method_name}'"
(class << self; self; end).instance_eval do
define_method(method_name) do |*args|
instance.send(method_name, *args)
end
end
instance.send(method_name, *args)
else
super
end
end
attr_reader :exchanges
def initialize
@exchanges = {} # Stores every Exchange created
end
def get(exchange)
@exchanges[Exchange.to_sym exchange] ||= Exchange.create(exchange)
end
end
# By default, there is no class method - `exchanges`
p ExchangeRegistry.singleton_methods.grep(/exchanges/)
#=> []
p ExchangeRegistry.exchanges
#=> ** Defining new method: 'exchanges'
#=> {}
# After first call to `exchanges`, a new class method is now available
# Future calls will not hit `method_missing` again.
p ExchangeRegistry.singleton_methods.grep(/exchanges/)
#=> [:exchanges]
p ExchangeRegistry.exchanges
#=> {}
Another answer of this question indicates that there is performance penalty for handling method_missing
. Hence, I have updated the answer to define the class method when first time a method_missing
is reported. The update is based on article: Dynamically create class methods in Ruby
来源:https://stackoverflow.com/questions/34828891/ruby-dry-class-methods-calling-singleton-instance-methods