Reputation: 14983
I'm trying to DRY up some code, and I'd like to hear some opinions.
We have a set of about 15-20 classes that are nearly identical at the model level but represent different data. They are therefore stored in separate database tables and separate app/models/*.rb
files and share behavior by including modules. That works fine.
We need read-only access to these classes through our REST API. We're using MetaSearch to pass search parameters to the model layer, which also works fine.
I don't want to write a new controller and view (and because of the way the API was designed, helper) for each of these models. They will all be practically identical, and that's 50+ redundant files.
How can I avoid doing that?
My first thought:
index
) that renders the records as JSON, because that's really all we needIt's trivial to determine the model class from the URL parameters, and the view is essentially just a call to model.as_json
. I like this solution, but I feel like I might be violating RESTful design by using one controller to manage many models (but keep in mind that the only action is index
).
Would it be better to:
resources
in my routes file, but it's awfully repetitive and results in a lot of nearly empty classes.SuperController
and inherit from this controller in the other 15-20 controllers? This might allow me to take better advantage of template inheritance, but it still results in a lot of nearly empty classes.Thanks for any suggestions.
Update: I think this question might just be about whether REST trumps DRY or vice versa. A RESTful design would result in a lot of empty or repetitive controllers, which violates DRY. A DRY design would result in a many-to-one mapping of models to controllers, which violates REST. So it might just come down to personal preference, but I'd still like to hear what others think.
Upvotes: 3
Views: 2178
Reputation: 607
For your API it's most important that it's RESTful, but for your implementation it's most important that it's DRY. So your are absolutely right for looking for a way to DRY this up.
I think a good way is to make a generic GenericAPIController in your controllers directory. You can define a route that routes all api requests to this controller.
The easiest way to handle exceptions is to make a controller for each model that diverges from the generic one that inherits from your api controller, and then just add a route to that controller above your generic route.
I have thought about the use of metaprogramming or other hacking to make this work dynamically without adding entries to your routes like the other answers, but it doesn't sound like it's worth it to me. If you play it right this will cause a maximum of 2 extra routes in your table. One for the generic api controller and one that regexes a list of exception controllers.
I made a small example as an exercise:
class GenericAPIController < ApplicationController
def model
params[:model].classify.constantize
end
def show
model.find(params[:id]).to_json
end
end
Upvotes: 1
Reputation: 55888
You might want to have a look at inherited_resources. It can be used to dry up your controllers and for simple case (like yours apparently) can reduce your manual controller to just a couple of lines.
If your controllers are are still too similar then, you could also apply some meta-programming and create the controller classes on the fly in some initializer like this
%w[Foo Bar Baz].each do |name|
klazz = Class.new(ApplicationController) do
respond_to :html, :json
def index
@model = name.constantize.find(params[:id])
respond_with @model
end
end
Kernel.const_set("#{name}Controller", klazz)
end
This code will create three minimal controllers called FooController
, BarController
, and BazController
.
If you are just calling model.to_json
in your views, you don't need views at all. Just use respond_to
and respond_with
(inherited_resources and my example code above do that). See one of the many articles about its usage for more information.
Edit: The meta-programming approach would help you avoid the copy&paste of many identical controllers. You still should put as much code as you can in a common parent class (or some included modules). In the end, maintaining a couple of almost empty classes isn't that bas as you don't copy complex code.
The example above could also expressed with less meta-programming but exactly the same behavior like the following example. This approach is probably a bit more natural. It still gives you almost all the advantages of the full-meta approach.
class MetaController < ApplicationController
respond_to :html, :json
def index(&block)
@model = model.find(params[:id])
instance_eval(&block) if block_given? # allow extensions
respond_with @model
end
protected
def model
@model_class ||= self.class.name.sub(/Controller$/, '').constantize
end
end
class FooController < MetaController
end
class BarController < MetaController
def index
super do
@bar = Specialties.find_all_the_things
end
end
end
class BazController < MetaController
end
As another point of thought, I included a simple extension mechanism. In child classes, you can pass a block to the super
call to perform additional steps which might be required by a slightly special view.
Upvotes: 3
Reputation: 2161
Inheritance. You could get tricky by doing some looping through to create your classes as others have presented, but that is not very explicit. That just makes it harder to maintain and understand (particularly if others have to come along and maintain your app). This may take a bit more time and code to implement, but I would still go with something like this...
class UberController < ApplicationController
def index
render :text => self.class.name
end
end
class SubController < UberController; end
class UnderController < UberController; end
Same basic concept for your models, too. You can always interrogate the actual class from the super class to make sure of where you are, or you can implement specific details in the subclasses. At least this way it's closer to a 1-to-1 implementation for RESTs-sake, and more comprehensible for being explicit.
Upvotes: 1