views:

167

answers:

4

I have two models that contain the same method:

def foo
  # do something
end

Where should I put this?

I know common code goes in the lib directory in a Rails app.

But if I put it in a new class in lib called 'Foo', and I need to add its functionality to both of my ActiveRecord models, do I do that like this:

class A < ActiveRecord::Base
includes Foo

class B < ActiveRecord::Base
includes Foo

and then both A and B will contain the foo method just as if I had defined it in each?

+9  A: 

Create a module, which you can put in the lib directory:

module Foo
  def foo
    # do something
  end
end

You can then include the module in each of your model classes:

class A < ActiveRecord::Base
  include Foo
end

class B < ActiveRecord::Base
  include Foo
end

The A and B models will now have a foo method defined.

If you follow Rails naming conventions with the name of the module and the name of the file (e.g. Foo in foo.rb and FooBar in foo_bar.rb), then Rails will automatically load the file for you. Otherwise, you will need to use require_dependency 'file\_name' to load your lib file.

Phil Ross
+2  A: 

One option is to put them in a new directory, for example app/models/modules/. Then, you can add this to config/environment.rb:

Dir["#{RAILS_ROOT}/app/models/modules/*.rb"].each do |filename|
  require filename
end

This will require every file in in that directory, so if you put a file like the following in your modules directory:

module SharedMethods
  def foo
    #...
  end
end

Then you can just use it in your models because it will be automatically loaded:

class User < ActiveRecord::Base
  include SharedMethods
end

This approach is more organized than putting these mixins in the lib directory because they stay near the classes that use them.

nicholaides
If both models call "before_save :before_method" and I also put this in SharedMethods, will that work too? Or does it only work for the method definitions?
Bryan Locke
Also, does it matter where your 'require' code appears in environment.rb?
Bryan Locke
You'll probably want it in the "Rails::Initializer.run do |config| ... end" section
nicholaides
And, yes, the method "before_method" can be in a mixin, but the code "before_save :before_method" cannot.
nicholaides
It is possible to add the before_save call to the mixin. I've added an answer that shows how.
EmFi
+3  A: 

You really have two choices:

  1. Use a module for common logic and include it in A & B
  2. Use a common class C that extends ActiveRecord and have A & B extend C.

Use #1 if the shared functionality is not core to each class, but applicable to each class. For example:

(app/lib/serializable.rb)
module Serializable
  def serialize
    # do something to serialize this object
  end
end

Use #2 if the shared functionality is common to each class and A & B share a natural relationship:

(app/models/letter.rb)
class Letter < ActiveRecord::Base
  def cyrilic_equivilent
    # return somethign similar
  end
end

class A < Letter
end

class B < Letter
end
BigCanOfTuna
+1  A: 

As others have mentioned include Foo is the way to do things... However it doesn't seem to get you the functionality you want with a basic module. The following is the form a lot of Rails plugins take to add class methods and new callbacks in addition to new instance methods.

module Foo #:nodoc:

  def self.included(base) # :nodoc:
    base.extend ClassMethods
  end

  module ClassMethods
    include Foo::InstanceMethods

    before_create :before_method
  end

  module InstanceMethods
    def foo
      ...
    end

    def before_method
      ...
    end
  end 

end
EmFi