Share
Share on Twitter
Share on Facebook
Share on LinkedIn

Inheritance and Abstract Class Pattern for Ruby on Rails Controllers

 
Abstract Class pattern in Ruby is represented by an abstract painting. Photo by Steve Johnson from Pexels

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
Source code has been simplified for brevity.


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
Part of the implementation has been skipped because it's not relevant for this blog post.


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.



Pawel Urbanek Full Stack Ruby on Rails developer avatar

I'm not giving away a free ebook, but you're still welcome to subscribe.


Back to index