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.

+3


source to share


3 answers


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.

+3


source


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.

+1


source


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

      

+1


source







All Articles