2

I have some logic in several of my rails models that I would like to split out into separate files.

Specifically, this is logic that is unique to these models and not something that would be shared between models. For that case I am aware of concerns/mixins and questions like this.

Since we are dealing with Ruby here it seems like the way to go is to have multiple class definitions. E.g:

# in app/models/user.rb
class User < ActiveRecord::Base
  ...
end

# in app/lib/integrations/ext/user.rb
class User
  ...
end

The problem I am facing here is now requiring the model extensions in the right place. Because of auto-loading, I am forced to explicitly require the model and the extension. My current best effort is to preload the User model and its extension inside of an initializer:

# in config/initializers/model_extensions.rb
require_dependency 'models/user'
require_dependency 'integrations/ext/user.rb'

But this creates issues with other gems (e.g. Devise not being loaded when the User model is loaded).

Is there a good way to do this or am I off keel here? Taking advantage of Ruby's open classes is a common idiom outside of Rails.

Community
  • 1
  • 1
ghempton
  • 7,777
  • 7
  • 48
  • 53

3 Answers3

2

Add the following to config/application.rb

config.autoload_paths += Dir["#{config.root}/lib"]

This will autoload the classes from integrations/ext as Rails requires them. (This happens to be exactly how DHH's gist includes the app/model/concerns files too).

(I changed my autoload to match what @denis.pelin has. I missed that integrations/ is in lib/, so the above should be enough to autoload).

deefour
  • 34,974
  • 7
  • 97
  • 90
  • Auto loading only loads classes when they are referenced. In the case of a mixin/concern (which I am avoiding in this case) the class is referenced in the model so it is loaded. In my case I am trying to simply open the class back up (the original model definition has no reference to the extension). – ghempton Aug 05 '12 at 15:27
  • Sorry, it's not clear how you're using the `User` class in `lib/integrations/ext`. `...` being the only thing in the body of both classes doesn't really help to understand what you're trying to do or why autoload won't work. – deefour Aug 05 '12 at 16:35
  • Just basic method definitions. I am simply trying to use Ruby's open classes to define User in two different class bodies (without mixins!). Thus, autoloading doesn't work because `app/models/user.rb` has no reference to `app/lib/integrations/ext/user.rb` – ghempton Aug 05 '12 at 17:17
  • I posted a 2nd answer offering an alternative approach to what I think you're trying to do. – deefour Aug 05 '12 at 19:30
1

If I understand correctly, what you're trying to do is move code that is still specific to a model (not common between multiple models suggesting a mixin is the right way to go) out of the model to keep it 'thin'; code which really just looks 'off' existing in the model.

When I see code in my model that is getting a little too complicated or involves tasks that just look wrong sitting directly in a model, I create a file in lib/. For (a simplified) example, given a model like

class User < ActiveRecord::Base

  def self.create_admin!(attrs)
    # complex logic to setup admin user account here
    newly_created_user.deliver_confirmation_email!
  end

  def deliver_confirmation_email!
    # sends email off to user represented by this class instance
  end
end

This just looks bad to me being in the model. But having a couple dozen lines of code for the above methods in the create action in my controller looks even worse and is harder to test.

I will move this code to lib/MyNamespace/user_repo.rb

module MyNamespace
  module UserRepo
    extend self

    def create_admin!(attrs)
      # complex logic to setup admin user account here
      deliver_confirmation_email!(newly_created_user)
    end

    private
      def deliver_confirmation_email!(user)
        # sends email off to user represented by this class instance
      end
  end
end

Now, in my create action in my controller, instead of calling

User.create_admin!(params[:user])

I will instead call

MyNamespace::UserRepo::create_admin!(params[:user])

The MyNamespace::UserRepo is responsible for managing what happens to the User record for the admin account, leaving both my controller action and my model nice and clean. It's also easier to test MyNamespace::UserRepo because of this separation.

This still doesn't solve the problem you're having with getting rails to require the code you're looking for, but perhaps offers an alternative solution to what you're trying to achieve.

deefour
  • 34,974
  • 7
  • 97
  • 90
  • Thanks for this alternative approach. You are indeed correct in that I want to separate code that is unique to a particular model (not shared between models). Your approach here reminds me of using a service layer to encapsulate model logic. – ghempton Aug 05 '12 at 19:37
  • I upvoted but am not going to mark this as the answer. What I am trying to do is a common Ruby idiom outside of rails. – ghempton Aug 05 '12 at 19:38
  • I look forward to seeing what the accepted solution looks like. Good luck! – deefour Aug 05 '12 at 19:39
0

In one of my applications I have uncommented autoload line in application.rb:

# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

some module in lib directory:

# lib/some_module.rb
module SomeModule
  def some_method
  end
end

and inclusion in model:

# app/models/user.rb
class User < ActiveRecord::Base
  include SomeModule
end

Now User instances have some_method

denis.peplin
  • 9,585
  • 3
  • 48
  • 55