How to refactor model classes implementing similar methods? - ruby-on-rails

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

Related

Rails, writing module. Where is to store setting?

I have a behavior for models.
I'd like to include like this:
class Building < ActiveRecord::Base
has_many :blocks
include Staticizable
check_children :blocks #here name of children model to check
...
end
The behavior looks like:
module Staticizable
extend ActiveSupport::Concern
module ClassMethods
def check_children child
self.child_model = child #where is to store model name to check?
end
end
def was_staticized
(DateTime.now - self.staticized_date.to_datetime).to_i
end
def staticized?
if #child_model.present?
ap self.send(#child_model)
else
if staticize_period > was_staticized
true
else
false
end
end
end
def staticized
self.staticized_date = DateTime.now
save
end
end
So I need to know where I can store model name to check children. And what is the right way to do things like this?
Finally, I got the answer, not sure that is the best way, if no, let me know, please:
module Staticizable
extend ActiveSupport::Concern
included do
class << self;
attr_accessor :has_child_custom
end
end
module ClassMethods
def check_children child
self.has_child_custom = child
end
end
def was_staticized
unless staticized_date
return false
end
(DateTime.now - self.staticized_date.to_datetime).to_i
end
def staticized?
if self.class.has_child_custom.present?
self.send(self.class.has_child_custom).each do |child|
unless child.staticized?
return false
end
end
else
if self.staticize_period > was_staticized
true
else
false
end
end
end
def staticized
self.staticized_date = DateTime.now
save
end
end
Also I've created a file in /config/initializers with
ActiveRecord::Base.send :include, Staticizable
And set it up in model with:
class Building < ActiveRecord::Base
check_children :blocks
...
end

Ruby on rails DRY strip whitespace from selective input forms

I'm fairly new to rails so bear with me.
I want to strip whitespace from a selective group of input forms.
But I would like a DRY solution.
So I was thinking there might be a solution such as a helper method, or a custom callback. Or a combination such as before_validation strip_whitespace(:attribute, :attribute2, etc)
Any help is awesome! Thanks!
EDIT
I have this in my model file ...
include ApplicationHelper
strip_whitespace_from_attributes :employer_name
... and I have this in my ApplicationHelper ...
def strip_whitespace_from_attributes(*args)
args.each do |attribute|
attribute.gsub('\s*', '')
end
end
but now I'm getting the error message:
undefined method `strip_whitespace_from_attributes' for "the test":String
EDIT II -- SUCCESS
I added this StripWhitespace module file to the lib directory
module StripWhitespace
extend ActiveSupport::Concern
module ClassMethods
def strip_whitespace_from_attributes(*args)
args.each do |attribute|
define_method "#{attribute}=" do |value|
#debugger
value = value.gsub(/\s*/, "")
#debugger
super(value)
end
end
end
end
end
ActiveRecord::Base.send(:include, StripWhitespace)
and then added this to any model class this wants to strip whitespace ...
include StripWhitespace
strip_whitespace_from_attributes #add any attributes that need whitespace stripped
I would go with sth like this (not tested):
module Stripper # yeah!
extend ActiveSupport::Concern
module ClassMethods
def strip_attributes(*args)
mod = Module.new
args.each do |attribute|
define_method "#{attribute}=" do |value|
value = value.strip if value.respond_to? :strip
super(value)
end
end
end
include mod
end
end
end
class MyModel < ActiveRecord::Base
include Stripper
strip_attributes :foo, :bar
end
m = MyModel.new
m.foo = ' stripped '
m.foo #=> 'stripped'
If you can get your attributes in to a single array (perhaps there's a [:params] key you can use instead), you can do the following:
class FooController < ApplicationController
before_create strip_whitespace(params)
private
def strip_whitespace(*params)
params.map{ |attr| attr.strip }
end
end

How to access a class method (or variable) in an instance method using concern?

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

How would I implement my own Rails-style validates() method in Ruby?

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.

Initialize a Ruby class depending on what modules are included

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

Resources