
Inheritance is often frowned upon, because “You wanted a banana but got the whole jungle…“. In some scenarios, it can be a viable alternative to modules composition for sharing behavior. In this tutorial, I will describe a practical use case where using abstract base class pattern plays well with Ruby on Rails controllers layer.
Read on if you want to find out how to “write Java in Ruby”.
Theoretical example
Let’s have a look at a sample implementation of an abstract class using plain old Ruby objects:
class Fruit
def initialize
raise "Cannot initialize an abstract Fruit class"
end
def tasty?
true
end
def eat
puts "I'm eating a #{description}."
end
private
def description
raise NotImplementedError
end
end
class Apple < Fruit
attr_reader :color
def initialize(color)
@color = color
end
private
def description
"#{color} apple"
end
end
Fruit.new => RuntimeError ("Cannot initialize...")
apple = Apple.new("red")
apple.tasty? => true
apple.eat => "I'm eating a red apple."
Base Fruit class cannot be instantiated directly, but it (as much as Ruby allows to) declares an interface expected from its children by explicitly stating that the description
method is not implemented although it is used in an eat
method.
In this theoretical example, the base Fruit
class knows how to perform the eat
action but delegates the task of describing themselves to its more specialized child classes.
Let’s move on to a more practical example to see how a similar approach can be used to work with Ruby on Rails controllers.
Rails controllers using abstract base classes
Following code samples come from Abot, a Slack plugin that allows sending anonymous feedback messages. The project has two types of HTTP interfaces. A standard one for rendering web pages in the browser, and the other for interacting with the Slack API calls.
Let’s have a look at two abstract base controller classes:
app/controllers/web/base_controller.rb
class Web::BaseController < ApplicationController
before_action :set_headers
layout "web"
rescue_from AuthenticationError do |e|
ExceptionNotifier.notify_exception(e)
flash[:error] = "Access denied."
redirect_to root_path
end
private
def set_headers
response.headers["Content-Security-Policy"] = "..."
...
end
end
app/controllers/slack/base_controller.rb
class Slack::BaseController < ApplicationController
before_action :check_slack_signature!, :check_permission!
layout "slack"
rescue_from PermissionError do |e|
render "slack/base/permission_error", locales: {
error: e.message
}
end
private
def check_slack_signature!
# Verify API request origin
...
end
def check_permission!
raise NotImplementedError
end
end
As you can see, controllers are defining shared behavior for each type of HTTP interface. Eg. Web base controller takes care of setting the correct security headers. Slack base controller verifies whether the requests are originating from the official Slack API and have not been tampered with.
Base controllers also implement the error handling. Whenever any of the child classes raises an exception it is propagated to the parent that knows how to deal with it.
I call those base classes abstract because they are never directly referenced in the config/routes.rb
file. In this context, routing to the controller class is an equivalence of directly instantiating an object. Base controllers can still implement the view (eg. index
show
) methods, but only if they are supposed to be inherited.
Now let’s have a look at the sample child class of a Slack base controller:
app/controllers/slack/direct_feedbacks_controller.rb
class Slack::DirectFeedbacksController < Slack::BaseController
def create
# Logic for rendering the feedback dialog using Slack API
...
render :create
end
private
def check_permission!
return if current_team.permit_direct?
raise PermissionError, "Direct feedback messages are disabled for your team."
end
end
Teams using Abot can fine-tune how they want to apply anonymous communication features, and optionally disable e.g. direct messages. The logic for that differs per controller and is determined by check_permission!
method.
As you can see, the Slack::DirectFeedbacksController
child class implements the check_permission!
method that has been explicitly defined as raising NotImplementedError
in its parent. Because Slack::BaseContoller
runs the check_permission!
in before_action
we are forced to implement its more specific version in the child class. The described approach guarantees that we always have to implement the “interface” defined by the parent class.
Different child controller e.g., Slack::ChannelFeedbacksController
would have a separate implementation of check_permission!
method, validating that the team has enabled channel messages and that a target channel is whitelisted for anonymous communication.
Summary
I am using this approach for controllers that encapsulate the common behavior. Being more explicit and verbose by declaring the abstract methods in the parent class can help you avoid the mistake of forgetting to implement a more specialized method down the inheritance chain.
An additional advantage is that extracting the abstract base controllers can help you clearly separate different types of HTTP interfaces for your Rails app.