Rails Controller inheritance and Modules
August 28th, 2007 Posted in ActionController, Object-Oriented ProgrammingThe topic of Controller inheritance may be straightforward for more advanced developers, but it isn’t completely straightforward for people new to Rails or the Ruby language in general.
Rails Controllers generally come in the following flavor:
class SomeController < ApplicationController
some_declarations_here
def public_method
end
private
def private_method
end
end
The Rails Controller inherits from a superclass, ApplicationController, which can be found in the app/controllers/application_controller.rb file. Anyone that is familiar with Object Oriented Programming knows, thus, that any method defined in ApplicationController is available in its subclasses. Take the following ApplicationController definition:
class ApplicationController < ActionController::Base def dance end end
Now we can access the method ApplicationController#dance in SomeController by way of the URI /some/dance. We can actually this this one step further:
class ApplicationController < ActionController::Base
layout 'fancy_pickle'
around_filter :check_for_fanciness
def dance
end
protected
def check_for_fanciness
if session[:fancy]
yield
else
render(:text => 'You are not fancy enough'
end
end
end
What did we just do here? With a few simple and powerful lines of code, we’ve done all of the following:
- We’ve defined a universal default layout for all the controllers in our application. The best part about this is that nothing is set in stone. We can easily override this in a sub controller with the definition:
layout 'fancier_pickle'
- We’ve defined an ‘around_filter’ that gets executed before ALL public actions in ALL controllers. This is very useful for almost any application requiring some kind of authentication or authorization matrix. The ‘yield’ statement is where the method being called is executed. In my example, if the session variable :fancy is set, that is the only way these methods would execute.
Let’s apply this into practice by defining two more controllers:
class OtherController < ApplicationController def sing end end
class AnotherController < ApplicationController
def yodel
end
end
What have we done? We’ve defined 2 more controllers that inherit from ApplicationController. That’s right, our Application now has methods at the following URIs:
/some/dance /some/public_method /other/sing /other/dance /another/yodel /another/dance
All of which are wrapped in a ‘check_for_fanciness’ around filter.
Let’s add one more level of complexity to our project. Suppose we need a method, ‘drink_water’, in our controllers which Yodel and Sing. One way we could do this would be to define it in ApplicationController, in accordance with DRY principles:
BAD!! BAD!!!
class ApplicationController < ActionController::Base
layout 'fancy_pickle'
around_filter :check_for_fanciness
def dance
end
def drink_water
end
protected
def check_for_fanciness
if session[:fancy]
yield
else
render(:text => 'You are not fancy enough'
end
end
end
While this will work, this is horrible style for at least two reasons. First, the method ‘drink_water’ will be available in SomeController, which does not have a Sing or Yodel method. Secondly, any future controllers we define will automatically inherit the method whether they should or not. How do we resolve this issue?
Enter Modules. In C++, we would use multiple inheritance. In Java, we would probably use some combination of Interfaces and Object aggregation. In Ruby, because Ruby is a weakly typed language (officially called ‘Duct Typing” - I’ll probably write about this in a future post), we would import a Module. It’s interesting, actually, that Ruby, which touts itself as such an Object Oriented language, would have a feature like this. For those developers familiar with PHP, this is basically the same thing as defining a bunch of methods in a file and then calling ‘require’ on the file. But - I digress. Let’s solve our problem at hand:
module DrinkMethods def drink_water
end end
This defines our Module. Let’s save it as drink_methods.rb, in /app/controllers, for simplicity’s sake - but for future reference it probably does not belong in that directly.
Now let’s modify our Controllers that need this method:
require 'drink_methods' class OtherController < ApplicationController import DrinkMethods def sing end end
require 'drink_methods'
class AnotherController < ApplicationController
import DrinkMethods
def yodel
end
end
Voila! Now we have access to the following URIs without introducing unnecessary inheritances:
/other/drink_water /another/drink_water
Now get out there and write some Ruby.