Ruby and module inheritance - ruby

This should be an easy one for a ruby dev. I'm playing around with a gem and I need help with inheriting module variables. Code should speak better than me:
module SomeModule
extend ActiveSupport::Concern
attr_accessor :bbonified
class Railtie < Rails::Railtie
initializer "some_module.study" do
Rails.application.eager_load!
# => I WANT TO ACCESS HERE
puts #bbonified
end
end
module ClassMethods
def bbonify(*columns)
# => WHAT I DEFINE HERE
#bbonified = columns
end
end
end
ActiveRecord::Base.send(:include, SomeModule)

You're not going to be able to access #bbonified directly, that's a local variable in the class that imports this module.
You need to define a separate accessor method to retrieve it:
module ClassMethods
def bbonified
#bbonified
end
end
Then later you need to refer to this somehow, but as you're just talking about a module it will depend on what class has been extended.
Rails.application.eager_load!
SomeClass.bbonified

Related

Why do Ruby refinements only modify classes, not modules?

The Ruby docs on refinements state:
Refinements only modify classes, not modules so the argument must be a class.
Why is this?
It's possible to monkey-patch a module:
module MyModule
def my_method
"hello"
end
end
include MyModule
puts my_method # => hello
module MyModule
def my_method
"goodbye"
end
end
puts my_method # => goodbye
I'm sure this isn't a good idea, but it might not be as bad if you could limit the scope of such a monkey patch. So why can't you?
Refinements of modules are just as useful as refinements of classes. Consider how
module Foobar
refine Enumerable
def all_zero?; all? &:zero? end
end
end
is more polite than direct monkey patching:
module Enumerable
def all_zero?; all? &:zero? end
end
There was an implementation issue with refining modules, which seems to be solved by now.
The refine in Ruby is intended to deal with issues of monkey-patching and inheritance, where the intention is to limit the monkey patching to instances of classes within a specific namespace.
These same inheritance issues don't apply to modules in the same way, since modules can be extended or included within other modules (as opposed to classes) using mixins.
This would allow namespace limitation of the monkey-patching by creating a new module which extends and overrides the original within it's own namespace.
If to use your example:
module MyModule
def my_method
"hello"
end
end
include MyModule
puts my_method
# => hello
module MyOtherModule
extend MyModule
puts my_method # will print: hello
def my_method
"goodbye"
end
extend self
puts my_method # will print: goodbye
end
# => hello
# => goodbye
puts my_method
# => hello
As you can see, We managed to limit the 'monkey-patch' to the MyOtherModule namespace without using refine.
Since we aren't using instances of MyModule (MyModule is NOT a class), this approach works perfectly.
The same is not possible with classes since, among other reasons, class instances might not be limited to the namespace of the module within which they are used... Hence, for Class instances, refine should be used.

How do I import another module's class methods into my ruby class/module?

I know I can import instance_methods, but is it possible to import class methods, and how?
A common idiom is this:
module Bar
# object model hook. It's called when module is included.
# Use it to also bring class methods in by calling `extend`.
def self.included base
base.send :include, InstanceMethods
base.extend ClassMethods
end
module InstanceMethods
def hello
"hello from instance method"
end
end
module ClassMethods
def hello
"hello from class method"
end
end
end
class Foo
include Bar
end
Foo.hello # => "hello from class method"
Foo.new.hello # => "hello from instance method"
What's with that InstanceMethods module?
When I need module to include both instance and class methods to my class, I use two submodules. This way the methods are neatly grouped and, for example, can be easily collapsed in code editor.
It also feels more "uniform": both kinds of methods are injected from self.included hook.
Anyway, it's a matter of personal preference. This code works exactly the same way:
module Bar
def self.included base
base.extend ClassMethods
end
def hello
"hello from instance method"
end
module ClassMethods
def hello
"hello from class method"
end
end
end
The short answer is: no, you cannot cause methods of the module object itself ("class" methods of the module) to be in the inheritance chain for another object. #Sergio's answer is a common workaround (by defining the "class" methods to be part of another module).
You may find the following diagram instructive (click for full-size or get the PDF):
(source: phrogz.net)
Note: this diagram has not yet been updated for Ruby 1.9, where there are additional core objects like BasicObject that slightly change the root flow.

How can I refer to a module method without referring to the entire namespace?

I have a module defined as:
module MyApp
module Utility
def Utility.my_method
I want to use that method in several other classes. But I don't want to have to call:
MyApp::Utility.my_method
I would rather just call:
Utility.my_method
Is that reasonable? I've tried include MyApp::Utility and include MyApp to no avail.
Well, just assign any alias you want, e.g.:
ShortNameGoesHere = MyApp::Utility
ShortNameGoesHere.my_method
Here is an example of mixing in my_method to a class:
#myapp.rb
module MyApp
module Utility
def my_method
"called my_method"
end
end
end
#test.rb
require './myapp'
class MyClass
include MyApp::Utility
end
if __FILE__ == $0 then
m = MyClass.new
puts m.my_method
end
It sounds like you want to maintain the namespace of the module on the mixed-in method. I have seen attempts to do so (https://stackoverflow.com/a/7575460/149212) but it seems pretty messy.
If you need my_method to be namespaced, you could simply include a module identifier in the method name:
module MyApp
module Utility
def util_my_method
"called my_method"
end
end
end

How do I use class variables from class and instance methods which are mixed in via a Module

I want to be able to make an option passed to my class method (auditable) available to instance methods. I'm mixing in both the class and instance methods using a Module.
The obvious choice is to use a class variable, but I get an error when trying access it:
uninitialized class variable ##auditable_only_once in Auditable
class Document
include Auditable
auditable :only_once => true
end
# The mixin
module Auditable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def auditable(options = {})
options[:only_once] ||= false
class_eval do
# SET THE OPTION HERE!!
##auditable_only_once = options[:only_once]
end
end
end
private
def audit(action)
# AND READ IT BACK LATER HERE
return if ##auditable_only_once && self.audit_item
AuditItem.create(:auditable => self, :tag => "#{self.class.to_s}_#{action}".downcase, :user => self.student)
end
end
I've stripped out some of the code to make this a bit easier to read, the full code is here: https://gist.github.com/1004399 (EDIT: Gist now includes the solution)
Using ## class instance variables is irregular and the number of of occasions when they're strictly required is exceedingly rare. Most of the time they just seem to cause trouble or confusion. Generally you can use regular instance variables in the class context without issue.
What you might want to do is use a different template for this sort of thing. If you have mattr_accessor, which is provided by ActiveSupport, you may want to use that instead of that variable, or you can always write your own equivalent in your ClassMethods component.
One approach I've used is to break up your extension into two modules, a hook and an implementation. The hook only adds the methods to the base class that can be used to add the rest of the methods if required, but otherwise doesn't pollute the namespace:
module ClassExtender
def self.included(base)
base.send(:extend, self)
end
def engage(options = { })
extend ClassExtenderMethods::ClassMethods
include ClassExtenderMethods::InstanceMethods
self.class_extender_options.merge!(options)
end
end
This engage method can be called anything you like, as in your example it is auditable.
Next you create a container module for the class and instance methods that the extension adds when it is exercised:
module ClassExtenderMethods
module ClassMethods
def class_extender_options
#class_extender_options ||= {
:default_false => false
}
end
end
module InstanceMethods
def instance_method_example
:example
end
end
end
In this case there is a simple method class_extender_options that can be used to query or modify the options for a particular class. This avoids having to use the instance variable directly. An example instance method is also added.
You can define a simple example:
class Foo
include ClassExtender
engage(:true => true)
end
Then test that it is working properly:
Foo.class_extender_options
# => {:default_false=>false, :true=>true}
foo = Foo.new
foo.instance_method_example
# => :example

Unable to invoke mixin module method in spec

I have the following module:
# lib/translator/sms_command.rb
module Translator
module SmsCommand
def self.included(klass)
puts "SmsCommand initialized in #{klass}"
end
def alias_command(name)
end
end
end
And the following spec:
# spec/translator/sms_command_spec.rb
require 'spec_helper'
class ExampleCommand
include Translator::SmsCommand
alias_command :x
end
module Translator
describe SmsCommand do
describe "#alias_command" do
it "registers the class with the command registry" do
Translator::SmsCommand.command_registry.should include_key :x
end
end
end
end
Yes #alias_command does nothing at this stage but that's because I'm in the midst of developing it. However, when I run my spec I am seeing...
SmsCommand initialized in ExampleCommand
so the module is certainly being mixed in, however the spec barfs on the alias_command :x line in the ExampleCommand as if the alias_command method is never becoming available.
/spec/translator/sms_command_spec.rb:5:in `<class:ExampleCommand>': undefined method `alias_command' for ExampleCommand:Class (NoMethodError)
I could solve this problem through inheritance though I'd prefer the mixin module. What am I missing?
It's because it's not an include but an extend to access alias_command in your Class method
class ExampleCommand
extend Translator::SmsCommand
alias_command :x
end
You want to define a class method called alias_command, don't you?
In this case you need to extend the class with the module, a simple include will turn the included method into an instance method!
You can do it in a widely accepted way as follows:
module Translator
module SmsCommand
def self.included(klass)
puts "SmsCommand initialized in #{klass}"
klass.extend ClassMethods
end
module ClassMethods
def alias_command(name)
end
end
end
end
This way when you include the module, it will automatically extend the target class with the class methods!

Resources