I'm wondering what is the best way to initialize a class in ruby depending on modules included. Let me give you an example:
class BaseSearch
def initialize query, options
#page = options[:page]
#...
end
end
class EventSearch < BaseSearch
include Search::Geolocalisable
def initialize query, options
end
end
class GroupSearch < BaseSearch
include Search::Geolocalisable
def initialize query, options
end
end
module Search::Geolocalisable
extend ActiveSupport::Concern
included do
attr_accessor :where, :user_location #...
end
end
What I don't want, is having to initialize the :where and :user_location variables on each class that include the geolocalisable module.
Currently, I just define methods like def geolocalisable?; true; end in my modules, and then, I initialize these attributes (added by the module) in the base class:
class BaseSearch
def initialize query, options
#page = options[:page]
#...
if geolocalisable?
#where = query[:where]
end
end
end
class EventSearch < BaseSearch
#...
def initialize query, options
#...
super query, options
end
end
Is there better solutions? I hope so!
Why not override initialize in the module? You could do
class BaseSearch
def initialize query
puts "base initialize"
end
end
module Geo
def initialize query
super
puts "module initialize"
end
end
class Subclass < BaseSearch
include Geo
def initialize query
super
puts "subclass initialize"
end
end
Subclass.new('foo') #=>
base initialize
module initialize
subclass initialize
Obviously this does require everything that includes your modules to have an initialize with a similar signature or weird stuff might happen
See this code :
module Search::Geolocalisable
def self.included(base)
base.class_eval do
attr_accessor :where, :user_location #...
end
end
end
class EventSearch < BaseSearch
include Search::Geolocalisable
end
Related
Currently in my application I have one helper.rb (Helper module is defined in this file) which is included in my controller.rb file like this:
class Controller
before_action :authenticate_user!
include Helper
Problem is that I need to define one more module e.g. Helper2 and I don't know how to include them using if condition and I don't know if even this solution is possible.
example what I want to do:
class Controller
before_action :authenticate_user!
if variable = 1
include Helper
else
include Helper2
end
Thx for answers!
YAGNI.
There are better ways.
The easist way to make the behavior customizable is to just have a set of methods that can be overridden by classes that consume the module:
module Greeter
def initialize(name)
#name = name
end
def salution
"Hello"
end
def hello
"#{salution}!, my name is #{#name}"
end
end
class Person
include Greeter
end
puts Person.new('Bob').hello # Hello!, my name is Bob
class Dog
include Greeter
def salution
"Woof"
end
end
puts Dog.new('Laika').hello # Woof!, my name is Laika
For more complex tasks there is the "macro method" pattern you'll see all over in Ruby:
module Configurable
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
#options ||= {}
end
end
module ClassMethods
def configure(**kwargs)
#options.merge!(kwargs)
end
def options
#options
end
end
end
class Foo
include Configurable
configure(bar: :baz)
end
puts Foo.options.inspect
# {:bar=>:baz}
This is simply a class method that defines class variables / class instance variables, defines methods or whatever you need to be done. For example these very simplefied API clients:
class Client
include HTTParty
format :json
def answers
self.class.get('/answers')
end
end
class StackoverflowClient < Client
base_uri 'https://stackoverflow.com'
end
class SoftwareEngineeringClient < Client
base_uri 'https://softwareengineering.stackexchange.com'
end
I want to be able to include my module in ActiveRecord::Base so that the has_folder_attachments method is available to my Rails AR classes.
I'm doing this to extend the original module's function to support AR hooks; however the variables #physical_path and #dice are both nil and I don't understand why.
module FolderAttachments
module ClassMethods
def has_folder_attachments(physical_path, excludes: [])
#physical_path = physical_path
super
end
end
def self.prepended(base)
class << base
prepend ClassMethods
end
end
attr_reader :physical_path
end
module ActiveRecord
class Base
prepend FolderAttachments
attr_reader :dice
# This should run after the module method
def self.has_folder_attachments(*args)
#dice = true
end
end
end
class Damned < ActiveRecord::Base
has_folder_attachments :for_real
end
damn = Damned.new
puts damn.physical_path # => nil
puts damn.dice # => nil
You are mixing instance and (meta)class context when using the two variables. Both variables are set their values in methods that are run in the class context (more precisely in the context of the metaclass). Thus, you cannot access these variables (and their attr_readers) in an instance context.
For the attr_readers to work, you have to move them to the class context and access them from there:
module FolderAttachments
module ClassMethods
...
attr_reader :physical_path
end
end
module ActiveRecord
class Base
...
class << self
attr_reader :dice
end
end
end
damn = Damned.new
damn.class.physical_path # => :for_real
damn.class.dice # => true
Or you may also add instance-level readers that delegate to the class-level readers so that you can access them also in instance context:
module FolderAttachments
module ClassMethods
...
attr_reader :physical_path
end
def physical_path
self.class.physical_path
end
end
module ActiveRecord
class Base
...
class << self
attr_reader :dice
end
def dice
self.class.dice
end
end
end
damn = Damned.new
damn.physical_path # => :for_real
damn.dice # => true
I need to access a class method (defined in ClassMethods) in an instance method inside a concern.
My brain is melted and I'm sure that is a simple thing that I'm doing wrong.
I need to access comparable_opts inside comparison. How can I do it?
Follow snippets below:
Concern
# app/models/concerns/compare.rb
module Compare
extend ActiveSupport::Concern
attr_accessor :comparable_opts
module ClassMethods
attr_reader :arguable_opts
def comparable_opts
##comparable_opts
end
private
def default_opts
#default_opts ||= {fields: [:answers_count,
:answers_correct_count,
:answers_correct_rate,
:users_count]}
end
def compare(opts={})
#comparable_opts = default_opts.merge(opts)
end
end
def comparison
end
end
Model
# app/models/mock_alternative.rb
class MockAlternative < ActiveRecord::Base
include Compare
belongs_to :mock, primary_key: :mock_id, foreign_key: :mock_id
compare fields: [:answers_count, :question_answers_count, :question_answers_rate],
with: :mock_aternative_school
def question_answers_rate
self[:answers_count].to_f/self[:question_answers_count].to_f
end
end
Solution:
I've just used cattr_accessor in my method compare. Thank everyone.
module Compare
extend ActiveSupport::Concern
module ClassMethods
attr_reader :arguable_opts
def comparison_klass
"ActiveRecord::#{comparable_opts[:with].to_s.classify}".constantize
end
private
def default_opts
#default_opts ||= {fields: [:answers_count,
:answers_correct_count,
:answers_correct_rate,
:users_count]}
end
def compare(opts={})
cattr_accessor :comparable_opts
self.comparable_opts = default_opts.merge(opts)
end
end
def comparison
comparable_opts
end
end
I'm trying to understand some Ruby metaprogramming concepts.
I think I understand classes, objects, and metaclasses. Unfortunately, I'm very unclear on exactly what happens with included Modules with respect to their instance/'class' variables.
Here's a contrived question whose solution will answer my questions:
Suppose I'm writing my own crappy Rails "validates" method, but I want it to come from a mixed-in module, not a base class:
module MyMixin
# Somehow validates_wordiness_of() is defined/injected here.
def valid?
# Run through all of the fields enumerated in a class that uses
# "validate_wordiness_of" and make sure they .match(/\A\w+\z/)
end
end
class MyClass
include MyMixin
# Now I can call this method in my class definition and it will
# validate the word-ness of my string fields.
validate_wordiness_of :string_field1, :string_field2, :string_field3
# Insert rest of class here...
end
# This should work.
MyMixin.new.valid?
Ok, so how would you store that list of fields from the validate_wordiness_of invocation (in MyClass) in such a way that it can be used in the valid? method (from MyMixin)?
Or am I coming at this all wrong? Any info would be super appreciated!
So here are two alternative ways of doing it:
With "direct" access
module MyMixin
def self.included(base)
base.extend(ClassMethods)
end
def wordy?(value)
value.length > 2
end
module ClassMethods
def validates_wordiness_of(*attrs)
define_method(:valid?) do
attrs.all? do |attr|
wordy?(send(attr))
end
end
end
end
end
class MyClass
include MyMixin
validates_wordiness_of :foo, :bar
def foo
"a"
end
def bar
"asrtioenarst"
end
end
puts MyClass.new.valid?
The downside to this approach is that several consecutive calls to validates_wordiness_of will overwrite each other.
So you can't do this:
validates_wordiness_of :foo
validates_wordiness_of :bar
Saving validated attribute names in the class
You could also do this:
require 'set'
module MyMixin
def self.included(base)
base.extend(ClassMethods)
end
module Validation
def valid?
self.class.wordy_attributes.all? do |attr|
wordy?(self.send(attr))
end
end
def wordy?(value)
value.length > 2
end
end
module ClassMethods
def wordy_attributes
#wordy_attributes ||= Set.new
end
def validates_wordiness_of(*attrs)
include(Validation) unless validation_included?
wordy_attributes.merge(attrs)
end
def validation_included?
ancestors.include?(Validation)
end
end
end
class MyClass
include MyMixin
validates_wordiness_of :foo, :bar
def foo
"aastrarst"
end
def bar
"asrtioenarst"
end
end
MyClass.new.valid?
# => true
I chose to make the valid? method unavailable until you actually add a validation. This may be unwise. You could probably just have it return true if there are no validations.
This solution will quickly become unwieldy if you introduce other kinds of validations. In that case I would start wrapping validations in validator objects.
I am using Ruby on Rails 3.2.9 and Ruby 1.9.3. I have many model classes implementing similar methods as-like the following:
class ClassName_1 < ActiveRecord::Base
def great_method
self.method_1
end
def method_1 ... end
end
class ClassName_2 < ActiveRecord::Base
def great_method
result_1 = self.method_1
result_2 = self.method_2
result_1 && result_2
end
def method_1 ... end
def method_2 ... end
end
...
class ClassName_N < ActiveRecord::Base
def great_method
result_1 = self.method_1
result_2 = self.method_2
...
result_N = self.method_N
result_1 && result_2 && ... && result_N
end
def method_1 ... end
def method_2 ... end
...
def method_N ... end
end
Those model classes behaves almost the same (not the same) since some of those has an interface with some less or more methods. All methods are differently named (for instance, method_1 could be named bar and method_2 could be named foo), all return true or false, are always the same in each class and there is no relation between them.
What is the proper way to refactor those classes?
Note: At this time I am thinking to refactor classes by including the following module in each one:
module MyModule
def great_method
result_1 = self.respond_to?(:method_1) ? self.method_1 : true
result_2 = self.respond_to?(:method_2) ? self.method_2 : true
...
result_N = self.respond_to?(:method_N) ? self.method_N : true
result_1 && result_2 && ... && result_N
end
end
But I don't know if it is the proper way to accomplish what I am looking for. Furthermore, I am not sure of related advantages and disadvantages...
Looks like you're on the right track. If the method_n methods are unique to your classes then just build the module that you already have into a superclass that each ClassNameN inherits from:
class SuperClassName < ActiveRecord::Base
def great_method
#... what you have in your module
end
end
class ClassNameN < SuperClassName
def method_1 ... end
def method_2 ... end
end
There may be additional ways for you to factor out code depending on what goes on in your method_n methods, but it's impossible to say without more detail.
I would use a metaprogramming solution to clean this up somewhat.
module BetterCode
extend ActiveSupport::Concern
module ClassMethods
def boolean_method(name, *components)
define_method name do
components.all? { |c| send c }
end
end
end
end
And in your models:
class MyModel < ActiveRecord::Base
include BetterCode
boolean_method :great_method, :foo, :bar, :baz, :quux
end
Instances of MyModel will then respond to great_method with a boolean value indicating whether or not foo, bar, baz and quux are all true.
You can abstract out the great_method with something like this:
require 'active_support/concern'
module Greatest
extend ActiveSupport::Concern
module ClassMethods
attr_accessor :num_great_methods
def has_great_methods(n)
#num_great_methods = n
end
end
def great_method
(1..self.class.num_great_methods).each do |n|
return false unless self.__send__("method_#{n}")
end
true
end
end
class ClassName_3
include Greatest
has_great_method 3
# stub out the "method_*" methods
(1..3).each do |n|
define_method "method_#{n}" do
puts "method_#{n}"
true
end
end
end
puts ClassName_1.new.greatest