Advanced non-flat controllers hierarchy with rails

ADSENSE HERE!
REST became the best practice for organizing CRUD server side interface.
But what to do when we have something more than hello-world application that
goes forward from data CRUD and provides many presentations
of the same data with different filters and different layouts?


How the flat controller architecture goes to hell



Starting from REST-full controller people use to stick to them and add more and more actions to the same class. And that is how the controller code goes to hell:

class UsersController < ApplicationController
#
# Filters
#
skip_before_filter :check_user_is_valid, :only => [:edit, :update]
before_filter :require_user, :only => [
:update, :edit, :settings, :disconnect_twitter, :connect_facebook_account, :google_contacts
]

before_filter :load_user, :except => [
:index, :new, :create, :remote_validate_email, :autocomplete_for_user_full_name,
:remote_validate_facebook_uid, :google_contacts
]

before_filter :check_permissions, :only => [ :edit, :update, :settings, :update_photo, :connect_facebook_account ]

before_filter :load_invitation, :only => [:new, :create]
before_filter :ensure_authenticated_to_facebook, :only => :connect_facebook_account
before_filter :initialize_form, :only => [:edit]

#
# And 20 almost independent actions goes here
# And the twice longer tests file for this controller
#

end


Namespaces and nesting


The root of the problem is in ignoring the following simple rule:
The default choice for implementing new feature is do it in other controller
It's always better to have 20 controllers with one action then one controller with 20 actions.
Usually there is no compromise variant. And 20 controllers with one action ain't as bad as you imagine.


In order to handle the large number of controller we are actively using the namespaces and nested resources features. Let me give an example: User has many projects, Project belongs to category. We need to list projects per user and per category. In the flat hierarchy you would be confused but namespaces and nesting solves the problem.

map.resources :categories do |c|
c.namespace :categories do |categories|
categories.resouces :projects
end
end
map.resources :users do |u|
c.namespace :users do |users|
users.resouces :projects
end
end

class Categories::ProjectsController

def index
@projects = ....
end
end

class Users::ProjectsController

def index
@projects = ....
end
end



Everyone heard about restful authentication but not so many people applied this idea to other not so restful from the first sight things. Like TwitterConnectionController:

class Users::TwitterConnectionController < ApplicationController

def create
end

def destroy
end
end


Many people will be pushing all such staff to UsersController until their editors would run out of memory.

Out of scope



State control actions are not in REST but should be one day. Placing them in the same controller with CRUD is generally a good idea:

class ArticlesController 

def {new, create, update, edit, destroy}
end

def publish
@article.publish!
redirect_to articles_path
end

end


Some sugar from generators



Rails generate script supports namespaces very well:
$ ./script/generate rspec_controller users/categories
create app/controllers/users
create app/helpers/users
create app/views/users/categories
create spec/controllers/users
create spec/helpers/users
create spec/views/users/categories
create spec/controllers/users/categories_controller_spec.rb
create spec/helpers/users/categories_helper_spec.rb
create app/controllers/users/categories_controller.rb
create app/helpers/users/categories_helper.rb


And classes created in appropriate namespace and folder as well as specs for them.
The only one thing you should do manually is add routes.

Drawbacks



Every engeneering solution has it's drawbacks. While you have different controllers that operates on the same classes you might need to reuse functionality among them.
I preffer to solve it by mixing in a module:
class Users::ProjectController

include UserNestedResource

end


Some people use inheritance instead. Both ways are almost the same. Nothing hard here as well.

Summary



At the end I 'll just repeat it again:

Default policy for placing two actions that has some kind of connection is SPLIT.
ADSENSE HERE!

2 comments:

  1. Useful stuff -- it solves an important problem.

    Is there maybe a typo in the second map.resources block? I suspect it should be "u.namespace :users do |users|..." rather than "c.namespace...".

    Thanks again.

    ReplyDelete
  2. Oh, this is quite interesting. Actulally, I found your blog on google search. I will tell my friend about your blog later.

    ReplyDelete

Komen dong, tapi yang sopan dan tidak spam ya

Copyright © Spesial Unik. All rights reserved. Template by CB. Theme Framework: Responsive Design