DDD on Rails Part 1


Removing Navigation Logic from Controllers

DDD on Rails Series

In this post I am going to talk about some problems I used to have writing controllers, and how I solved them while keeping the code clean and reusable.

The Dark Days

Back then, I used to write a lot of controllers that looked like this one:

class UsersController < ApplicationController
  before_filter :load_resources, :except => [:index, :show]

  def index
    if params[:id].present?
      @users = User.where("location_id = ? and id <> ?", params[:id], current_user.id)
    else
      @users = {
        "Users"    => User.users,
        "Admins"   => User.admins,
        "Managers" => User.managers
      }
    end

    respond_to do |format|
      format.html
      format.json { render :json => @users }
    end
  end

  def show
    @user = User.find(params[:id])

    respond_to do |format|
      format.html
      format.json { render :json => @user }
    end
  end

  def new
    @user = User.new

    respond_to do |format|
      format.html
      format.json { render :json => @user }
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def create
    @user = User.new params[:user]

    if @user.save
      flash[:notice] = "Success Message."
      redirect_to @user
    else
      flash[:error] = @user.errors.full_messages.first
      render :action => "new"
    end
  end

  def update
    @user = User.find(params[:id])

    if @user.update_attributes params[:user]
      flash[:notice] = "Success Message."

      if current_user.role == "admin"
        redirect_to @user
      else
        redirect_to root_path
      end
    else
      flash[:error] = @user.errors.full_messages.first
      
      render :action => "edit"
    end
  end

  def destroy
    @user = User.find(params[:id])
    @user.destroy

    respond_to do |format|
      format.html { redirect_to users_url }
      format.json { head :no_content }
    end
  end

  def load_resources
    @locations = Location.all
    @roles = Role.list
  end
end

I know, it is pretty horrible, shame on me!!! But I promise I won"t write this kind of thing anymore.

As you saw, there are a lot of problems out there... a lot of business logic, a lot of rendering logic, a lot of crap tied together in a great mess. I was not happy, specially when I had to change something. It was hard to understand, hard to change and harder yet to explain to my colleagues.

So I started my research... and the first thing I found was the Interactor gem.

The description reads:

"An interactor is a simple, single-purpose object. Interactors are used to encapsulate your application"s business logic. Each interactor represents one thing that your application does."

As they said, it is a single-purpose object, so I started migrating each action (Use Case) Logic from the controller to a single object. However the interactors folder quickly started to become crowded.

Therefore I gave up using this gem, went back to my research and read about a technique called Passive Controllers.

Part 1: Passive Controllers

A Passive Controller, basically, is an object responsible only for the rendering and redirecting. It delegates the Business Logic to some other object and passes itself as a parameter, becoming a listener that is notified after the processing is finished. This way I could write classes which grouped a lot of Use Case classes that shares Domain Logic.

With it we could do something like this and delegate all the logic to a business class:

def index
  BusinessClass.index(current_user, params[:id], self)
end

def show
  BusinessClass.find_resource(params[:id], self)
end

def new
  BusinessClass.build_resource(self)
end

def edit
  BusinessClass.find_resource(params[:id], self)
end

def create
  BusinessClass.save(params, self)
end

def update
  BusinessClass.save(params, self)
end

def destroy
  BusinessClass.destroy(params, self)
end

Cool, but as I said this business class uses the controll

Here we go:

def render_resource(resource)
  respond_to do |format|
    format.html { @resource = resource }
    format.json { render :json => resource }
  end
end

def destroy_success()
  respond_to do |format|
    format.html { redirect_to users_url }
    format.json { head :no_content }
  end
end

def save_success(user, message)
  flash[:notice] = message if message
  redirect message, user
end

def save_failure(user)
  action = user.id ? :edit : :new

  flash[:error] = user.errors.full_messages.first
  render :action => action
end

def redirect(message=", user=nil)
  flash[:notice] = message if message

  path = user ? user : root_path

  path = users_path if user.nil && current_user.admin?

  redirect_to path
end

Some of these callback methods could be placed in the ApplicationController to avoid duplication on every controller yout write, but today, they are going to stay here.

With this refactor we got rid of a lot of mess and 33 lines of code. But hey, wait... Where did all the business logic go?

I swept it under the rug!!!

Just kidding, I moved it to a class that I call Navigator.

OK, nice! Show me what this so called Navigator looks like!

Part 2: Navigators

A Navigator is a class responsible for the navigation logic and all the rendering and redirecting is repassed to the Controller using its callback methods.

Simplifying, it receives the data from the request, process it and executes the right callback. Pretty neat, huh?

Let me show you some code:

class UsersNavigator
  def index(current_user, location_id, listener)
    users = nil

    if location_id.present?
      users = User.where("location_id = ? and id <> ?", location_id, current_user.id)
    else
      users = {
        "Users"    => User.users,
        "Admins"   => User.admins,
        "Managers" => User.managers
      }
    end

    listener.render_resource(users)
  end

  def build_resource(listener)
    listener.render_resource(resource)
  end

  def find_resource(id, listener)
    listener.render_resource(get_resource(id))
  end

  def save(params, listener)
    user = params[:id] ? get_resource(params[:id]) : resource
    
    if user.update_attributes(params[:user])
      listener.save_success(user, "Success Message.")
    else
      listener.save_failure(user)
    end
  end

  def destroy(id, listener)
    user = get_resource(id)
    message = "Destroy failure"

    message = "Destroy Success" if user && user.destroy
    
    listener.redirect(message)
  end

  private
  def get_resource(id)
    return unless id

    User.find(id)
  end

  def resource
    User.new
  end
end

Now you know where the Business Logic went. This UsersNavigator class is our BusinessClass used on the Controller above, we can fix that so they refer to the correct class.

The Navigator separates the Navigation Logic from framework specific code and keep it clean and easy to refactor. You could even group navigators inside domain namespaces creating a better organization for your Business Logic.

With time your system grows and you can have a lot of code duplication as most of the Navigator methods are generic. Thinking on this, I wrote a simple gem (Navigators) that encapsulates these generic methods and prevent duplication.

Conclusion

The Controllers usually have a lot of Logic inside them, which sometimes are moved to the Models, creating monolithic classes that has a lot of responsibilities, breaking the Single Responsibility Principle (SRP) and bringing a lot of problems with them.

Using passive controllers we remove this logic from the Controllers and keep only the rendering and redirecting on it, restoring the single purpose of the class, and with Navigators, we create a home for the Navigation Logic from the Controllers so they are not just moved to the Models or to generic Service Classes and also uncouple them from the framework.

So, what you guys think of this approach? Tell me on the comments bellow.



comments powered by Disqus