Extracting many models from one controller in a RESTful project
I am trying to dry out the code and I would like to hear some opinions.
Background
We have a set of roughly 15-20 classes that are nearly identical at the model level, but represent different data. Therefore, they are stored in separate database tables and share app/models/*.rb
and exchange files , including modules. This works great.
We need read-only access to these classes through our REST API. We use MetaSearch to pass the search parameters to the model layer, which also works great.
Problem
I don't want to write a new controller and view (and because of how the API was designed, the helper) for each of these models. They will all be nearly identical, as well as 50+ redundant files.
How can I avoid this?
My first thought:
- one controller that delegates the appropriate model class to find records, and
- one kind (
index
) that renders the records as JSON because that's really all we need
It is trivial to define the model class from the URL parameters, and the view is just a call model.as_json
. I love this solution, but I feel like I might break the RESTful design by using one controller to manage many models (but keep in mind that the only action is index
).
Would be better:
- Create one controller and view each model and share with helpers and other modules? This is most explicitly RESTful and allows me to use
resources
routes in the file, but it is horribly repetitive and results in a lot of almost empty classes. - Create
SuperController
and inherit from this controller in other 15-20 controllers? This may allow me to make better use of template inheritance, but it still results in a lot of almost empty classes. - Do something else?
Thanks for any suggestions.
Update: I think this question can only be that REST wins DRY or vice versa. A RESTful design would result in a lot of empty or duplicate controllers, which violates DRY. The DRY construct will result in multiple display of controller models, which breaks REST. So it might come down to personal preference, but I'd still like to hear what others think.
source to share
You might want to take a look at inherited_resources . It can be used to dry out your controllers, and for a simple case (like yours presumably) can shrink your hand controller by just a couple of lines.
If your controllers are still too similar, you can also apply some metaprograms and create 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). For more information, see one of the many usage articles .
Edit: The metaprogramming technique will help you avoid copying and pasting many identical controllers. You should put as much code as you can in the common parent class (or some included modules). After all, maintaining multiple nearly empty classes is not basic since you are not copying complex code.
The above example can also be expressed with less metaprogramming, but is exactly the same as the following example. This approach is probably more natural. It still gives you almost all the benefits of the full meta topic 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 view, I've included a simple extension mechanism. In child classes, you can pass a block into the call super
to perform additional steps that might be required for a slightly special look.
source to share
Inheritance. You might get confused by doing a few loops to create your classes as others have presented, but this is not very explicit. It just makes it harder to maintain and understand (especially if others come and support your application). It might take a little more time and code to implement, but I'll still have something like this ...
class UberController < ApplicationController
def index
render :text => self.class.name
end
end
class SubController < UberController; end
class UnderController < UberController; end
The same basic concept for your models. You can always poll the actual class from the superclass to be sure where you are, or you can implement specific details in the subclasses. At least this way it is closer to a 1-to-1 implementation for REST sake and more understandable to be explicit.
source to share
For your API, it is most important that it is RESTful, but for your implementation, it is most important that it is DRY. So you are absolutely right if you want to find a DRY way.
I think a good way is to create 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 create a controller for each model that diverges from the common one that inherits from your api controller and then just add a route to that controller above your common route.
I thought about using metaprogramming or some other hack to make this work dynamically without adding entries to your routes like the other answers, but it seems to me that it doesn't cost me. If played correctly, this will trigger a maximum of 2 additional routes in your table. One for the generator api controller and one that displays the list of exception controllers.
I made a small example of an exercise:
class GenericAPIController < ApplicationController
def model
params[:model].classify.constantize
end
def show
model.find(params[:id]).to_json
end
end
source to share