Best practice for handling two similar objects
Let's say I have two different controller/view sets that are basically identical.
Business hours and Staff hours
The first contains a business_id, the latter a staff_id. Otherwise they look the same. id, mon_start_time, mon_stop_time, tues_start_time, tues_stop_time, etc...
1) Is there a way I would use the same controller for these since they are so similar? It doesn't seem to make THAT much sense, but I just keep thinking about how similar they are and how much duplicate code there is.
开发者_开发百科2) Additionally, for each one I have a form in a partial. I'm trying to use the same one for both business hours and staff hours. The partial, in the most simplified state looks like:
-form_for(obj) do |f|
=f.error_messages %p =select obj.class, :mon_start_time, hours to =select obj.class, :mon_stop_time, hours=link_to 'Back', obj_path
So there are 3 unique things I need to pass in. The 'obj', either business_hours or staff_hours. That's okay in the form_for, but how do I get the lowercase controller name for the first parameter of the selects? I need 'business_hour', and 'staff_hour', from 'obj'. Then I need it to know the correct 'Back' link.
I know I can pass parameters into the partial, but I'm just curious if there's a slicker way of going about this. Thanks!
Duplicate code has a carrying cost, a cost to maintain it. We sometimes don't know how high that cost is until we refactor the duplicate code and find outselves breathing a sigh of relief: Now I can change the business rule in just one place. Now I can stop typing things twice.
You can use two controllers but still refactor the duplicate code. One way is to put the common code in a module included by both controllers.
module CommonStuff
def stuff_that_is_the_same
end
end
controller FooController < ApplicationController
include CommonStuff
def stuff_that_is_different
# Stuff specific to Foo
stuff_that_is_the_same
# More stuff specific to Foo
end
end
controller BarController < ApplicationController
include CommonStuff
def stuff_that_is_different
# Stuff specific to Bar
stuff_that_is_the_same
# More stuff specific to Bar
end
end
As far as getting the name of controller is concerned you can get it in any controller action by calling the method
controller_name
and the controller state is available in @controller instance variable to your views, so if you want to access it in your views then you can do
@controller.controller_name
Now looking at your BusinessHours and StaffHours classes, I would say the best thing to do here to make them polymorphic. The first thing you will achieve here is to get rid of an almost identical table. So check out the rails core polymorphic docs
NOTE: But the has_many_polymorphs mentioned by @amurmann is not yet available in rails core, though you can use it as a plugin. Pratik wrote a blog post about it here
For removing duplicate code from the controller, you can either put that in a module (as @Wayne said) or create a base controller from which your Business and Staff hours controllers inherit all the common functionality. Now the solution totally depends what makes more sense in your application. Personally, I will create a base controller as it is more OO, keep my classes structured and code will not be hidden in some module. But some people may think otherwise.
Have you looked into Single Table Inheritance? I think it's the all around best solution for you here.
Essentially you define an hours table and model, that looks exactly like either staff hours or business hours and includes a string column named type. Then you create subclasses of Hours to define methods and validations specific to Business and Staff hours respectively. You also might want to redefine your association to something more generic. In the example, I've called it keeper, as on one who keeps those hours.
In a nutshell rails is handling all the polymorphic behaviour for you.
Models:
class Hour < ActiveRecord::Base
# common associations/methods etc go here
end
class BusinessHour < Hour
belongs_to :keeper, :class_name => "Business"
#business hour specific logic
end
class StaffHour < Hour
belongs_to :keeper, :class_name => "Staff"
#staff hour specific logic
end
class Business < ActiveRecord::Base
has_many :business_hours, :foreign_key => :keeper_id
...
end
class Staff < ActiveRecord::Base
has_many :staff_hours, :foreign_key => :keeper_id
...
end
Route:
map.resources :business_hours, :controller => "hours", :class => "BusinessHour"
map.resources :staff_hours, :controller => "hours", :class => "StaffHour"
Controller:
class HoursController < ApplicationController
before_filter :select_class
def select_class
@class = Kernel.const_get(params[:class])
end
def new
@hour = @class.new
...
end
def show
@hour = @class.find(params[:id])
...
end
...
end
Views will look almost exactly what you have now, but you're going to want some helpers to keep the output straight.
module HourHelper
def keeper_type
@class == "BusinessHours" ? "Business" : "Staff"
end
end
-form_for(obj) do |f|
=f.error_messages
%p =select keeper_type, :mon_start_time, hours
to
=select keeper_type, :mon_stop_time, hours
=link_to 'Back', obj_path
You might also want to consider creating a custom form builder to simplify the form creation.
It sounds to me like you might be able to combine both into one resource. Let's just call the resource "hours", for this discussiion. The problem that "hour" might belong to either a business or staff could potentially be solved by making the relation polymorph.
You can for sure solve that relation problem with has_many_polymorphs:
class Hours < ActiveRecord::Base
has_many_polymorphs :participants, :from => [:staff, :business], :through => :hours_participants
end
This would require a new table and might not be the solution with the best performance. However, it should be a DRY solution.
精彩评论