The best way to “split” up a large Rails controller - ruby-on-rails

I currently have an already-large controller that’s getting bigger. I was wondering what would be the best way to slim down my controllers. I’m not necessarily looking for the easiest way, but a safe and efficient way. I’ve been developing with Rails for a while now but I’m still not familiar with how “subclassing” works and I’m not even sure if it’s supposed to be used in this way. I was thinking maybe something like this?
class SomeController < ApplicationController
end
class MoreFunctionsController < SomeController
end
That’s currently untested – I’m still working on it right now – but I hope that this would sort of give you an idea of what direction I’m trying to go. I’m also not sure how the routing for this would look. What would be the best way to “split” up a large controller?

ActiveSupport::Concern (documentation) is what you are looking for.
Update
Something like this:
# config/application.rb
config.autoload_paths += %W(#{Rails.root}/app/controllers/concerns) # in Rails4 this is automatic
# app/controllers/my_controller.rb
class MyController < ApplicationController
include GeneralStuffConcern
def index
render text: foo
end
end
# app/controllers/concerns/general_stuff_concern.rb
module GeneralStuffConcern
extend ActiveSupport::Concern
def show
redirect_to root_path
end
protected
def foo
'fooo'
end
end
update 2
I actually recommend this more http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
update 3 (2022)
Bounded contexts https://blog.eq8.eu/article/rails-bounded-contexts.html

Related

How to DRY up 2 controllers/models/views that are basically the exact same

An Order has_many AItems and BItems. As you can tell, the items are basically identical but with an important business reason for categorizing them separately. Wondering what's the best strategy to DRY this up. I realize this is a little opinionated... but hoping to get some clear points of view and arguments.
View code
Currently I'm using a partial. Like this:
class AItemsController
def new
end
end
class BItemsController
def new
end
end
# view files layout
> views
> AItems
> new.html.erb
> BItems
> new.html.erb
# routing
get '/AItems/new'
get '/BItems/new'
# code for /views/AItems/new.html.erb
<%= render "layouts/items_new", object: "AItems" %>
# code for /views/BItems/new.html.erb
<%= render "layouts/items_new", object: "BItems" %>
I'm wondering if it'd be easier to get rid of the partial entirely and just do parameters like this:
class AItemsController
def new
end
end
class BItemsController
def new
end
end
# view files layout
> views
> Items
> new.html.erb
# routing
get '/items/new/:type'
# code for /views/Items/new.html.erb
# code from partial evaluating the param[:type] instead of a passed object
Controller code
Currently everything is duplicated... (I haven't made any attempt at DRYing yet) as in it looks like this (very illustrative, the point is to just show that short of the naming conventions literally everything is basically the same):
class AItemsController
def new
#items = AItems.joins(:order).where("orders.status_id IS NULL")
end
def do_something
a_items_params.each do |item_params|
key_var = item_params[:some_attribute]
...
end
end
end
class BItemsController
def new
#items = BItems.joins(:order).where("orders.status_id IS NULL")
end
def do_something
b_items_params.each do |item_params|
key_var = item_params[:some_attribute]
...
end
end
end
I haven't DRYed this yet because I'm a little conflicted as to how. Examples below are illustrative, forgive if the code isn't exact, but hopefully you get the gist.
Solution A: In one way, I could keep the action definitions in each controller, and then have the code within the action pull from a shared concern:
class AItemsController
include SharedCode
def new
shared_new
end
def do_something
shared_do_something
end
end
Solution B: abstract away the action definitions to the shared concern:
class AItemsController
included SharedAction
shared_action("AItems")
end
Solution C: route everything to a singular controller and again use params to differentiate (passed from view)
class ItemsController
def new
item_type = params[:item_type]
end
def do_something
item_type = params[:item_type]
end
end
Model code
This one is a little more cut and dry, and I don't need a ton of feedback here, I will just used shared concerns for key methods/ callback.
Obviously the answer for one will affect the other. For example if everything routes through a single controller, then I'll have a single view with parameters rather than a partial approach. But because the controller has multiple DRYing options, there's still room for debate.
If you've read this far, I will happily take angry comments about how this question is too loosely defined in exchange for at least some thoughts on what you would do. What's more understandable for you if you were taking over my code?
I am trying to learn and the best way to do that is to solicit multiple points of view and pros and cons to weigh out.
Check out the InheritedResources Gem: https://github.com/josevalim/inherited_resources
Inherited Resources speeds up development by making your controllers
inherit all restful actions so you just have to focus on what is
important. It makes your controllers more powerful and cleaner at the
same time.
Or the Responders Gem, a replacement to Inherited Resources: https://github.com/plataformatec/responders
A set of responders modules to dry up your Rails 4.2+ app.

custom link_to - ruby on rails

I have a class Thingy, whose objects often need to create links to other websites. So far the only approach working for me is the following:
class Thingy < Active:Record::Base
def makeLink
result = ""+self.secondProperty+""
end
...
Now in any view I could use this link-method as follows
thingy.makeLink.html_safe
Somehow I feel there should be a much better way. What is the right approach here?
the proper approach would be to add a ThingyHelper in app/helpers
def behavior_describing_method_name(thingy)
link_to thingy.secondProperty, "someUrl#{thingy.firstProperty}"
end

Rails: How to POST internally to another controller action?

This is going to sound strange, but hear me out...I need to be able to make the equivalent of a POST request to one of my other controllers. The SimpleController is basically a simplified version of a more verbose controller. How can I do this appropriately?
class VerboseController < ApplicationController
def create
# lots of required params
end
end
class SimpleController < ApplicationController
def create
# prepare the params required for VerboseController.create
# now call the VerboseController.create with the new params
end
end
Maybe I am over-thinking this, but I don't know how to do this.
Inter-controller communication in a Rails app (or any web app following the same model-adapter-view pattern for that matter) is something you should actively avoid. When you are tempted to do so consider it a sign that you are fighting the patterns and framework your app is built on and that you are relying on logic has been implemented at the wrong layer of your application.
As #ismaelga suggested in a comment; both controllers should invoke some common component to handle this shared behavior and keep your controllers "skinny". In Rails that's often a method on a model object, especially for the sort of creation behavior you seem to be worried about in this case.
You shouldn't be doing this. Are you creating a model? Then having two class methods on the model would be much better. It also separates the code much better. Then you can use the methods not only in controllers but also background jobs (etc.) in the future.
For example if you're creating a Person:
class VerboseController < ApplicationController
def create
Person.verbose_create(params)
end
end
class SimpleController < ApplicationController
def create
Person.simple_create(params)
end
end
Then in the Person-model you could go like this:
class Person
def self.verbose_create(options)
# ... do the creating stuff here
end
def self.simple_create(options)
# Prepare the options as you were trying to do in the controller...
prepared_options = options.merge(some: "option")
# ... and pass them to the verbose_create method
verbose_create(prepared_options)
end
end
I hope this can help a little. :-)

SessionsHelper in railstutorial.org: Should helpers be general-purpose modules for code not needed in views?

railstutorial.org has a suggestion which strikes me as a little odd.
It suggests this code:
class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
end
The include SessionsHelper makes the methods available from ApplicationController, yes, but it makes them available in any view, as well. I understand that authentication/authorization is cross-cutting, but is this really the best place?
That seems to me to be potentially too broad of a scope. Putting code which implements, say, a before_filter which conditionally redirects (as the railstutorial.org example does) in a module which more commonly contains view helpers seems surprising.
Would functionality not strictly needed in views be better placed in ApplicationController or elsewhere?
Or am I just thinking too much about this?
Indeed, your feeling is correct.
I would implement this the other way around: add the functions sign_in and current_user to ApplicationController (or if you really want to: in a separate module defined in lib and include it), and then make sure that the current_user method is available in the view.
In short:
class ApplicationController
helper_method :current_user
def sign_in
end
def current_user
#current_user ||= user_from_remember_token
end
end
Of course, if you have a lot of code to place into your ApplicationController it can get messy. In that case I would create a file lib\session_management.rb:
module SessionManagement
def self.included(base)
base.helper_method :current_user
end
def sign_in
..
end
def current_user
..
end
end
and inside your controller you can then just write:
class ApplicationController
include SessionManagement
end
They seem to be taking (sneaky) advantage of the fact that, in Rails, Helpers are just ruby Modules.
Placing behavior that is shared across Controllers in a Module is, in my opinion, good practice. Putting it in a Helper, on the other hand, is potentially misleading and I would avoid it. Place it in a “standard” Module.
This is a philosophical question on the same level as the argument that questions the REST method provided in scaffolding and if A scaffold is worth having at all. You have to consider the fact that the tutorial book in RailsTutorial.org is a get-up-and-go Rails instructive guide. So for the purpose which it serves, I think it does the job.
However, is there a better place to put code needed across controllers and views? Yes, there is.
Some may follow Michael Hartl form Railstutorial and include the entire SessionHelper into the ApplicationController
Others may decide to expose only the essential helpers needed for the view i.e. sign_in, sign_out, current_user and the like.
I see a suggestion to put such code in the /lib directory and include it where needed.
All are viable options. Whichever you take may not matter that much in performance because Ruby would have to parse the file which you want to call (or include) a class, module or method from. What happens is, before any code is executed in a class, Ruby goes through the whole class once to know what's in it. It all depends on what one needs and the design of their app
FWIW, I store the current user in the User class:
class User < ActiveRecord::Base
cattr_accessor :current
...
end
This can be referenced in all 3 MVC tiers; it is set in the controller like so (and likewise on login, of course):
def set_current_user
User.current = (session[:user_id]) ? User.find_by_id(session[:user_id]) : nil
end
Among other things, this allows me to have audit logs at the ActiveRecord level that capture the current user (when applicable).

Where to put user access controls? The controller or the model?

I understand that we're supposed to avoid putting logic in the controller. So what is the proper way to implement things like user access controls. Let's suppose I have User, where each instance has a flag method admin? that determines whether the user can access information from other users.
Option 1: Put access controls in custom model
Model:
class User < ActiveRecord::Base
def self.get_list(accessor)
return [] unless accessor.admin?
self.all
end
end
Controller:
class UsersController < ApplicationController
def index
#users = User.get_list(current_user)
end
end
current_user would probably be defined somewhere in the application controller.
Option 2: Put access controls in the controller
Model:
class User < ActiveRecord::Base
end
Controller:
class UsersController < ApplicationController
def index
#users = current_user.admin? User.all : []
end
end
There are also some peripheral consequences such as where tests go and how they're implemented.
My instinct is that the first of the two options is preferable, but I've only ever used option 2 in the past. Also, it seems like the generally accepted (as far as I can tell) practice of putting on action-wide access filters is done at the controller level as in:
class UsersController < ApplicationController
before_filter :verify_logged_in
end
Any logic controlling routing of your application belongs in your controller. As for your example, the first is preferable but really there's not much in it.
It's easy for opinionated frameworks make us almost obsessive about doing things in a perceived correct way. In your example such a tiny amount of logic would, in my opinion, be perfectly fine to leave in your controller. If you were to abstract it, give it a descriptive name that described better what the method is doing otherwise you're simply making your code needlessly difficult to read.
As a rails rookie, the second option to me seems far easier to read and comphrehend. I prefer how I can read the single line in the controller and see exactly what you are doing.
In the first example, this logic is hidden someplace else (obviously the model, but I'm a rookie remember!) and seems slightly verbose (for this particular example).
I don't mean to suggest keeping it easier for new kids is A Good Thing, just pointing out a preference.

Resources