Abstract resource
A large portion of the internet is governed by HTTP and the World Wide Web in particular is designed based on the REST architectural style. It makes sense to design web applications or web based services in a way that respects and harnesses the web's underlying architecture.
When it comes to developing web applications, Model-View-Controller (MVC) is one of the dominant architectural patterns current web frameworks are based on. MVC is not restricted to building web apps, on the contrary, its history can be traced back to 1979 and Smalltalk and has been originally applied to the development of applications which involved user interfaces.
The majority of Ruby web frameworks, especially the ones inspired by Rails, employ MVC and offer some sort of support for REST style application development, typically by defining resources which can be accessed through a URI and manipulated by making use of standard HTTP methods such as GET, PUT, POST, DELETE.
The above unveils an obvious similarity between the way HTTP resources can be manipulated - the four verbs can fundamentally constitute CRUD operations - and another common tier in web applications nowadays, databases.
Controllers in
Merb,
Rails or other similar Ruby, or not, web frameworks are a busy abstraction. A controller typically needs to dispatch to relevant actions, consolidate HTTP payloads, deal with sessions, sometimes caching, etc. These controllers are usually REST aware, meaning that they will by default map routed URI HTTP operations to a standard set of actions, namely
index
,
show
,
create
,
edit
,
update
,
destroy
.
If we focus on our application exposing strictly REST resource based interfaces, and assume that these resources directly map to the application's database schema, we can relieve controllers from some of the associated strain by abstracting away the discussed common functionality.
module CrudTemplate def resource raise "You must define a resource" end def index instance_variable_set(resource_sym_plural, resource.find(:all)) render end def show assign_resource(resource.find(params[:id])) render end alias edit show alias delete show def new assign_resource(resource.new(resource_attrs)) render end def create r = resource.new(resource_attrs) assign_resource(r) if r.save on_create_success(r) else on_create_failure(r) end end def on_create_success(r) redirect(resource_sym) end alias on_update_success on_create_success def on_create_failure(r) assign_resource(r) render(:new, :status => 400) end def update r = resource.find(params[:id]) if r.update_attributes(resource_attrs) on_update_success(r) else on_update_failure(r) end end def on_update_failure(r) assign_resource(r) render(:edit) end def destroy if resource.destroy(params[:id]) on_destroy_success(r) else on_destroy_failure(r) end redirect(resource_sym) end def self.included(controller) controller.show_action(*shown_actions) end protected def resource_attrs {} end def self.shown_actions [:index, :show, :create, :new, :edit, :update] end private def assign_resource(r) instance_variable_set(resource_sym, r) end def resource_sym @resource_sym ||= :"@#{resource.name.underscore.split("/").last}" end def resource_sym_plural @resource_sym_plural ||= :"@#{resource.name.underscore.split("/").last.pluralize}" end end
By doing so, we can write controllers that look something like the following.
class Reservations < Application include CrudTemplate def resource Reservation end def on_create_success flash[:notice] = "Thank you" redirect("/") end protected def self.shown_actions [:new, :create] end def resource_attrs params[:reservation].merge(session[:member]) end end
Things are usually more complicated. The above model falls short for the majority of web applications I've worked on. Resources rarely are direct matches to database tables and there is usually good reason for them not to be. Applications involve complex business logic, spanning further from what a set CRUD operations is appropriate for. One might argue that business logic can be incorporated into Models (as in ORM classes), but I generally prefer to avoid keeping business logic near the persistence layer and opt for a database agnostic, rich domain tier.
This however doesn't imply that controllers shouldn't think in terms of resources. Controllers are close to the web, and the web works well with resources. It suffices for domain layer endpoints that intend to communicate with a controller to expose an interface the controller understands. If we define that interface so that it matches its database specific counterpart, we can achieve the best of both worlds.
Controllers can transparently operate on plain ruby components which include an
AbstractResource
module (interface) and choose to implement any of its methods, or directly on ORM models, such as
ActiveRecord
classes, where appropriate.
module AbstractResource attr_reader :params def initialize(params = {}) @params = params end def save raise "Implement me" end def update_attributes(attrs = {}) raise "Implement me" end def valid? raise "Implement me" end def errors raise "Implement me" end module ClassMethods def delete(id) raise "Implement me" end def find(id) raise "Implement me" end end def self.included(target) target.extend(ClassMethods) end end
P.S. Credit due to Carlos Villela whose observations have been the core and inspiration behind the ideas in this article.