I've got a class such as
# this has been simplified for the example
class MyClass
##private_attributes = []
def self.private_attributes(*args)
##private_attributes = args
end
def private_attributes
#private_attributes ||= ##private_attributes
end
end
It works great. I've this ##private_attributes set at class level which's then used at instance level in multiple ways.
I want to abstract this logic somewhere else to simplify my class, something like that
class MyClass
include PrivateAttributes
end
When I create the module PrivateAttributes, however I shape it, the ##private_attributes isn't understood at MyClass level.
I tried many things but here's the latest code attempt
module PrivateAttributes
include ProcessAttributes
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
##private_attributes = []
def private_attributes(*args)
##private_attributes = args
end
end
module InstanceMethods
def private_attributes
#private_attributes ||= process_attributes_from(##private_attributes)
end
def private_attributes?
instance_options[:scope] == :private
end
end
end
It crashes with this error
NameError:
uninitialized class variable ##private_attributes in PrivateAttributes::InstanceMethods
Did you mean? private_constant
In short, the ##private_attributes isn't transferred throughout the code, but looks like it stays at the module level.
What's the best way to abstract this logic from my original class ?
Working solution
An easy workaround is to use mattr_accessor on the class level or anything similar to communicate our data around. I preferred to write down my own methods in this case:
module PrivateAttributes
include ProcessAttributes
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
##private_attributes_memory = []
def private_attributes(*args)
##private_attributes_memory = args
end
def private_attributes_memory
##private_attributes_memory
end
end
module InstanceMethods
def private_attributes
#private_attributes ||= process_attributes_from private_attributes_memory
end
# you can add diverse methods here
# which will be used in MyClass once included
private
def private_attributes_memory
self.class.private_attributes_memory
end
end
end
Related
I'd like my module to define new instance methods based on its including class' instance methods. But in the included hook, the class methods are not defined yet (as the module is included at the top of the class, before the class methods are defined):
module MyModule
def self.included(includer)
puts includer.instance_methods.include? :my_class_method # false <- Problem
end
end
class MyClass
include MyModule
def my_class_method
end
end
I want the users of the module to be free to include it at the top of their class.
Is there a way to make a module define additional methods to a class?
Note: I don't have to use the included hook if there is another way to achieve this.
There'a a method_added callback you could use:
module MyModule
def self.included(includer)
def includer.method_added(name)
puts "Method added #{name.inspect}"
end
end
end
class MyClass
include MyModule
def foo ; end
end
Output:
Method added :foo
If you want to track both, existing and future methods, you might need something like this:
module MyModule
def self.on_method(name)
puts "Method #{name.inspect}"
end
def self.included(includer)
includer.instance_methods(false).each do |name|
on_method(name)
end
def includer.method_added(name)
MyModule.on_method(name)
end
end
end
Example:
class MyClass
def foo ; end
include MyModule
def bar; end
end
# Method :foo
# Method :bar
I want to create a DSL which stores various blocks, and then can call them. I want this to be a reusable module that I can include in multiple classes.
I figured out a way to do this using class variables, but rubocop complains and says that I should use class instance variables instead. I can't figure out a way to do this though. Is it possible?
module MyModule
def self.included(base)
base.extend(ClassMethods)
end
def run_fixers
ClassMethods.class_variable_get(:##fixers).each(&:call)
end
module ClassMethods
def fix(_name, &block)
##fixers ||= []
##fixers << block
end
end
end
class MyClass
include MyModule
def initialize
run_fixers
end
fix 'test' do
puts 'testing'
end
end
Rubocop complains about ##class_variable style. Class variables usage can be refactored into using instance variable of a class:
class SomeClass
#some_variable ||= []
end
instead of
class SomeClass
##some_variable ||= []
end
You can define instance variable of a class in any place where self equals to the class object. For example, in a class method:
module MyModule
def self.included(base)
base.extend(ClassMethods)
end
def run_fixers
ClassMethods.fixers.each(&:call)
end
module ClassMethods
def self.fixers
#fixers ||= []
end
def fix(_name, &block)
ClassMethods.fixers << block
end
end
end
So while I'm all against extending existing classes this way, sometimes (hacking rspec) it's necessary to do something like:
module MyModule
module ClassMethods
def define_something(name)
##names ||= []
##names << name
end
end
def self.included(base)
base.extend ClassMethods
end
def all_names
##names
end
end
class Example
include MyModule
define_something "one"
define_something "two"
end
Example.new.all_names
and then it yields this error:
NameError: uninitialized class variable ##names in MyModule
and I understand that because at the time of writing MyModule::ClassMethods - we are working on instance not class (not self.), so I tried:
module MyModule
module ClassMethods
def define_something(name)
#names ||= []
#names << name
end
end
def self.included(base)
base.extend ClassMethods
end
def all_names
##names
end
end
class Example
include MyModule
define_something "one"
define_something "two"
end
Example.new.all_names
it does not work either, finally I ended up with:
module MyModule
module ClassMethods
def define_something(name)
#names << name
end
end
def self.included(base)
base.instance_variable_set(:#names, [])
base.send(:define_method, :all_names) { base.instance_variable_get(:#names) }
base.extend ClassMethods
end
end
class Example
include MyModule
define_something "one"
define_something "two"
end
Example.new.all_names
Is there a better way to do that?
It's usually better from a design perspective to avoid crossing the class/instance line using instance variable references. This is usually more clear:
module MyModule
module ClassMethods
def define_something(name)
self.defined_somethings << name
end
def defined_somethings
#_my_module_names ||= []
end
end
def self.included(base)
base.extend ClassMethods
end
def all_names
self.class.defined_somethings
end
end
class Example
include MyModule
define_something "one"
define_something "two"
end
Example.new.all_names.inspect
#=> ["one","two"]
I've taken care here to create a class-level instance variable with a verbose name. Calling it #name could put it into conflict with a variable defined by the class that includes this module. Remember, when designing mixin code you're a guest and you need to be extra polite.
Class-type instance variables like ##name are sometimes trouble because they can end up spanning inheritance chains depending on how they're used. Defining a method means you can override it at any point in the chain, something not possible with a shared variable.
In other words, treat the instance's class as a separate object and make clear, well-defined method calls to maintain that separation.
Try the following
module MyModule
module ClassMethods
def define_something(name)
#names ||= []
#names << name
end
end
def self.included(base)
base.extend ClassMethods
end
def all_names
self.class.instance_variable_get(:#names)
end
end
class Example
include MyModule
define_something "one"
define_something "two"
end
Example.new.all_names
A class variable can be accessed using ## operator from a class level method i.e. self. methods.
#tadman's answer has a generic approach for similar problems (generally found in gems that provide modules to implement a functionality)
module Gym
def self.included(class_or_module)
class_or_module.send(:include, InstanceMethods)
class_or_module.extend(ClassMethods)
end
module ClassMethods
def build
end
end
module InstanceMethods
def open
end
def book_for_practice
end
def close
end
end
end
this is an example in the Ruby's Object Lifecycle Callbacks section of RubyMonk. I don't understand how it's supposed to work or what the point of this is. self.included should just document how the two modules within Gym get used, right? why does class_or_module then get sent/extended? why doesn't it get saved in some sort of arrays that document the lifecyle, like in the examples leading up to this one, such as
##extended_objects = []
def self.extended_objects
##extended_objects
end
def self.extended(class_or_module)
##extended_objects << class_or_module
It's not just documentation. self.included is a callback method that gets called as soon as the module is being included in any other module or class.
Instance methods are included via send, class or module methods via extend in that example.
Find out more in the Ruby documentation.
Let me answer your Question One by one.
1: self.included should just document how the two modules within Gym get used ?
module A
def instance_methods_1
p 'hello instance_methods_1'
end
def instance_methods_2
p 'hello instance_methods_2'
end
module KlassMethods
def klass_methods_1
p 'Hello!! klass_methods_1'
end
def klass_methods_2
p 'Hello!! klass_methods_2'
end
end
end
class B
include A # instead of writing two piece of code like this we could wrap in one using `self.included` method Hook
extend A::KlassMethods
end
B.new.instance_methods_1
B.new.instance_methods_2
B.klass_methods_1
B.klass_methods_2
Another version of same program with method hook using self.included
module A
# this is special method one of the methods hook in ruby.
def self.included(base)
base.extend(KlassMethods)
end
def instance_methods_1
p 'hello instance_methods_1'
end
def instance_methods_2
p 'hello instance_methods_2'
end
module KlassMethods
def klass_methods_1
p 'Hello!! klass_methods_1'
end
def klass_methods_2
p 'Hello!! klass_methods_2'
end
end
end
class B
include A
end
B.new.instance_methods_1
B.new.instance_methods_2
B.klass_methods_1
B.klass_methods_2
2: self.included
To know more about included hook and
Ruby Hook
3: why does class_or_module then get sent/extended ?
module AddAdditionalProperty
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def add_additional_property
p 'ClassMethods::add_additional_property'
end
end
end
module ActiveRecord
class Base
def test
p 'Base test Method'
end
end
end
ActiveRecord::Base.send(:include, AddAdditionalProperty)
ActiveRecord::Base.add_additional_property
## Another version of same Program
module AddAdditionalProperty
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def add_additional_property
p 'ClassMethods::add_additional_property'
end
end
end
module ActiveRecord
class Base
include AddAdditionalProperty
def test
p 'Base test Method'
end
end
end
ActiveRecord::Base.add_additional_property
Hope this answer help you !!!
I have Ruby class into which I want to include both class and instance methods. Following the pattern described here, I'm currently using the following:
class SomeObject
include SomeObject::Ability
def self.some_builder_method(params)
# use some_class_method ...
end
end
module SomeObject::Ability
module ClassMethods
def some_class_method(param)
# ...
end
end
def self.included(klass)
klass.extend(ClassMethods)
end
def some_instance_method
# ...
end
end
I'd rather not make two separate modules (one being included and the other being extended), because all the methods in my module logically fit together. On the other hand, this pattern a) requires me to define an additional ClassMethods module and b) requires me to write a boilerplate self.included method for every module.
Is there a better way to do this?
Edit 1: I've found another way, but I'm unsure if this is better than the first.
module Concern
def included(base)
# Define instance methods.
instance_methods.each do |m|
defn = instance_method(m)
base.class_eval { define_method(m, defn) }
end
# Define class methods.
(self.methods - Module.methods).each do |m|
unless m == __method__
base.define_singleton_method(m, &method(m))
end
end
end
end
module SomeModule
extend Concern
def self.class_m
puts "Class"
end
def instance_m
puts "Instance"
end
end
class Allo
include SomeModule
end
Allo.class_m # => "Class"
Allo.new.instance_m # => "Instance"
If I understand you correctly, you really just want to use ActiveSupport::Concern:
module PetWorthy
extend ActiveSupport::Concern
included do
validates :was_pet, inclusion: [true, 'yes']
end
def pet #instance method
end
module ClassMethods
def find_petworthy_animal
# ...
end
end
end
class Kitty
include PetWorthy
end
Kitty.find_petworthy_animal.pet
You (hopefully obviously) don't need to use the included method if you don't have any behavior to trigger on include, but I put it in just to demonstrate.